1. Spring Web MVC 自动配置概览
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多数场景我们都无需自己配置)
The auto-configuration adds the following features on top of Spring’s defaults:
- Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans.- 内容协商视图解析器和 BeanName 视图解析器
- Support for serving static resources, including support for WebJars (covered later in this document).
- 静态资源(包括 webjars)
- Automatic registration of
Converter
,GenericConverter
, andFormatter
beans.- 自动注册
Converter
,GenericConverter
,Formatter
- 自动注册
- Support for
HttpMessageConverters
(covered later in this document).- 支持
HttpMessageConverters
,配合内容协商理解原理
- 支持
- Automatic registration of
MessageCodesResolver
(covered later in this document).- 自动注册
MessageCodesResolver
(国际化)
- 自动注册
- Static
index.html
support.- 静态
index.html
页面支持
- 静态
- Automatic use of a
ConfigurableWebBindingInitializer
bean (covered later in this document).- 自动使用
ConfigurableWebBindingInitializer
,(DataBinder 负责将请求数据绑定到 JavaBean 上)
- 自动使用
If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration
class of type WebMvcConfigurer
but without @EnableWebMvc
.
不使用
@EnableWebMVC
注解,使用@Configuration
+@WebMvcConfigurer
自定义规则
If you want to provide custom instances of RequestMappingHandlerMapping
, RequestMappingHandlerAdapter
, or ExceptionHandlerExceptionResolver
, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations
and use it to provide custom instances of those components.
声明
WebMvcRegistrations
改变默认底层组件
If you want to take complete control of Spring MVC, you can add your own @Configuration
annotated with @EnableWebMvc
, or alternatively add your own @Configuration
-annotated DelegatingWebMvcConfiguration
as described in the Javadoc of @EnableWebMvc
.
使用
@EnableWebMvc
+@Configuration
+DelegatingWebMvcConfiguration
全面接管 SpringMVC
2. 简单功能分析
2.1 静态资源访问
2.1.1 默认静态资源目录
默认的静态资源访问路径:/static、/public、/resources 与 /META-INF/resources【2.3 小节会进行源码分析,剖析这 4 个路径是由何而来的】
访问方式:当前项目根路径/静态资源名(如:localhost:8080/pic.jpg
)
⭐若静态资源名与 Controller 请求名相同(
pic.jpg
与@RequestMapping("/pic.jpg")
),则先进行业务处理,无法处理再交给静态资源处理器,若没有对应的静态资源,则返回 404 页面。
当然,我们也可以改变 Spring Boot 提供的默认静态资源目录位置:
# 改变静态资源文件夹【/public、/resources、/static、/META-INF/resources】为【/place、/ano-place】
spring:
resources:
static-locations: [classpath:/place/, classpath:/ano-place/]
2.1.2 静态资源访问前缀
我们可能需要为静态资源添加访问前缀,因为 Filter 会过滤掉静态资源【拦截所有请求:/**
】,而我们可以修改前缀从而在拦截的所有请求下配置放行:/res/静态资源名
spring:
mvc:
static-path-pattern: /res/**
2.1.3 webjars
<!-- 支持 WebJars: 将 BootStrap、JQuery 等打成了Jar包 -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
访问 http://localhost:8080/webjars/jquery/3.5.1/jquery.js
(和依赖的包路径要一致,至于为什么该路径能访问到,详见下文源码分析):
2.2 Welcome Page & favicon
Spring Boot 既支持静态 (static) 欢迎页面,也支持模板 (template) 欢迎页面。 它首先在配置的静态内容位置中查找 index.html 文件。 如果没有找到,则查找索引模板。 如果找到其中一个,它将自动用作应用程序的欢迎页面。
index.html & favicon 都需要放置在静态资源目录下:
注意:不可以配置静态资源的访问前缀 (spring.mvc.static-path-pattern
),否则会导致 index.html 无法被默认访问!
2.3 静态资源配置原理【源码分析🔥】
我们都知道 Spring Boot 启动会默认加载 xxxAutoConfiguration
自动配置类,所以 Spring MVC 功能的自动配置类 WebMvcAutoConfiguration
也会生效。
如果配置类只有一个有参构造器,那么该构造器所有的参数的值都会从容器中获取!
Tip:以下源码有标注注释,这些注释都是有助于理解源码内容及其配置原理的!
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
...
@Configuration
@Import(EnableWebMvcConfiguration.class)
// @EnableConfigurationProperties 注解绑定了 WebMvcProperties、ResourceProperties 配置文件
// @EnableConfigurationProperties 同时将 WebMvcProperties 组件与 ResourceProperties 组件加入到容器中,方便下文注入
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
...
// 该类唯一的有参构造函数,所以所有参数的值都会从容器中获取!【包括WebMvcProperties与ResourceProperties】
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
}
}
...
}
ResourceProperties
属性绑定以 spring.resources
为前缀的值,WebMvcProperties
属性绑定 spring.mvc
为前缀的值:
所以现在配置类已经从容器中获取到相应的值了,接下来:
public class WebMvcAutoConfiguration {
...
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 当 spring.resources.add-mappings=false 时,if语句判断正确,日志打印如下语句,并返回...
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
// webjars下的所有请求都会转到 classPath:/META-INF/resources/webjars 路径下,完美地解释了 2.1.3 小节的疑问!
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
// 此处获取到 MvcProperties 绑定的配置文件值,即 spring.mvc.static-path-pattern 的值!
// 若 yaml 文件中没有配置该值,staticPathPattern 也有默认值:/** 【默认资源访问路径】
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
// 与上面webjars请求访问原理相同,staticPathPattern【/**】的请求都会到getResourceLocations(this.resourceProperties.getStaticLocations()下去找!
// getResourceLocations(this.resourceProperties.getStaticLocations() 指向哪里?别急,看下图!
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
...
}
我们进入 this.resourcesProperties.getStaticLocations()
中发现:
是不是茅塞顿开有没有?!原来静态资源的默认访问路径底层早已配置好,其来龙去脉我们也大致搞清楚了。
然后还有 Welcome Page【即 index.html】为什么放在静态资源目录下会被 Spring Boot 自动识别?
为了解释以上这个问题,我们再来翻一翻源码(同样还是在 WebMvcAutoConfiguration
类下查找):
我们在解析这段源码之前,我们得知道
HandlerMapping
是做什么的:处理器映射,其中保存了每一个Handler
所能处理的请求【某些请求对应某些处理器】,并使用反射调用处理方法。所以
WelcomePageHandlerMapping
就是专门用于处理 Welcome Page 的处理器。
WebMvcAutoConfiguration.java:
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors());
return welcomePageHandlerMapping;
}
WelcomePageHandlerMapping
实例化并从容器中获取参数,然后我们进去 WelcomePageHandlerMapping
类中:
final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping {
...
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
// 如果欢迎页存在,且静态资源请求为 /**【无前缀】,则可以直接在项目启动时跳转到 index.html
// 所以当我们配置了 spring.mvc.static-path-pattern 值后,index.html 就无法被自动识别并展示了【因为此时不等于 /**】
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage.get());
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
...
}
application.yaml(以上注释提及的配置文件属性):
spring:
resources:
add-mappings: false # 禁用所有静态资源!
static-locations: [classpath:/place1, classpath:/place2] # 修改默认的静态资源路径
mvc:
static-path-pattern: /res/** # 添加静态资源访问前缀,方便放行
静态资源配置原理分析大致就这些,感兴趣的小伙伴可以自己去详细解读下 WenMvcAutoConfiguration
...
3. 请求参数处理
3.1 RESTful 风格
RESTful 风格支持:可以使用 HTTP 请求方式动词表示对资源的操作。
- Spring MVC:GET、POST、DELETE、PUT
- 普通浏览器只支持 GET、POST 方式,并不支持其他方式,所以 Spring Boot 通过增加过滤器来增加支持 DELETE、PUT 请求的方式。
获取 Resultful 风格的参数:juejin.cn/post/699580…
相关注解:
注解 | 概述 |
---|---|
@RestController | 由 @Controller + @ResponseBody 组成(返回 JSON 数据格式) |
@PathVariable | URL 中的 {xxx} 占位符可以通过 @PathVariable("xxx") 绑定到控制器处理方法的形参中 |
@RequestMapping | 请求地址的解析,是最常用的一种注解 |
@GetMapping | 查询请求(等价于 @RequestMapping(path = "", method = RequestMethod.GET) ) |
@PostMapping | 添加请求(同上) |
@PutMapping | 更新请求(同上) |
@DeleteMapping | 删除请求(同上) |
@RequestParam | 参数绑定:juejin.cn/post/699580… |
实现 RESTful 风格的核心:HiddenHttpMethodFilter
- 用法:表单
method=post
、隐藏域、_method
、delete
!- 例如:
<input name="_method" type="hidden" value="delete">
- 例如:
- Spring Boot 中
HiddenHttpMethodFilter
默认开启。 - Rest 原理(表单提交要使用 REST 的时候)
- 表单提交会带上
_method=PUT
- 请求过来被
HiddenHttpMethodFilter
拦截- 判断:请求是否正常,是否为 POST
- 获取到 _method 的值
- 兼容以下请求:PUT、DELETE、PATCH
- 包装模式
requesWrapper
重写了原生request(post)
的getMethod
方法,返回的是传入的值- 过滤器链放行的时候用
wrapper
,以后的方法调用getMethod
是调用requesWrapper
,即包装后getMethod
- 过滤器链放行的时候用
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class) // 用户自定义Filter优先
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = true)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
如上源码显示,如果我们没有配置 spring.mvc.hiddenmethod.filter.enable
,则默认开启,所以如下代码可有可无:
spring:
mvc:
hiddenmethod:
filter:
enable: true # "defaultValue": true
前后端具体代码:
page.html
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit"/>
</form>
<!--当且仅当在POST方式下提交,PUT、DELETE、POST才有效!【底层源码规定】-->
<form action="/user" method="post">
<input name="_method" type="hidden" value="POST"/>
<input value="REST-POST 提交" type="submit"/>
</form>
<form action="/user" method="post">
<!-- _method这一input标签用于标识提交方法! -->
<input name="_method" type="hidden" value="PUT"/>
<input value="REST-PUT 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="delete"/>
<input value="REST-DELETE 提交" type="submit"/>
</form>
Controller.java
//@RequestMapping(value = "/user",method = RequestMethod.GET)
@GetMapping("/user")
public String getUser(){
return "GET-张三";
}
//@RequestMapping(value = "/user",method = RequestMethod.POST)
@PostMapping("/user")
public String saveUser(){
return "POST-张三";
}
//@RequestMapping(value = "/user",method = RequestMethod.PUT)
@PutMapping("/user")
public String putUser(){
return "PUT-张三";
}
//@RequestMapping(value = "/user",method = RequestMethod.DELETE)
@DeleteMapping("/user")
public String deleteUser(){
return "DELETE-张三";
}
拓展,若我们要自己自定义 Filter
的 MethodParam
:
@Configuration
public class WebConfig {
// 替换 _method :自定义
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
hiddenHttpMethodFilter.setMethodParam("_meth");
return hiddenHttpMethodFilter;
}
}
此时请求为:<input name="_meth" type="hidden" value="delete">
,后端才能响应 PUT
、PATCH
、DELETE
。
注意:REST 风格的参数只能适用于表单的提交,如果用户直接从客户端提交(如:POSTMAN),那么 request.getMethod()
不再是表单的 POST
然后再进行封装发送,不再会被 HiddenHttpMethodFilter
所拦截, 而是直接以 PUT
、PATCH
、DELETE
等方法直接作为 HTTP 请求方式。
Rest 映射及其源码解析:www.bilibili.com/video/BV19K…
3.2 请求参数映射原理
handlerMappings = {ArrayList} size=5
(5 个 HandlerMapping
,其中 RequestMappingHandlerMapping
用于请求映射):
RequestMappingHandlerMapping
:保存了所有@RequestMapping
和handler
的映射规则WelcomePageHandlerMapping
BeanNameUrlHandlerMapping
RouteFunctionMapping
SimpleUrlHandlerMapping
源码解析:www.bilibili.com/video/BV19K…
3.3 注解参数
回顾一下这些注解:juejin.cn/post/699580…
@PathVariable
@RequestHeader
@RequestParam
@CookieValue
@RequestBody
@RequestAttribute
@MatrixVariable
3.3.1 @PathVariable
、@RequestHeader
、@RequestParam
、@CookieValue
、@RequestBody
index.html
<!-- getParam() -->
<a href="/user/1/owner/kun?age=19&teachers=w&teachers=Q">/user/1/owner/kun?age=19&teachers=w&teachers=Q</a>
<!-- testRequestBody() -->
<form action="/save" method="post">
测试 @RequestBody 获取表单参数!<br/>
Username: <input name="username">
Email: <input name="email">
<input type="submit" value="提交">
</form>
Controller
@RestController
public class ParamTestController {
// 请求:/user/1/owner/kun
@RequestMapping("/user/{id}/owner/{username}")
public Map<String, Object> getParam(@PathVariable("id") String id,
@PathVariable("username") String username,
@PathVariable Map<String, String> pvMap,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String, String> headerMap,
@RequestParam("age") String age,
@RequestParam("teachers") String[] teacher,
@RequestParam Map<String, String> requestMap,
@CookieValue("Idea-7eb7317e") String cookieValue,
@CookieValue("Idea-7eb7317e") Cookie cookie) {
// @PathVariable
Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("username", username);
// pvMap: 所有的 @PathVariable 键值对! (必须是 Map<String, String>)
map.put("pvMap", pvMap);
// @RequestHeader
map.put("userAgent", userAgent);
// map.put("header", headerMap); //所有的Header!
// @RequestParam
map.put("age", age);
map.put("teacher", teacher);
// requestMap: @ReqeustParam 标记的键值对!
map.put("requestMap", requestMap);
// @CookieValue
map.put("cookie", cookieValue);
System.out.println(cookie.getName() + ": " + cookie.getValue());
return map;
}
@RequestMapping("/save")
public Map<String, String> testRequestBody(@RequestBody String content) {
Map<String, String> map = new HashMap<>();
map.put("表单的所有内容", content);
return map;
}
}
3.3.2 @RequestAttribute
@Controller
public class RequestAttributeController {
@RequestMapping("/goto")
public String gotoPage(HttpServletRequest request) {
request.setAttribute("message", "测试@RequestAttribute成功!");
request.setAttribute("code", "200 OK");
// 转发给 /success 处理请求
return "forward:/success";
}
@ResponseBody
@GetMapping("/success")
public Map<String, Object> success(@RequestAttribute("message") String msg,
@RequestAttribute("code") String code,
HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
map.put("annotMessage", msg);
map.put("code", code);
String requestMsg = (String) request.getAttribute("message");
map.put("requestMsg", requestMsg);
return map;
}
}
3.3.3 @MatrixVariable
与 UrlPathHelper
矩阵变量 @MatrixVariable
格式:关于矩阵变量:根据 RFC3985 的规范,矩阵变量【@MatrixVariable】应当绑定在路径变量【@PathVariable】中。若是有多个矩阵变量,应当使用英文符号 ;
进行分隔,若是一个矩阵变量有多个值,则应当使用英文符号 ,
及进行分隔,或命名多个重复的 key 也可。
/cars/sell;low=34;brand=bm,byd,aodi
:一个矩阵变量多个值,
分隔/boss/1;age=20/2;age=10
:把矩阵参数看称是路径变量的一部分,注意每段 URI 都可以有;
/abc;jsessionid=xxxx
:假如页面开发中 Cookie 被禁用了,而 session 是通过 jsessionid 绑定在 cookie 上的,所有 session 同时也被禁用;若想在 Cookie 被禁用的情况下使用 session,则可以使用矩阵变量,区分与一般的路径请求。
测试 @MatrixVariable
:
@ResponseBody
// 请求URI:/cars/sell;low=199;brand=byd,baoma,yd
@RequestMapping("/cars/{path}")
public Map testMatrixVariable(@MatrixVariable("low") String low,
@MatrixVariable("brand") String[] brand,
@PathVariable("path") String path) {
Map<String, Object> map = new HashMap<>();
map.put("low", low);
map.put("brand", brand);
map.put("pathVariable",path);
return map;
}
结果:
嗯?这是怎么回事?这里先给出结论,然后我们再到底层源码去看看为什么矩阵变量失效了!
🔥结论:矩阵变量需要在 Spring Boot 中手动开启(默认关闭)!
我们还是从 WebMvcAutoConfiguration
找起:
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
//...
@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
//...
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern());
configurer.setUseRegisteredSuffixPatternMatch(
this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern());
}
//...
}
//...
}
单刀直入,WebMvcAutoConfigurationAdapter
实现了 WebMvcConfigurer
这个接口,并重写了 configurePathMatch(PathMatchConfigurer configurer)
,我们再点击进入 PathMatchConfigurer
这个形参类,得知其中有一个 setter
方法,即 setUrlPathHelper()
,这个 UrlPathHelper
就是该节的 "主角"。
UrlPathHelper
类是 Spring 中的一个帮助类,有很多与 URL 路径有关的实用方法,矩阵变量的失效也毫不例外与它有关;所以我们继续进入到它的源码当中:
既然底层默认移除分号,那么我们要开启矩阵变量的话,就将 UrlPathHelper
下的 removeSemicolonContent
变量设置为 false
,然后设置到 PathMatchConfigurer
类中,那么我们就得重写 WebMvcConfigurer
接口下的 configurePathMatch
方法,并将 WebMvcConfigurer
注入到容器中。
方法一:
@Configuration
public class WebConfig{
@Bean
public WebMvcConfigurer getWebMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 不移除分号: 矩阵变量生效!
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
}
方法二:
public class WebConfig implements WebMvcConfigurer{
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 不移除 ; 后面的内容,即开启矩阵变量!
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
再次用矩阵变量访问试试 (请求URI:/cars/sell;low=199;brand=byd,baoma,yd
):
再试试另一种特殊点的矩阵变量 (请求URI:/wpath/1;age=31/143;age=19
):
这里的 pathVar 猜也可以猜到是路径变量的名字,因为矩阵变量必须依附在路径变量上,这里用于区分不同段 URI 中矩阵变量名相同的情况。
@MatrixVariable(pathVar = "bossId", value = "age")
@ResponseBody
// 请求 URI: /multiPath/1;age=31/143;age=19
@RequestMapping("/multiPath/{bossId}/{employId}")
public Map testMultiMatrixVariable(@MatrixVariable(pathVar = "bossId", value = "age") int bossAge,
@MatrixVariable(pathVar = "employId", value = "age") int employAge,
@PathVariable("bossId") String path1,
@PathVariable("employId") String path2) {
Map<String, Object> map = new HashMap<>();
map.put("bossAge", bossAge);
map.put("employAge", employAge);
// 这里也顺便看看路径变量吧【矩阵变量是依附在路径变量上的,切记】
map.put("bossId", path1);
map.put("employId", path2);
return map;
}
这里再补充点 UrlPathHelper
的知识:
public String removeSemicolonContent(String requestUri) {
return (this.removeSemicolonContent ?
removeSemicolonContentInternal(requestUri) : removeJsessionid(requestUri));
}
private String removeSemicolonContentInternal(String requestUri) {
int semicolonIndex = requestUri.indexOf(';');
while (semicolonIndex != -1) {
int slashIndex = requestUri.indexOf('/', semicolonIndex);
String start = requestUri.substring(0, semicolonIndex);
requestUri = (slashIndex != -1) ? start + requestUri.substring(slashIndex) : start;
semicolonIndex = requestUri.indexOf(';', semicolonIndex);
}
return requestUri;
}
private String removeJsessionid(String requestUri) {
int startIndex = requestUri.toLowerCase().indexOf(";jsessionid=");
if (startIndex != -1) {
int endIndex = requestUri.indexOf(';', startIndex + 12);
String start = requestUri.substring(0, startIndex);
requestUri = (endIndex != -1) ? start + requestUri.substring(endIndex) : start;
}
return requestUri;
}
removeSemicolonContent
方法- 根据
removeSemicolonContent
属性决定是移除请求URI中的所有分号内容还是只移除jsessionid
部分,默认是前者,所以这两种情况都会移除jsessionid
部分
- 根据
removeSemicolonContentInternal
方法- 移除请求 URI 中所有的分号内容,注意 URI 中每段都可以有分号,如
/users/name;v=1.1/gender;value=male
等形式;
- 移除请求 URI 中所有的分号内容,注意 URI 中每段都可以有分号,如
removeJsessionid
方法- 只移除请求 URI 中,
jsessionid=xxx
的部分而保留 URI 的其余部分(包括其他分号),移除jsessionid
时不区分大小写。
- 只移除请求 URI 中,
3.4 Servlet API
Spring MVC 除了给参数标注一些注解,参数解析器【HandlerMethodArgumentResolver】还可以解析一些作为参数的 Servlet API:
- WebRequest
- ServletRequest
- MultipartRequest
- HttpSession
- PushBuilder
- Principal
- InputStream
- Reader
- HttpMethod
- Locale
- TimeZone
- ZoneId
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
3.5 复杂参数
- Map:作为参数时,数据会被放到 request 请求域中一同传递,即
request.setAttribute()
- Model:作为参数时,数据会被放到 request 请求域中一同传递,即
request.setAttribute()
- Errors/BindingResult
- RedirectAttributes:重定向携带数据
- ServletResponse
- SessionStatus
- UriComponentsBuilder
- ServletUriComponentsBuilder
⭐举例几个复杂参数的使用方法:
@RequestMapping("/request")
public String params(Map<String, Object> map,
Model model,
HttpServletRequest request,
HttpServletRequest response) {
map.put("hello", "world");
model.addAttribute("model", "modelValue");
request.setAttribute("request", "Attribute");
response.setAttribute("response", "Attribute");
return "forward:/complex";
}
@ResponseBody
@RequestMapping("/complex")
public Map testComplexParams(HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
Object hello = request.getAttribute("hello");
Object model = request.getAttribute("model");
Object request1 = request.getAttribute("request");
Object response = request.getAttribute("response");
map.put("hello", hello);
map.put("model", model);
map.put("request1", request1);
map.put("response", response);
return map;
}
3.6 POJO 参数 & 自定义 Converter
演示一下 POJO 参数的封装。
1)创建一个表单用于提交数据作为参数:
<!-- POJO 参数封装 -->
<form action="/saveUser" type="POST">
姓名:<input name="username" value="Wukkkkk"/>
年龄:<input name="age" value="21"/>
生日:<input name="birth" value="2021/07/26"/>
颜色名:<input name="color.colorName" value="blue"/>
颜色ID:<input name="color.id" value="1"/>
<input type="submit" value="提交">
</form>
2)创建 Controller:
@ResponseBody
@RequestMapping("/saveUser")
public User saveUser(User user) {
return user;
}
3)提交表单,查看结果:
那如果我们要按照自己的格式来封装参数呢?Spring MVC 底层提供的 Converter
是有限且固定的,所以我们如果要按照自己意愿来传递数据并正确封装到参数中,那么就需要自定义 Converter
了。
@Bean
// WebMvcConfigurer 定制 Spring MVC 的功能!!!
public WebMvcConfigurer getWebMvcConfigurer() {
return new WebMvcConfigurer() {
// 这是之前UrlPathHelper的设置,还记得吗
// @Override
// public void configurePathMatch(PathMatchConfigurer configurer) {
// UrlPathHelper urlPathHelper = new UrlPathHelper();
// 不移除分号: 矩阵变量生效!
// urlPathHelper.setRemoveSemicolonContent(false);
// configurer.setUrlPathHelper(urlPathHelper);
// }
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Color>() {
@Override
public Color convert(String source) {
if (!StringUtils.isNullOrEmpty(source)) {
Color color = new Color();
String[] strings = source.split(",");
color.setColorName(strings[0]);
color.setId(Integer.parseInt(strings[1]));
return color;
}
return null;
}
});
}
};
}
3.7 其他请求参数
基础请求参数见博客:juejin.cn/post/699580…
3.8 参数请求解析的原理分析
HandlerMapping 中找到能处理请求的 Handler,为当前 Handler 找一个适配器 HandlerAdapter,适配器执行目标方法并确定方法参数的每一个值。
参数解析器:argumentResolvers
- 源码剖析【P32 ~ P36】:www.bilibili.com/video/BV19K…
4. 数据响应与内容协商
💦 spring-boot-starter-web
底层默认使用的 JSON 解析框架是 Jackson:
下面我们看一下默认的 Jackson 框架对常用数据类型的转 JSON 处理:
详见博客:小白 の SpringMVC 学习笔记 💦
返回值处理器:returnValueHandlers
更多【P37 ~ P42】:www.bilibili.com/video/BV19K…
5. 视图解析与模板引擎
5.1 视图解析
处理方式:
- 转发
- 重定向
- 自定义视图
5.1.1 视图解析原理
5.2 模板引擎
常见的模板引擎包括 Thymeleaf、FreeMarker、Enjoy、Velocity、JSP 等。
为什么我们要学模板引擎:
- 模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这就大大提升了开发效率,良好的设计也使得代码重用变得更加容易。我们司空见惯的模板安装卸载等概念,基本上都和模板引擎有着千丝万缕的联系。
- 模板引擎不只是可以让你实现代码分离(业务逻辑代码和用户界面代码),也可以实现数据分离(动态数据与静态数据),还可以实现代码单元共享(代码重用),甚至是多语言、动态页面与静态页面自动均衡(SDE)等等与用户界面可能没有关系的功能。
5.2.1 Thymeleaf 简介
Spring Boot 默认打包方式为 JAR 包,JAR 包是压缩包,JSP 不支持在压缩包内编译,所以 Spring Boot 默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染、跳转。
Thymeleaf 是目前主流的模板引擎之一,Spring Boot 推荐!
Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
特性:
- Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外的属性来达到模板 + 数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。
- Thymeleaf 开箱即用的特性。它提供标准和 spring 标准两种方言,可以直接套用模板实现 JSTL、 OGNL 表达式效果,避免每天套模板、改 jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。
- Thymeleaf 提供 spring 标准方言和一个与 Spring MVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。
缺点:
- 后台管理系统若是高并发的系统,应该选取其他模板引擎而非 Thymeleaf,简而言之:性能较低。
5.2.2 Thymeleaf 初体验
底层规定 Thymeleaf 模板都放置在 classpath:/templates/
目录下,且后缀 .html
也已指定好,返回页面时可以不用添加前后缀。在 Spring MVC 中的视图解析器 InternalResourceViewResolver 也有该功能,只不过需要我们自己配置。这里则自动帮你配置好了。
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
...
}
我们来快速搭建一个 Thymeleaf 页面吧!
applicationProperties.yaml(设置项目访问路径):
# 仅仅为了测试 th:href="${github}" 与 th:href="@{github}"
server:
servlet:
context-path: /springboot
ThymeleafController.java:
@Controller
public class ThymeleafController {
// 访问需要加上项目的访问路径(server.servlet.context-path): /springboot/thymeleaf
@RequestMapping("/thymeleaf")
public String success(Model model) {
model.addAttribute("word","Hello Thymeleaf!");
model.addAttribute("github","https://github.com/Wu-yikun");
// 跳转到 classpath:templates/thymeleaf.html
return "thymeleaf";
}
}
Thymeleaf.html:
<!DOCTYPE html>
<!-- 指定Thymeleaf的名称空间【相当于约束,会给提示】 -->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Thymeleaf 的使用</title>
</head>
<body>
<!-- 取出 model 中的 attribute -->
<h2 th:text="${word}">Hello World!</h2>
<a href="http://www.baidu.com" th:target="_blank" th:href="${github}">百度首页? No!</a>
<a href="http://www.baidu.com" th:href="@{github}">百度???</a>
</body>
</html>
启动项目,访问 localhost:8080/springboot/thymeleaf
:
换种方式:我们直接打开 html 文件,可以发现是未渲染数据的静态文件。
${}
vs @{}
:
5.2.3 Thymeleaf 基本语法
英文文档:www.thymeleaf.org/doc/tutoria…
中文文档:www.docs4dev.com/docs/zh/thy…
5.2.4 Thymeleaf 实操项目⭐
构建 AdminEx 后台管理系统:gitee.com/Wu-Yikun/sp…
后面讲解的所有内容,都是基于该项目进行的!【当然,所有功能都只会完成部分,仅仅为了演示】
-
基本页面跳转功能
-
抽取公共页面
-
遍历数据
-
拦截器
-
文件上传
-
异常处理
-
原生组件注入(Servlet、Filter、Listener)
-
数据访问
-
单元测试
-
指标监控
-
高级特性
⭐页面跳转功能
首先一定要注意的是在每一个 html 页面下引入 thymeleaf 空间:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
User.java:
public class User {
private String username;
private String password;
// setter、getter
}
LoginController.java:
@Controller
public class LoginController {
// 登录页面
@GetMapping({"/login", "/"})
public String loginUser() {
// 跳转到登陆页面
return "login";
}
// @GetMapping("/login"): URI、<a></a>超链接地址、表单(method=get)
// @PostMapping("/login"): 表单(method=post)
@PostMapping("/login")
public String main(User user, HttpSession session, Model model) {
if (user.getUsername().equals("w") && user.getPassword().equals("123456")) {
// 添加Session用户,防止有经验的程序员未登录直接从网址进入后台!
// 其实所有页面都需要验证是否已登录, 登录则加入Session, 退出要注销Session
session.setAttribute("loginUser", user);
// 重定向默认为: GET请求
return "redirect:/main.html";
} else {
model.addAttribute("status", "账号或密码错误!");
return "login";
}
}
// 放置刷新页面时重复提交表单(POST),所以将post重定向到get请求
@GetMapping("/main.html")
public String mainPage(HttpSession session, Model model) {
// 但我们不可能在每一个页面都向以下方法来检查是否登录
// 所以更快捷的办法就是: 设置全局拦截器,未登录的用户直接跳转到登陆页面(下一节讲解拦截器演示)
Object loginUser = session.getAttribute("loginUser");
if (loginUser != null) {
// 已登录,可跳转
return "main";
} else {
model.addAttribute("status", "请登录");
return "login";
}
}
}
login.html
<form class="form-signin" action="/login" method="post" th:action="@{/login}">
...
<label style="color:red" th:text="${status}">Message</label>
<input type="text" class="form-control" placeholder="用户名" name="username" autofocus>
<input type="password" class="form-control" placeholder="密码" name="password">
⭐抽取公共页面功能
了解下
href
与th:href
区别:在默认项目路径为空时,打 Jar 包单独运行时。二者效果一致。
在使用 Maven 内嵌 Tomcat 或打 War 包部署到 Servlet 容器,或者在项目内执行 App 启动类,且有配置项目路径时。
二者区别如下:
href
始终从端口开始作为根路径,如 `http://localhost:8080/channel/page/add``- ``th:href
会寻找项目路径作为根路径,如
http://localhost:8080/dx/channel/page/add`
以抽取 footer.html 为例讲解基本语法 th:fragment=""
、th:insert=" :: "
、th:replace=" :: "
、th:include=" :: "
:
So an HTML fragment like this:
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
…included three times in host <div>
tags, like this:
<body>
...
<!-- footer指的是footer.html; copy指的是th:fragment -->
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>
</body>
…will result in:
<body>
...
<div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
<div>
© 2011 The Good Thymes Virtual Grocery
</div>
</body>
Referencing fragments without th:fragment
...
<div id="copy-section">
© 2011 The Good Thymes Virtual Grocery
</div>
...
We can use the fragment above simply referencing it by its id
attribute, in a similar way to a CSS selector:
<body>
...
<div th:insert="~{footer :: #copy-section}"></div>
</body>
例 common.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!-- 抽取公共页面 -->
<head th:fragment="header-link">
<!-- common -->
<link href="css/style.css" rel="stylesheet">
<link href="css/style-responsive.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="js/html5shiv.js"></script>
<script src="js/respond.min.js"></script>
<![endif]-->
</head>
<body>
<!-- left side start-->
<div th:fragment="common-left-side" class="left-side sticky-left-side">
<!--logo and iconic logo start-->
<div class="logo">
<a th:href="@{/main.html}">
<img src="images/logo.png" alt=""></a>
</div>
<div class="logo-icon text-center">
<a th:href="@{/main.html}">
<img src="images/logo_icon.png" alt=""></a>
</div>
<!--logo and iconic logo end-->
<!--侧边栏导航开始-->
<ul class="nav nav-pills nav-stacked custom-nav">
<!--此处省略1W字-->
<li class="menu-list nav-active">
<a href="#">
<i class="fa fa-th-list"></i>
<span>数据表</span>
</a>
<ul class="sub-menu-list">
<li class="active">
<a th:href="@{/basic_table}">basic_table.html</a>
</li>
<li>
<a th:href="@{/dynamic_table}">dynamic_table.html</a>
</li>
<li>
<a th:href="@{/responsive_table}">responsive_table</a>
</li>
<li>
<a th:href="@{/editable_table}">editable_table</a>
</li>
</ul>
</li>
<!--此处省略1W字-->
</ul>
<!--侧边栏导航结束-->
<!--此处省略1W字-->
</div>
<!-- left side end-->
<!-- header section start-->
<div th:fragment="common-header-section" class="header-section">
<a class="toggle-btn"><i class="fa fa-bars"></i></a>
<form class="searchform">
<!-- 此处省略1W字 -->
</form>
<div class="menu-right">
<!-- 此处省略1W字 -->
<a href="#" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<img src="images/photos/user-avatar.png" alt=""/>
<!-- 登录后在右上角的个人信息中显示名字:由于没有标签,所以无法使用 th:text="${}" -->
<!-- 直接使用 Thymeleaf 提供的如下引用方式 -->
[[${session.loginUser.username}]]
<span class="caret"></span>
</a>
<ul class="dropdown-menu dropdown-menu-usermenu pull-right">
<li><a href="#"><i class="fa fa-user"></i> Profile</a></li>
<li><a href="#"><i class="fa fa-cog"></i> Settings</a></li>
<!-- Log Out 要清除Session 才能退出,这里仅做一个简单的返回登录界面的跳转! -->
<li><a th:href="@{/}"><i class="fa fa-sign-out"></i> Log Out</a></li>
</ul>
<!-- 此处省略1W字 -->
</div>
</div>
<!-- header section end-->
<!--footer section start-->
<footer th:fragment="common-footer">
2014 © AdminEx by ThemeBucket
</footer>
<!--footer section end-->
<div id="common-script">
<!-- Placed js at the end of the document so the pages load faster -->
<script th:src="@{/js/jquery-1.10.2.min.js}"></script>
<script th:src="@{/js/jquery-ui-1.9.2.custom.min.js}"></script>
<script th:src="@{/js/jquery-migrate-1.2.1.min.js}"></script>
<script th:src="@{/js/bootstrap.min.js}"></script>
<script th:src="@{/js/modernizr.min.js}"></script>
<script th:src="@{/js/jquery.nicescroll.js}"></script>
<!--common scripts for all pages-->
<script th:src="@{/js/scripts.js}"></script>
</div>
</body>
</html>
TableController.java:
@Controller
public class TableController {
@RequestMapping("/basic_table")
public String basic() {
return "table/basic_table";
}
@RequestMapping("/dynamic_table")
public String dynamic() {
return "table/dynamic_table";
}
@RequestMapping("/editable_table")
public String editable() {
return "table/editable_table";
}
@RequestMapping("/pricing_table")
public String pricing() {
return "table/pricing_table";
}
@RequestMapping("/responsive_table")
public String responsive() {
return "table/responsive_table";
}
}
⭐遍历数据
基本语法:
th:each="user,stat:${users}"
th:text="${user.xxx}"
stat.xxx
<table class="display table table-bordered" id="hidden-table-info">
<thead>
<tr>
<th>#</th>
<th>UserName</th>
<th>Password</th>
</tr>
</thead>
<tbody>
<tr class="gradeX" th:each="user,stat:${users}">
<!-- th:each="遍历对象,状态:${遍历数组}" -->
<td th:text="${stat.count}"></td>
<td th:text="${user.username}">user</td>
<td th:text="${user.password}">password</td>
<!-- or -->
<!--<td>[[${user.password}]]</td>-->
</tr>
</tbody>
</table>
6. 拦截器
拦截访问路径 /**
与 /*
:
- /**:
Spring
家族的写法 - /*:
Servlet
的写法
6.1 HandlerInterceptor 接口
public interface HandlerInterceptor {
// 方法执行前
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
// 方法返回前
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
// 方法执行后
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
6.2 自定义拦截器
1、编写拦截器实现 HandlerInterceptor 接口
@Controller
public class LoginInterceptor implements HandlerInterceptor {
// 请求方法(/xxx)执行前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
if (session.getAttribute("loginUser") != null) {
// 用户已登录,可放行
return true;
}
// 用户未登录,拦截并返回登陆界面
// 提示信息
request.setAttribute("status", "请先登录!");
// 转发到登陆界面(并携带提示信息)
request.getRequestDispatcher("/").forward(request, response);
// 重定向无法携带消息!!!所以使用转发!
// response.sendRedirect("/login");
// 拦截
return false;
}
// 请求方法(/xxx)返回前
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 此处可对 ModelAndView 对象进行操作!
System.out.println("postHandle...");
}
// 请求方法(/xxx)执行后
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}
2、拦截器注册到容器中(实现 WebMvcConfigurer
的 addInterceptors
)
定制 Spring MVC 功能【必须向容器中注入
WebMvcConfigurer
组件】的两种方法:1)
@Configuration
+implements WebMvcConfigurer
@Configuration public class AdminWebConfig implements WebMvcConfigurer { @Override ... }
2)
@Bean
@Bean public WebMvcConfigurer getWebMvcConfigurer() { return new WebMvcConfigurer() { @Override ... } }
这里我们使用第一种,并重写 addInterceptors
方法:
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**");
}
}
3、指定拦截规则 /**
【如果是拦截所有,静态资源也会被拦截,需放行静态资源】
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**");
6.3 拦截器原理
7. 文件上传
文件上传 の XML 版本:juejin.cn/post/699580…
7.1 实现文件上传
文件表单固定格式:
<form role="form" action="/upload" method="post" enctype="multipart/form-data"></form>
单文件:
<input type="file" name="head">
多文件:
<input type="file" name="photos" multiple>
form_layouts.html
<!--只截取上传文件的表单部分-->
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="exampleInputEmail1">邮箱</label>
<input type="email" class="form-control" id="exampleInputEmail1" name="email"
placeholder="Enter email">
</div>
<div class="form-group">
<label for="exampleInputPassword1">密码</label>
<input type="password" class="form-control" id="exampleInputPassword1" name="password"
placeholder="Password">
</div>
<div class="form-group">
<label for="exampleInputFile">头像</label>
<input type="file" id="exampleInputFile" name="head">
<p class="help-block">Example block-level help text here.</p>
</div>
<div class="form-group">
<label for="exampleInputFile">照片s</label>
<input type="file" id="exampleInputFiles" name="photos" multiple>
<p class="help-block">Example block-level help text here.</p>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
FormController.java
@Controller
public class FormController {
@RequestMapping("/form_layouts")
public String form_layouts() {
// 前后缀自动补齐!
return "form/form_layouts";
}
/**
* MultipartFile 会自动封装上传过来的文件!
*
* @param email String
* @param password String
* @param head MultipartFile
* @param photos MultipartFile[]
* @return
* @throws IOException
*/
@PostMapping("/upload")
public String uploadFiles(@RequestParam("email") String email,
@RequestParam("password") String password,
@RequestPart("head") MultipartFile head,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
System.out.println("邮箱: " + email);
System.out.println("密码: " + password);
// 单个文件大小不为空, 则上传到文件服务器(这里上传到本地文件夹)
if (!head.isEmpty()) {
String originalFilename = head.getOriginalFilename();
head.transferTo(new File("D:\\ChromeTemp\\Admin-Spring Boot\\" + originalFilename));
}
// 文件组不为空, 则上传到文件服务器(这里上传到本地文件夹)
if (photos.length > 0) {
for (MultipartFile file : photos) {
if (!file.isEmpty()) {
String originName = file.getOriginalFilename();
file.transferTo(new File("D:\\ChromeTemp\\Admin-Spring Boot\\" + originName));
}
}
}
// 上传后返回首页
return "main";
}
}
首次提交表单,提示上传文件超出限制:
MultipartProperties
默认配置:
所以我们需要更改一下 yaml 配置,像这样:
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
再次上传,上传成功:
7.2 文件上传原理
文件上传自动配置类:MultipartAutoConfiguration
@ConfigurationProperties(prefix = "spring.servlet.multipart", ignoreUnknownFields = false)
public class MultipartProperties {
8. 异常处理
异常处理 の XML 版本:juejin.cn/post/699580…
8.1 异常处理默认规则
官方文档中对于 Spring Boot 的 Error Handling 是这样解释的:
- 默认情况下,Spring Boot 提供
/error
来处理所有错误的映射 - 对于机器客户端,它将生成 JSON 响应,其中包含错误,HTTP 状态和异常消息的详细信息
- 对于浏览器客户端,则会响应 "Whitelabel Error Page",以 HTML 格式呈现相同的数据
机器客户端:
{
"timestamp": "2021-09-13T15:23:26.019+00:00",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/hello"
}
浏览器客户端:
静态资源目录下的 error
文件夹下的 4xx
、5xx
页面会被自动解析:
有精确的错误状态码页面
404.html
就匹配精确的错误页面,找不到再匹配4xx.html
错误页面,若也没有则触发 Whitelabel Error Page。
8.2 自定义错误页面
4xx.html:
<!-- ${status}:错误状态码 -->
<h2 th:text="${status}">page not found</h2>
<!-- ${message}:错误信息 -->
<h3 th:text="${message}">We Couldn’t Find This Page</h3>
8.2.1 @ControllerAdvice + @ExceptionHandler
GlobalExceptionHandler.java
@ControllerAdvice
public class GlobalExceptionHandler {
// 处理 ArithmeticException.class, NullPointerException.class 这两类异常
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String handlerArithNullException() {
// 返回登陆页面
return "login";
}
}
8.2.2 @ResponseStatus + 自定义异常
UserTooManyException.java
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "Too much users")
public class UserTooManyException extends RuntimeException {
public UserTooManyException() {
}
public UserTooManyException(String message) {
super(message);
}
}
4xx.html 或 403.html 才能响应 403:
8.3 异常处理自动配置原理
异常处理最终也是返回一个 ModelAndView
!
底层几个重要的组件:
- ErrorMvcAutoConfiguration
- DefaultErrorViewResolver:把状态响应码作为错误页的地址
error/500.html
,模板引擎最终响应这个页面/resources/templates/error/5xx.html
- DefaultErrorAttributes
- BasicErrorController
- ...
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
错误页面能包含的基本属性:
8.4 Http 状态响应码
public enum HttpStatus {
CONTINUE(100, "Continue"),
SWITCHING_PROTOCOLS(101, "Switching Protocols"),
PROCESSING(102, "Processing"),
CHECKPOINT(103, "Checkpoint"),
OK(200, "OK"),
CREATED(201, "Created"),
ACCEPTED(202, "Accepted"),
NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information"),
NO_CONTENT(204, "No Content"),
RESET_CONTENT(205, "Reset Content"),
PARTIAL_CONTENT(206, "Partial Content"),
MULTI_STATUS(207, "Multi-Status"),
ALREADY_REPORTED(208, "Already Reported"),
IM_USED(226, "IM Used"),
MULTIPLE_CHOICES(300, "Multiple Choices"),
MOVED_PERMANENTLY(301, "Moved Permanently"),
FOUND(302, "Found"),
/** @deprecated */
@Deprecated
MOVED_TEMPORARILY(302, "Moved Temporarily"),
SEE_OTHER(303, "See Other"),
NOT_MODIFIED(304, "Not Modified"),
/** @deprecated */
@Deprecated
USE_PROXY(305, "Use Proxy"),
TEMPORARY_REDIRECT(307, "Temporary Redirect"),
PERMANENT_REDIRECT(308, "Permanent Redirect"),
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
PAYMENT_REQUIRED(402, "Payment Required"),
FORBIDDEN(403, "Forbidden"),
NOT_FOUND(404, "Not Found"),
METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
NOT_ACCEPTABLE(406, "Not Acceptable"),
PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"),
REQUEST_TIMEOUT(408, "Request Timeout"),
CONFLICT(409, "Conflict"),
GONE(410, "Gone"),
LENGTH_REQUIRED(411, "Length Required"),
PRECONDITION_FAILED(412, "Precondition Failed"),
PAYLOAD_TOO_LARGE(413, "Payload Too Large"),
/** @deprecated */
@Deprecated
REQUEST_ENTITY_TOO_LARGE(413, "Request Entity Too Large"),
URI_TOO_LONG(414, "URI Too Long"),
/** @deprecated */
@Deprecated
REQUEST_URI_TOO_LONG(414, "Request-URI Too Long"),
UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
REQUESTED_RANGE_NOT_SATISFIABLE(416, "Requested range not satisfiable"),
EXPECTATION_FAILED(417, "Expectation Failed"),
I_AM_A_TEAPOT(418, "I'm a teapot"),
/** @deprecated */
@Deprecated
INSUFFICIENT_SPACE_ON_RESOURCE(419, "Insufficient Space On Resource"),
/** @deprecated */
@Deprecated
METHOD_FAILURE(420, "Method Failure"),
/** @deprecated */
@Deprecated
DESTINATION_LOCKED(421, "Destination Locked"),
UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"),
LOCKED(423, "Locked"),
FAILED_DEPENDENCY(424, "Failed Dependency"),
TOO_EARLY(425, "Too Early"),
UPGRADE_REQUIRED(426, "Upgrade Required"),
PRECONDITION_REQUIRED(428, "Precondition Required"),
TOO_MANY_REQUESTS(429, "Too Many Requests"),
REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"),
UNAVAILABLE_FOR_LEGAL_REASONS(451, "Unavailable For Legal Reasons"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
NOT_IMPLEMENTED(501, "Not Implemented"),
BAD_GATEWAY(502, "Bad Gateway"),
SERVICE_UNAVAILABLE(503, "Service Unavailable"),
GATEWAY_TIMEOUT(504, "Gateway Timeout"),
HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version not supported"),
VARIANT_ALSO_NEGOTIATES(506, "Variant Also Negotiates"),
INSUFFICIENT_STORAGE(507, "Insufficient Storage"),
LOOP_DETECTED(508, "Loop Detected"),
BANDWIDTH_LIMIT_EXCEEDED(509, "Bandwidth Limit Exceeded"),
NOT_EXTENDED(510, "Not Extended"),
NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required");
...
}
9. Web 原生组件注入
Web 三大原生组件:
- Servlet
- Filter
- Listener
为了使得三大原生组件生效,必须将 Servlet 扫描进容器,所以我们需要在主配置类中添加一个注解 @ServletComponentScan(basePackages = "")
:
When using an embedded container, automatic registration of classes annotated with
@WebServlet
,@WebFilter
, and@WebListener
can be enabled by using@ServletComponentScan
.
@ServletComponentScan(basePackages = "com.one.admin")
@SpringBootApplication
public class SpringbootAdminApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootAdminApplication.class, args);
}
}
9.1 @WebServlet
@WebServlet(urlPatterns = {"/servlet","/my"})
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("MyServlet...");
}
}
9.2 @WebFilter
// Servlet: /*
// Spring 家族: /**
@WebFilter(urlPatterns = {"/css/*","/images/*"})
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init...");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// ...
// 满足则放行
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
System.out.println("destroy...");
}
}
9.3 @WebListener
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("项目初始化...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("项目销毁...");
}
}
9.4 ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean
@Configuration
public class MyRegistrationBean {
@Bean
public ServletRegistrationBean myServlet() {
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet, "/servlet", "/my");
}
@Bean
public FilterRegistrationBean myFilter() {
MyFilter myFilter = new MyFilter();
// 第一种
// return new FilterRegistrationBean(myFilter, myServlet());
// 第二种
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/servlet", "/my"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MyServletContextListener listener = new MyServletContextListener();
return new ServletListenerRegistrationBean(listener);
}
}
原创不易🧠 转载请标明出处🌊
若本文对你有所帮助,点赞支持嗷🔥