本文深入解析了 Spring Boot 中 WebMvcAutoConfiguration
自动配置的原理与应用,涵盖静态资源映射、内容协商机制、自定义 MVC
行为、模板引擎 Thymeleaf
的整合、国际化、错误处理、嵌入式容器支持以及如何通过 @EnableWebMvc 接管
SpringMVC。最后介绍了 Web 新特性如 ProblemDetails 和函数式 Web
编程,帮助开发者系统掌握 Spring Web 栈的使用与扩展能力。
0. WebMvcAutoConfiguration
原理
1. 生效条件
1 2 3 4 5 6 7 8 9 @AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class }) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @ImportRuntimeHints(WebResourcesRuntimeHints.class) public class WebMvcAutoConfiguration {}
2. 效果
放了两个 Filter:
HiddenHttpMethodFilter
:页面表单提交 Rest
请求(GET、POST、PUT、DELETE)
FormContentFilter
: 表单内容 Filter,GET(数据放 URL
后面)、POST(数据放请求体)请求可以携带数据,PUT、DELETE
的请求体数据会被忽略
给容器中放了WebMvcConfigurer
组件;给 SpringMVC
添加各种定制功能
所有的功能最终会和配置文件进行绑定
WebMvcProperties: spring.mvc
配置文件
WebProperties: spring.web
配置文件
1 2 3 4 5 6 7 @Configuration(proxyBeanMethods = false) @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer , ServletContextAware{}
提供了配置 SpringMVC 底层的所有组件入口
4. 静态资源规则源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public void addResourceHandlers (ResourceHandlerRegistry registry) { if (!this .resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled" ); return ; } addResourceHandler(registry, this .mvcProperties.getWebjarsPathPattern(), "classpath:/META-INF/resources/webjars/" ); addResourceHandler(registry, this .mvcProperties.getStaticPathPattern(), (registration) -> { registration.addResourceLocations(this .resourceProperties.getStaticLocations()); if (this .servletContext != null ) { ServletContextResource resource = new ServletContextResource (this .servletContext, SERVLET_LOCATION); registration.addResourceLocations(resource); } }); }
规则一:访问 /webjars/**
路径就去
classpath:/META-INF/resources/webjars/
下找资源。
规则二:访问 /**
路径就去
静态资源默认的四个位置找资源
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/
规则三:静态资源默认都有缓存规则的设置
所有缓存的设置,直接通过配置文件 :
spring.web
cachePeriod: 缓存周期; 多久不用找服务器要新的。 默认没有,以 s
为单位
cacheControl: HTTP 缓存控制;
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching
useLastModified:是否使用最后一次修改。配合 HTTP Cache
规则。
如果浏览器访问了一个静态资源
index.js
,如果服务这个资源没有发生变化,下次访问的时候就可以直接让浏览器用自己缓存中的东西,而不用给服务器发请求。
1 2 3 registration.setCachePeriod(getSeconds(this .resourceProperties.getCache().getPeriod())); registration.setCacheControl(this .resourceProperties.getCache().getCachecontrol().toHttpCacheControl()); registration.setUseLastModified(this .resourceProperties.getCache().isUseLastModified());
5. EnableWebMvcConfiguration
源码
1 2 3 4 5 6 @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(WebProperties.class) public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {}
WebMvcAutoConfiguration 是一个自动配置类,它里面有一个
EnableWebMvcConfiguration
。
EnableWebMvcConfiguration
继承
DelegatingWebMvcConfiguration
。
DelegatingWebMvcConfiguration
利用 DI 把容器中所有
WebMvcConfigurer
注入进来。
这样无论哪一个WebMvcConfigurer
里面配置的东西,都会被统一处理,放到一个公共的WebMvcConfigurer
中,作为最终使用的配置类。
这里存在一个Bean 注入顺序问题 (案例 )
DelegatingWebMvcConfiguration 继承了
WebMvcConfigurationSupport
而 WebMvcAutoConfig 存在注解
@ConditionalOnMissingBean ({WebMvcConfigurationSupport.class})
但如上面所说 WebMvcAutoConfig 加载后也会导入
DelegatingWebMvcConfiguration。
所以说WebMvcAutoConfig
注入的时间比WebMvcConfigurationSupport
早。
判断ConditionalOnMissingBean
时,WebMvcConfigurationSupport
还没有注入。
因此WebMvcAutoConfig
能生效。
但如果我们手动添加了@EnableWebMvc
注解,就会导致提前注入WebMvcConfigurationSupport
从而使WebMvcAutoConfig
失效,没有默认的配置了,只剩下用户自己定义的配置。
7. WebMvcConfigurationSupport
提供了很多的默认设置。
判断系统中是否有相应的类:如果有,就加入相应的HttpMessageConverter
1 2 3 4 jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper" , classLoader) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator" , classLoader); jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper" , classLoader); jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory" , classLoader);
1. Web 场景
1. 自动配置
1、整合 web 场景
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency >
2、引入了autoconfigure
功能
3、@EnableAutoConfiguration
注解使用
@Import(AutoConfigurationImportSelector.class)
批量导入组件
4、加载
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中配置的所有组件
5、所有自动配置类如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration ====以下是响应式web场景和现在的没关系====== org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration org.springframework.boot.autoconfigure.web.reactive.ReactiveMultipartAutoConfiguration org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration ================以上没关系================= org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
6、绑定了配置文件的一堆配置项
1、SpringMVC 的所有配置 spring.mvc
2、Web 场景通用配置 spring.web
3、文件上传配置 spring.servlet.multipart
4、服务器的配置 server
: 比如:编码方式
2. 默认效果
默认配置:
包含了 ContentNegotiatingViewResolver
和
BeanNameViewResolver
组件,方便视图解析。
默认的静态资源处理机制:静态资源放在 static
文件夹下即可直接访问。
自动注册了Converter,GenericConverter,Formatter
组件,适配常见数据类型转换和格式化需求
。
支持 HttpMessageConverters
,可以方便返回 json
等数据类型。
注册
MessageCodesResolver
,方便国际化及错误消息处理。
支持静态 index.html
自动使用ConfigurableWebBindingInitializer
,实现消息处理、数据绑定、类型转化、数据校验等功能
重要:
如果想保持 Springboot mvc 的默认配置,并且自定义更多的 mvc
配置,如:interceptors,formatters,view controllers
等。可以使用@Configuration 注解添加一个
WebMvcConfigurer 类型的配置类,并不要标注@EnableWebMvc
如果想保持 boot mvc
的默认配置,但要自定义核心组件实例,比如:RequestMappingHandlerMapping,
RequestMappingHandlerAdapter, 或
ExceptionHandlerExceptionResolver,给容器中放一个 WebMvcRegistrations
组件即可。
如果想全面接管 Spring MVC,@Configuration
标注一个配置类,并加上 @EnableWebMvc 注解,实现
WebMvcConfigurer 接口。
2. 静态资源
1. 默认规则
1. 静态资源映射
静态资源映射规则在 WebMvcAutoConfiguration 中进行了定义:
/webjars/** 的所有路径资源都在
classpath:/META-INF/resources/webjars/
/** 的所有路径资源都在
classpath:/META-INF/resources/、classpath:/resources/、classpath:/static/、classpath:/public/
所有静态资源都定义了缓存规则。【浏览器访问过一次,就会缓存一段时间】,但此功能参数无默认值。
period: 缓存间隔。 默认 0S;
cacheControl:缓存控制。 默认无;
useLastModified:是否使用 lastModified 头。 默认 false;
2. 静态资源缓存
如前面所述
3. 欢迎页
欢迎页规则在 WebMvcAutoConfiguration 中进行了定义:
在静态资源目录下找 index.html
没有就在 templates 下找 index 模板页
4. Favicon
在静态资源目录下找 favicon.ico
5. 缓存实验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 server.port =9000 spring.web.resources.add-mappings =true spring.web.resources.cache.cachecontrol.max-age =7200 spring.web.resources.cache.use-last-modified =true
2. 自定义静态资源规则
自定义静态资源路径、自定义缓存规则
1. 配置方式
spring.mvc
: 静态资源访问前缀路径
spring.web
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 spring.web.resources.add-mappings =true spring.web.resources.cache.period =3600 spring.web.resources.cache.cachecontrol.max-age =7200 spring.web.resources.cache.cachecontrol.cache-public =true spring.web.resources.cache.use-last-modified =true spring.web.resources.static-locations =classpath:/a/,classpath:/b/,classpath:/static/ spring.mvc.webjars-path-pattern =/wj/** spring.mvc.static-path-pattern =/static/**
2. 代码方式
容器中只要有一个 WebMvcConfigurer 组件。配置的底层行为都会生效
@EnableWebMvc //禁用 boot
的默认配置
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class MyConfig implements WebMvcConfigurer { @Override public void addResourceHandlers (ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**" ) .addResourceLocations("classpath:/a/" ,"classpath:/b/" ) .setCacheControl(CacheControl.maxAge(1180 , TimeUnit.SECONDS)); } }
或者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class MyConfig { @Bean public WebMvcConfigurer webMvcConfigurer () { return new WebMvcConfigurer () { @Override public void addResourceHandlers (ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**" ) .addResourceLocations("classpath:/a/" , "classpath:/b/" ) .setCacheControl(CacheControl.maxAge(1180 , TimeUnit.SECONDS)); } }; } }
3. 路径匹配
Spring5.3
之后加入了更多的请求路径匹配的实现策略;
以前只支持 AntPathMatcher 策略, 现在提供了
PathPatternParser
策略。并且可以让我们指定到底使用那种策略。
1. Ant 风格路径用法
Ant 风格的路径模式语法具有以下规则:
*:表示任意数量的字符。
?:表示任意一个字符。
**:表示任意数量的目录。
{}:表示一个命名的模式占位符。
[]:表示字符集合,例如[a-z]表示小写字母。
例如:
*.html 匹配任意名称,扩展名为.html 的文件。
/folder1/*/*.java 匹配在 folder1 目录下的任意两级目录下的.java
文件。
/folder2/**/*.jsp 匹配在 folder2 目录下任意目录深度的.jsp
文件。
/{type}/{id}.html
匹配任意文件名为{id}.html,在任意命名的{type}目录下的文件。
注意:Ant 风格的路径模式语法中的特殊字符需要转义,如:
要匹配文件路径中的星号,则需要转义为\*。
要匹配文件路径中的问号,则需要转义为\?。
2. 模式切换
AntPathMatcher 与 PathPatternParser
PathPatternParser 在 jmh 基准测试下,有 6~8 倍吞吐量提升,降低
30%~40%空间分配率
PathPatternParser 兼容 AntPathMatcher
语法,并支持更多类型的路径模式
PathPatternParser “**”
多段匹配的支持仅允许在模式末尾使用
1 2 3 4 5 6 7 8 @GetMapping("/a*/b?/{p1:[a-f]+}") public String hello (HttpServletRequest request, @PathVariable("p1") String path) { log.info("路径变量p1: {}" , path); String uri = request.getRequestURI(); return uri; }
总结:
使用默认的路径匹配规则,是由 PathPatternParser 提供的
如果路径中间需要有 **,替换成 ant 风格路径
1 2 3 4 spring.mvc.pathmatch.matching-strategy =ant_path_matcher
4. 内容协商
1. 多端内容适配
1. 默认规则
基于请求头 内容协商:(默认开启)
客户端向服务端发送请求,携带 HTTP 标准的Accept
请求头 。
Accept :
application/json
、text/xml
、text/yaml
服务端根据客户端请求头期望的数据类型 进行动态返回
基于请求参数 内容协商:(需要手动开启)
发送请求 GET /projects/spring-boot?format=json
匹配到 @GetMapping (“/projects/spring-boot”)
根据参数协商 ,优先返回 json
类型数据【需要开启参数匹配设置 】
发送请求 GET /projects/spring-boot?format=xml
则会优先返回 xml 类型数据
2. 效果演示
请求同一个接口,可以返回 json 和 xml 不同格式数据
1 2 3 4 <dependency > <groupId > com.fasterxml.jackson.dataformat</groupId > <artifactId > jackson-dataformat-xml</artifactId > </dependency >
1 2 3 4 5 6 7 8 @JacksonXmlRootElement @Data public class Person { private Long id; private String userName; private String email; private Integer age; }
1 2 3 4 spring.mvc.contentnegotiation.favor-parameter =true spring.mvc.contentnegotiation.parameter-name =type
3. 配置协商规则与支持类型
修改内容协商方式
1 2 3 4 spring.mvc.contentnegotiation.favor-parameter =true spring.mvc.contentnegotiation.parameter-name =type
2. 自定义内容返回
1. 增加 yaml 返回支持
导入依赖
1 2 3 4 <dependency > <groupId > com.fasterxml.jackson.dataformat</groupId > <artifactId > jackson-dataformat-yaml</artifactId > </dependency >
把对象写出成 YAML
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main (String[] args) throws JsonProcessingException { Person person = new Person (); person.setId(1L ); person.setUserName("张三" ); person.setEmail("aaa@qq.com" ); person.setAge(18 ); YAMLFactory factory = new YAMLFactory ().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER); ObjectMapper mapper = new ObjectMapper (factory); String s = mapper.writeValueAsString(person); System.out.println(s); }
编写配置
1 2 spring.mvc.contentnegotiation.media-types.yaml =text/yaml
增加HttpMessageConverter
组件,专门负责把对象写出为 yaml
格式
1 2 3 4 5 6 7 8 9 @Bean public WebMvcConfigurer webMvcConfigurer () { return new WebMvcConfigurer () { @Override public void configureMessageConverters (List<HttpMessageConverter<?>> converters) { converters.add(new MyYamlHttpMessageConverter ()); } }; }
2. 如何实现自定义
MessageConverter 组件
3. HttpMessageConverter
的示例写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class MyYamlHttpMessageConverter extends AbstractHttpMessageConverter <Object> { private ObjectMapper objectMapper = null ; public MyYamlHttpMessageConverter () { super (new MediaType ("text" , "yaml" , Charset.forName("UTF-8" ))); YAMLFactory factory = new YAMLFactory () .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER); this .objectMapper = new ObjectMapper (factory); } @Override protected boolean supports (Class<?> clazz) { return true ; } @Override protected Object readInternal (Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return null ; } @Override protected void writeInternal (Object methodReturnValue, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { try (OutputStream os = outputMessage.getBody()){ this .objectMapper.writeValue(os,methodReturnValue); } } }
3.
内容协商原理-HttpMessageConverter
HttpMessageConverter
怎么工作?合适工作?
定制 HttpMessageConverter
来实现多端内容协商
编写WebMvcConfigurer
提供的configureMessageConverters
底层,修改底层的MessageConverter
1. @ResponseBody 由
HttpMessageConverter 处理
标注了@ResponseBody
的返回值 将会由支持它的
HttpMessageConverter
写给浏览器
如果 controller 方法的返回值标注了
@ResponseBody
注解
请求进来先来到DispatcherServlet
的doDispatch()
进行处理
找到一个
HandlerAdapter
适配器。利用适配器执行目标方法
RequestMappingHandlerAdapter
来执行,调用invokeHandlerMethod
来执行目标方法
目标方法执行之前,准备好两个东西
HandlerMethodArgumentResolver
:参数解析器,确定目标方法每个参数值
HandlerMethodReturnValueHandler
:返回值处理器,确定目标方法的返回值改怎么处理
RequestMappingHandlerAdapter
里面的invokeAndHandle
真正执行目标方法
目标方法执行完成,会返回返回值对象
找到一个合适的返回值处理器
HandlerMethodReturnValueHandler
最终找到RequestResponseBodyMethodProcessor
能处理标注了
@ResponseBody
注解的方法
RequestResponseBodyMethodProcessor
调用writeWithMessageConverters
,利用MessageConverter
把返回值写出去
综上可知:@ResponseBody
由HttpMessageConverter
处理
HttpMessageConverter
会先进行内容协商
遍历所有的MessageConverter
看谁支持这种内容类型的数据
最终因为要json
所以MappingJackson2HttpMessageConverter
支持写出
json
jackson 用ObjectMapper
把对象写出去
2.
WebMvcAutoConfiguration 提供几种默认 HttpMessageConverters
EnableWebMvcConfiguration
通过
addDefaultHttpMessageConverters
添加了默认的MessageConverter
ByteArrayHttpMessageConverter
: 支持字节数据读写
StringHttpMessageConverter
: 支持字符串读写
ResourceHttpMessageConverter
:支持资源读写
ResourceRegionHttpMessageConverter
:
支持分区资源写出
AllEncompassingFormHttpMessageConverter
:支持表单
xml/json 读写
MappingJackson2HttpMessageConverter
: 支持请求响应体
Json 读写
系统提供默认的 MessageConverter 功能有限,仅用于 json
或者普通返回数据。额外增加新的内容协商功能,必须增加新的HttpMessageConverter
5. 模板引擎
由于 SpringBoot 使用了嵌入式 Servlet
容器 。所以 JSP
默认是不能使用 的。
如果需要服务端页面渲染 ,优先考虑使用
模板引擎。
模板引擎页面默认放在 src/main/resources/templates
SpringBoot 包含以下模板引擎的自动配置
FreeMarker
Groovy
Thymeleaf
Mustache
Thymeleaf 官网 :https://www.thymeleaf.org/
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html xmlns:th ="http://www.thymeleaf.org" > <head > <title > Good Thymes Virtual Grocery</title > <meta http-equiv ="Content-Type" content ="text/html; charset=UTF-8" /> <link rel ="stylesheet" type ="text/css" media ="all" th:href ="@{/css/gtvg.css}" /> </head > <body > <p th:text ="#{home.welcome}" > Welcome to our grocery store!</p > </body </html >
1. Thymeleaf 整合
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency >
自动配置原理
开启了
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration
自动配置
属性绑定在 ThymeleafProperties 中,对应配置文件 spring.thymeleaf
内容
所有的模板页面默认在
classpath:/templates
文件夹下
默认效果
所有的模板页面在 classpath:/templates/
下面找
找后缀名为.html
的页面
2. 基础语法
1. 核心用法
th:xxx
:动态渲染指定的 html 标签属性值、或者 th
指令(遍历、判断等)
1 2 3 4 5 6 <p th:text ="${content}" > 原内容</p > <a th:href ="${url}" > 登录</a > <img src ="../../images/gtvglogo.png" th:attr ="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
表达式
:用来动态取值
${}
:变量取值;使用 model
共享给页面的值都直接用${}
@{}
:url 路径
#{}
:国际化消息
~{}
:片段引用
*{}
:变量选择:需要配合 th:object 绑定对象
系统工具&内置对象: 详细文档
param
:请求参数对象
session
:session 对象
application
:application 对象
#execInfo
:模板执行信息
#messages
:国际化消息
#uris
:uri/url 工具
#conversions
:类型转换工具
#dates
:日期工具,是java.util.Date
对象的工具类
#calendars
:类似#dates,只不过是java.util.Calendar
对象的工具类
#temporals
: JDK8+ **java.time**
API
工具类
#numbers
:数字操作工具
#strings
:字符串操作
#objects
:对象操作
#bools
:bool 操作
#arrays
:array 工具
#lists
:list 工具
#sets
:set 工具
#maps
:map 工具
#aggregates
:集合聚合工具(sum、avg)
#ids
:id 生成工具
2. 语法示例
表达式:
变量取值:${…}
url 取值:@...
国际化消息:#{…}
变量选择:*{…}
片段引用: ~{…}
常见:
文本: ‘one text’,‘another one!’,…
数字: 0,34,3.0,12.3,…
布尔:true、false
null: null
变量名: one,sometext,main…
文本操作:
拼串: +
文本替换:| The name is ${name} |
布尔操作:
比较运算:
比较:>,<,<=,>=(gt,lt,ge,le)
等值运算:==,!=(eq,ne)
条件运算:
if-then: (if)?(then)
if-then-else: (if)?(then):(else)
default: (value)?:(defaultValue)
特殊语法:
所有以上都可以嵌套组合
1 'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))
3. 属性设置
th:href=“@/product/list ”
th:attr=“class=${active}”
th:attr=“src=@/images/gtvglogo.png ,title=${logo},alt=#{logo}”
th:checked=“${user.active}”
1 2 3 4 5 6 <p th:text ="${content}" > 原内容</p > <a th:href ="${url}" > 登录</a > <img src ="../../images/gtvglogo.png" th:attr ="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
4. 遍历
语法: th:each="元素名,迭代状态 : ${集合}"
1 2 3 4 5 6 7 8 9 10 11 <tr th:each ="prod : ${prods}" > <td th:text ="${prod.name}" > Onions</td > <td th:text ="${prod.price}" > 2.41</td > <td th:text ="${prod.inStock}? #{true} : #{false}" > yes</td > </tr > <tr th:each ="prod,iterStat : ${prods}" th:class ="${iterStat.odd}? 'odd'" > <td th:text ="${prod.name}" > Onions</td > <td th:text ="${prod.price}" > 2.41</td > <td th:text ="${prod.inStock}? #{true} : #{false}" > yes</td > </tr >
iterStat 有以下属性:
index:当前遍历元素的索引,从 0 开始
count:当前遍历元素的索引,从 1 开始
size:需要遍历元素的总数量
current:当前正在遍历的元素对象
even/odd:是否偶数/奇数行
first:是否第一个元素
last:是否最后一个元素
5. 判断
th:if
1 2 3 4 5 <a href ="comments.html" th:href ="@{/product/comments(prodId=${prod.id})}" th:if ="${not #lists.isEmpty(prod.comments)}" > view</a
th:switch
1 2 3 4 5 <div th:switch ="${user.role}" > <p th:case ="'admin'" > User is an administrator</p > <p th:case ="#{roles.manager}" > User is a manager</p > <p th:case ="*" > User is some other thing</p > </div >
6. 属性优先级
1 2 3 4 5 <ul > <li th:each ="item : ${items}" th:text ="${item.description}" > Item description here... </li > </ul >
Order
Feature
Attributes
1
片段包含
th:insert th:replace
2
遍历
th:each
3
判断
th:if th:unless th:switch th:case
4
定义本地变量
th:object th:with
5
通用方式属性修改
th:attr th:attrprepend th:attrappend
6
指定属性修改
th:value th:href th:src …
7
文本值
th:text th:utext
8
片段指定
th:fragment
9
片段移除
th:remove
7. 行内写法
1 2 [[...]] or [(...)] <p>Hello, [[${session.user.name}]]!</p>
8. 变量选择
1 2 3 4 5 <div th:object ="${session.user}" > <p > Name: <span th:text ="*{firstName}" > Sebastian</span > .</p > <p > Surname: <span th:text ="*{lastName}" > Pepper</span > .</p > <p > Nationality: <span th:text ="*{nationality}" > Saturn</span > .</p > </div >
等同于
1 2 3 4 5 <div > <p > Name: <span th:text ="${session.user.firstName}" > Sebastian</span > .</p > <p > Surname: <span th:text ="${session.user.lastName}" > Pepper</span > .</p > <p > Nationality: <span th:text ="${session.user.nationality}" > Saturn</span > .</p > </div
9. 模板布局
定义模板: th:fragment
引用模板:~{templatename::selector}
插入模板:th:insert
、th:replace
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <footer th:fragment ="copy" > © 2011 The Good Thymes Virtual Grocery</footer > <body > <div th:insert ="~{footer :: copy}" > </div > <div th:replace ="~{footer :: copy}" > </div > </body > <body > 结果: <body > <div > <footer > © 2011 The Good Thymes Virtual Grocery</footer > </div > <footer > © 2011 The Good Thymes Virtual Grocery</footer > </body > </body >
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > </dependency >
修改页面后;ctrl+F9
刷新效果;
java 代码的修改,如果devtools
热启动了,可能会引起一些
bug,难以排查
6. 国际化
国际化的自动配置参照MessageSourceAutoConfiguration
实现步骤 :
Spring Boot 在类路径根下查找 messages
资源绑定文件。文件名为:messages.properties
多语言可以定义多个消息文件,命名为messages_区域代码.properties
。如:
messages.properties
:默认
messages_zh_CN.properties
:中文环境
messages_en_US.properties
:英语环境
在程序中 可以自动注入
MessageSource
组件,获取国际化的配置项值
在页面中 可以使用表达式
#{}
获取国际化的配置项值
1 2 3 4 5 6 7 8 9 @Autowired MessageSource messageSource; @GetMapping("/haha") public String haha (HttpServletRequest request) { Locale locale = request.getLocale(); String login = messageSource.getMessage("login" , null , locale); return login; }
7. 错误处理
1. 默认机制
错误处理的自动配置 都在ErrorMvcAutoConfiguration
中,两大核心机制:
SpringBoot
会自适应 处理错误,响应页面 或JSON
数据
SpringMVC 的错误处理机制 依然保留,MVC
处理不了 ,才会交给 boot 进行处理
发生错误以后,转发给/error 路径,SpringBoot 在底层写好一个
BasicErrorController 的组件,专门处理这个请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml (HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null ) ? modelAndView : new ModelAndView ("error" , model); } @RequestMapping public ResponseEntity<Map<String, Object>> error (HttpServletRequest request) { HttpStatus status = getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity <>(status); } Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)); return new ResponseEntity <>(body, status); }
1 2 3 4 ModelAndView modelAndView = resolveErrorView(request, response, status, model);return (modelAndView != null ) ? modelAndView : new ModelAndView ("error" , model);
容器中专门有一个错误视图解析器
1 2 3 4 5 6 @Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean(ErrorViewResolver.class) DefaultErrorViewResolver conventionErrorViewResolver () { return new DefaultErrorViewResolver (this .applicationContext, this .resources); }
SpringBoot 解析自定义错误页的默认规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Override public ModelAndView resolveErrorView (HttpServletRequest request, HttpStatus status, Map<String, Object> model) { ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; } private ModelAndView resolve (String viewName, Map<String, Object> model) { String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this .templateAvailabilityProviders.getProvider(errorViewName, this .applicationContext); if (provider != null ) { return new ModelAndView (errorViewName, model); } return resolveResource(errorViewName, model); } private ModelAndView resolveResource (String viewName, Map<String, Object> model) { for (String location : this .resources.getStaticLocations()) { try { Resource resource = this .applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html" ); if (resource.exists()) { return new ModelAndView (new HtmlResourceView (resource), model); } } catch (Exception ex) { } } return null ; }
容器中有一个默认的名为 error 的 view; 提供了默认白页功能
1 2 3 4 5 @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView () { return this .defaultErrorView; }
封装了 JSON 格式的错误信息
1 2 3 4 5 @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes () { return new DefaultErrorAttributes (); }
规则:
解析一个错误页
如果发生了错误码 500、404、503、403 精确匹配
如果有模板引擎,默认在
classpath:/templates/error/精确码.html
如果没有模板引擎,在静态资源文件夹下找精确码.html
如果匹配不到精确码.html
这些精确的错误页,就去找5xx.html
,4xx.html
模糊匹配
如果有模板引擎,默认在
classpath:/templates/error/5xx.html
如果没有模板引擎,在静态资源文件夹下找5xx.html
如果模板引擎路径templates
下有
error.html
页面,就直接渲染
2. 自定义错误响应
1. 自定义 json 响应
使用@ControllerAdvice + @ExceptionHandler
进行统一异常处理
2. 自定义页面响应
根据 boot 的错误页面查找规则,自定义页面模板
3. 最佳实战
前后分离
@ControllerAdvice + @ExceptionHandler
进行统一异常处理。
服务端页面渲染
页面,JSON,可用的 Model 数据如下
8. 嵌入式容器
Servlet 容器 :管理、运行Servlet
组件 (Servlet、Filter、Listener)的环境,一般指服务器
1. 自动配置原理
SpringBoot 默认嵌入 Tomcat 作为 Servlet 容器。
自动配置类 是ServletWebServerFactoryAutoConfiguration
,EmbeddedWebServerFactoryCustomizerAutoConfiguration
自动配置类开始分析功能。xxxxAutoConfiguration
1 2 3 4 5 6 7 8 9 10 11 12 @AutoConfiguration @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @ConditionalOnClass(ServletRequest.class) @ConditionalOnWebApplication(type = Type.SERVLET) @EnableConfigurationProperties(ServerProperties.class) @Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, ServletWebServerFactoryConfiguration.EmbeddedTomcat.class, ServletWebServerFactoryConfiguration.EmbeddedJetty.class, ServletWebServerFactoryConfiguration.EmbeddedUndertow.class }) public class ServletWebServerFactoryAutoConfiguration {}
ServletWebServerFactoryAutoConfiguration
自动配置了嵌入式容器场景
绑定了ServerProperties
配置类,所有和服务器有关的配置
server
ServletWebServerFactoryAutoConfiguration
导入了嵌入式的三大服务器
Tomcat
、Jetty
、Undertow
导入 Tomcat
、Jetty
、Undertow
都有条件注解。系统中有这个类才行(也就是导了包)
默认Tomcat
配置生效。给容器中放
TomcatServletWebServerFactory
都给容器中 ServletWebServerFactory
放了一个 web
服务器工厂(造 web 服务器的)
web
服务器工厂都有一个功能, getWebServer
获取 web
服务器
TomcatServletWebServerFactory 创建了 tomcat。
ServletWebServerFactory 什么时候会创建 webServer 出来。
ServletWebServerApplicationContext
ioc
容器,启动的时候会调用创建 web 服务器
Spring
容器刷新(启动) 的时候,会预留一个时机,刷新子容器。onRefresh()
refresh() 容器刷新十二大步的刷新子容器会调用
onRefresh()
;
Web 场景的 Spring 容器启动,在 onRefresh 的时候,会调用创建 web
服务器的方法。
Web 服务器的创建是通过 WebServerFactory
搞定的。容器中又会根据导了什么包条件注解,启动相关的服务器配置,默认EmbeddedTomcat
会给容器中放一个
TomcatServletWebServerFactory
,导致项目启动,自动创建出
Tomcat
2. 自定义
切换服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <properties > <servlet-api.version > 3.1.0</servlet-api.version > </properties > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <exclusions > <exclusion > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-tomcat</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jetty</artifactId > </dependency >
3. 最佳实践
用法:
修改server
下的相关配置就可以修改服务器参数
通过给容器中放一个ServletWebServerFactory
,来禁用掉
SpringBoot
默认放的服务器工厂,实现自定义嵌入任意服务器 。
9. 全面接管 SpringMVC
1.
WebMvcAutoConfiguration 到底自动配置了哪些规则
SpringMVC
自动配置场景给我们配置了如下所有默认行为
WebMvcAutoConfiguration
web 场景的自动配置类
支持 RESTful 的 filter:HiddenHttpMethodFilter
支持非 POST 请求,请求体携带数据:FormContentFilter
导入EnableWebMvcConfiguration
:
RequestMappingHandlerAdapter
WelcomePageHandlerMapping
:
欢迎页功能支持(模板引擎目录、静态资源目录放
index.html),项目访问/就默认展示这个页面。
RequestMappingHandlerMapping
:找每个请求由谁处理的映射关系
ExceptionHandlerExceptionResolver
:默认的异常解析器
LocaleResolver
:国际化解析器
ThemeResolver
:主题解析器
FlashMapManager
:临时数据共享
FormattingConversionService
: 数据格式化
、类型转化
Validator
:
数据校验JSR303
提供的数据校验功能
WebBindingInitializer
:请求参数的封装与绑定
ContentNegotiationManager
:内容协商管理器
WebMvcAutoConfigurationAdapter
配置生效,它是一个WebMvcConfigurer
,定义
mvc 底层组件
定义好 WebMvcConfigurer
底层组件默认功能;所有功能详见列表
视图解析器:InternalResourceViewResolver
视图解析器:BeanNameViewResolver
,视图名(controller
方法的返回值字符串) 就是组件名
内容协商解析器:ContentNegotiatingViewResolver
请求上下文过滤器:RequestContextFilter
:
任意位置直接获取当前请求
静态资源链规则
ProblemDetailsExceptionHandler
:错误详情
SpringMVC 内部场景异常被它捕获:
定义了 MVC 默认的底层行为: WebMvcConfigurer
2. @EnableWebMvc 禁用默认行为
@EnableWebMvc
给容器中导入DelegatingWebMvcConfiguration
组件,他是
WebMvcConfigurationSupport
WebMvcAutoConfiguration
有一个核心的条件注解,
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
,容器中没有WebMvcConfigurationSupport
,WebMvcAutoConfiguration
才生效.
@EnableWebMvc 导入
WebMvcConfigurationSupport
导致
WebMvcAutoConfiguration
失效。导致禁用了默认行为
@EnableWebMVC 禁用了 Mvc
的自动配置
WebMvcConfigurer 定义 SpringMVC 底层组件的功能类
定义扩展 SpringMVC 底层功能
提供方法
核心参数
功能
默认
addFormatters
FormatterRegistry
格式化器 :支持属性上@NumberFormat 和@DatetimeFormat
的数据类型转换
GenericConversionService
getValidator
无
数据校验 :校验 Controller 上使用@Valid
标注的参数合法性。需要导入 starter-validator
无
addInterceptors
InterceptorRegistry
拦截器 :拦截收到的所有请求
无
configureContentNegotiation
ContentNegotiationConfigurer
内容协商 :支持多种数据格式返回。需要配合支持这种类型的
HttpMessageConverter
支持 json
configureMessageConverters
List<HttpMessageConverter<?>>
消息转换器 :标注@ResponseBody 的返回值会利用
MessageConverter 直接写出去
8 个,支持 byte,string,multipart,resource,json
addViewControllers
ViewControllerRegistry
视图映射 :直接将请求路径与物理视图映射。用于无 java
业务逻辑的直接视图页渲染
无
configureViewResolvers
ViewResolverRegistry
视图解析器 :逻辑视图转为物理视图
ViewResolverComposite
addResourceHandlers
ResourceHandlerRegistry
静态资源处理 :静态资源路径映射、缓存控制
ResourceHandlerRegistry
configureDefaultServletHandling
DefaultServletHandlerConfigurer
默认 Servlet :可以覆盖 Tomcat 的 DefaultServlet。让
DispatcherServlet 拦截/
无
configurePathMatch
PathMatchConfigurer
路径匹配 :自定义 URL
路径匹配。可以自动为所有路径加上指定前缀,比如 /api
无
configureAsyncSupport
AsyncSupportConfigurer
异步支持 :
TaskExecutionAutoConfiguration
addCorsMappings
CorsRegistry
跨域 :
无
addArgumentResolvers
List
参数解析器 :
mvc 默认提供
addReturnValueHandlers
List
返回值解析器 :
mvc 默认提供
configureHandlerExceptionResolvers
List
异常处理器 :
默认 3 个 ExceptionHandlerExceptionResolver
ResponseStatusExceptionResolver DefaultHandlerExceptionResolver
getMessageCodesResolver
无
消息码解析器 :国际化使用
无
10. 最佳实践
SpringBoot 已经默认配置好了Web
开发 场景常用功能。我们直接使用即可。
三种方式
方式
用法
效果
全自动
直接编写控制器逻辑
全部使用自动配置默认效果
手自一体
@Configuration
+ 配置WebMvcConfigurer
+
配置 WebMvcRegistrations
不要标注 @EnableWebMvc
保留自动配置效果 手动设置部分功能
定义 MVC 底层组件
全手动
@Configuration
+ 配置WebMvcConfigurer
标注 @EnableWebMvc
禁用自动配置效果 全手动设置
两种模式
1、前后分离模式
: @RestController 响应 JSON 数据
2、前后不分离模式
:@Controller + Thymeleaf 模板引擎
11. Web 新特性
1. Problemdetails
RFC 7807: https://www.rfc-editor.org/rfc/rfc7807
错误信息 返回新格式
原理
1 2 3 4 5 6 7 8 9 10 11 12 @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.mvc.problemdetails", name = "enabled", havingValue = "true") static class ProblemDetailsErrorHandlingConfiguration { @Bean @ConditionalOnMissingBean(ResponseEntityExceptionHandler.class) ProblemDetailsExceptionHandler problemDetailsExceptionHandler () { return new ProblemDetailsExceptionHandler (); } }
ProblemDetailsExceptionHandler
是一个
@ControllerAdvice
集中处理系统异常
处理以下异常。如果系统出现以下异常,会被 SpringBoot 支持以
RFC 7807
规范方式返回错误数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @ExceptionHandler({ HttpRequestMethodNotSupportedException.class, //请求方式不支持 HttpMediaTypeNotSupportedException.class, HttpMediaTypeNotAcceptableException.class, MissingPathVariableException.class, MissingServletRequestParameterException.class, MissingServletRequestPartException.class, ServletRequestBindingException.class, MethodArgumentNotValidException.class, NoHandlerFoundException.class, AsyncRequestTimeoutException.class, ErrorResponseException.class, ConversionNotSupportedException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, BindException.class })
效果:
默认响应错误的 json。状态码 405
1 2 3 4 5 6 7 8 { "timestamp" : "2023-04-18T11:13:05.515+00:00" , "status" : 405 , "error" : "Method Not Allowed" , "trace" : "org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' is not supported\r\n\tat org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.handleNoMatch(RequestMappingInfoHandlerMapping.java:265)\r\n\tat org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod(AbstractHandlerMethodMapping.java:441)\r\n\tat org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:382)\r\n\tat org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:126)\r\n\tat org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:68)\r\n\tat org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:505)\r\n\tat org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1275)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1057)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:563)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:631)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:166)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.base/java.lang.Thread.run(Thread.java:833)\r\n" , "message" : "Method 'POST' is not supported." , "path" : "/list" }
开启 ProblemDetails 返回, 使用新的 MediaType
Content-Type: application/problem+json
+ 额外扩展返回
1 2 3 4 5 6 7 { "type" : "about:blank" , "title" : "Method Not Allowed" , "status" : 405 , "detail" : "Method 'POST' is not supported." , "instance" : "/list" }
2. 函数式 Web
SpringMVC 5.2
以后
允许我们使用函数式 的方式,定义 Web
的请求处理流程 。
函数式接口
Web 请求处理的方式:
@Controller + @RequestMapping
:耦合式
(路由 、业务 耦合)
函数式 Web :分离式(路由、业务分离)
1. 场景
场景:User RESTful - CRUD
GET /user/1 获取 1 号用户
GET /users 获取所有用户
POST /user 请求体 携带 JSON,新增一个用户
PUT /user/1 请求体 携带 JSON,修改 1 号用户
DELETE /user/1 删除 1 号用户
2. 核心类
RouterFunction
RequestPredicate
ServerRequest
ServerResponse
3. 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.MediaType;import org.springframework.web.servlet.function.RequestPredicate;import org.springframework.web.servlet.function.RouterFunction;import org.springframework.web.servlet.function.ServerResponse;import static org.springframework.web.servlet.function.RequestPredicates.accept;import static org.springframework.web.servlet.function.RouterFunctions.route;@Configuration(proxyBeanMethods = false) public class MyRoutingConfiguration { private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON); @Bean public RouterFunction<ServerResponse> routerFunction (MyUserHandler userHandler) { return route() .GET("/{user}" , ACCEPT_JSON, userHandler::getUser) .GET("/{user}/customers" , ACCEPT_JSON, userHandler::getUserCustomers) .DELETE("/{user}" , ACCEPT_JSON, userHandler::deleteUser) .build(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import org.springframework.stereotype.Component;import org.springframework.web.servlet.function.ServerRequest;import org.springframework.web.servlet.function.ServerResponse;@Component public class MyUserHandler { public ServerResponse getUser (ServerRequest request) { ... return ServerResponse.ok().build(); } public ServerResponse getUserCustomers (ServerRequest request) { ... return ServerResponse.ok().build(); } public ServerResponse deleteUser (ServerRequest request) { ... return ServerResponse.ok().build(); } }