WebMvcAutoConfiguration原理
生效条件
            
            
              java
              
              
            
          
          @AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class }) //在这些自动配置之后,再进行配置
@ConditionalOnWebApplication(type = Type.SERVLET) //如果是web应用就生效,类型SERVLET 普通web、REACTIVE 响应式web
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) //容器中没有这个Bean,才生效。默认就是没有
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)//优先级
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration { 
}
        效果
- 放了两个Filter:
- HiddenHttpMethodFilter;页面表单提交Rest请求(GET、POST、PUT、DELETE)
 - FormContentFilter:表单内容Filter,GET(数据放URL后面)、POST(数据放请求体)请求可以携带数据,PUT、DELETE的请求体数据会被忽略
 
 - 给容器中放了 WebMvcConfigurer 组件;给SpringMVC添加各种定制功能
- 所有的功能最终会和配置文件进行绑定
 - WebMvcProperties:spring.mvc 配置文件
 - WebProperties:spring.web 配置文件
 
 
            
            
              java
              
              
            
          
          	@Configuration(proxyBeanMethods = false)
	@Import(EnableWebMvcConfiguration.class) //额外导入了其他配置
	@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
	@Order(0)
	public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware{
        
    }
        WebMvcConfigurer接口
提供了配置SpringMVC底层的所有组件入口

静态资源规则源码
            
            
              java
              
              
            
          
          @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    //1、
    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,如果服务这个资源没有发生变化,下次访问的时候就可以直接让浏览器用自己缓存中的东西,而不用给服务器发请求。
            
            
              java
              
              
            
          
          registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
        EnableWebMvcConfiguration 源码
            
            
              java
              
              
            
          
          //SpringBoot 给容器中放 WebMvcConfigurationSupport 组件。
//我们如果自己放了 WebMvcConfigurationSupport 组件,Boot的WebMvcAutoConfiguration都会失效。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware 
{
    
}
        - HandlerMapping :根据请其路径,找哪个handler能够处理请求
- WelcomePageHandlerMapping :
- 访问***/***** 路径下的所有请求,都在以前四个静态资源路径下找,欢迎页也一样
 - 找 index.html :只要静态资源的位置有一个 index.html页面,项目启动默认访问
 
 
 - WelcomePageHandlerMapping :
 
为什么容器中放一个WebMvcConfigurer就能配置底层行为?
- WebMvcAutoConfiguration 是一个自动配置类,在他里边有一个 EnableWebMvcConfiguration
 - EnableWebMvcConfiguration 继承于 DelegatingWebMvcConfiguration,这两个都生效
 - DelegatingWebMvcConfiguration 利用 DI 把容器中 所有 WebMvcConfigurer注入进来
 - 别人调用 DelegatingWebMvcConfiguration 的方法配置底层规则,而它调用所有 WebMvcConfigurer的配置底层方法
 
WebMvcConfigurationSupport
提供了很多的默认设置
判断系统中是否有相应的类:如果有,就加入相应的 HttpMessageConverter
            
            
              java
              
              
            
          
          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);
        Web场景
自动配置
- 整合web场景
 
            
            
              XML
              
              
            
          
                  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        - 引入了autoconfigure功能
 - @EnableConfiguration注解使用@Import(AutoConfigurationImportSelector.class)批量导入组件
 - 加载META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中配置的所有组件
 - 所有自动配置类如下
 
            
            
              java
              
              
            
          
          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
        - 绑定了配置文件的一堆配置项
- SpringMVC的所有配置spring.mvc
 - Web场景通用配置spring.web
 - 文件上传配置spring.servlet.multipart
 - 服务器的配置server
 
 
默认效果
默认配置:
- 包含了ContentNegotiatingViewResolver 和 BeanNameViewResolver 组件,方便视图解析
 - 默认的静态资源处理机制 : 静态资源放在 static文件夹下即可直接访问
 - 自动注册了 Converter,GenericConverter,Formatter 组件,适配常见数据类型转换和格式化需求
 - 支持 HttpMessageConverters ,可以方便返回 json 等数据类型
 - 注册 MessageCodesResolver ,方便国际化及错误消息处理
 - 支持 静态 index.html
 - 自动使用 ConfigurableWebBindingInitializer ,实现消息处理 、数据绑定 、类型转化 、数据校验等功能
 
重要:
- 如果想保持 boot mvc 的默认配置 ,并且自定义更多的 mvc 配置,如:interceptors , formatters , view controllers 等。可以使用***@Configuration*** 注解添加一个 WebMvcConfigurer 类型的配置类,并不要标注 @EnableWebMvc
 - 如果想保持 boot mvc 的默认配置,但要自定义核心组件实例,比如:RequestMappingHandlerMapping, RequestMappingHandlerAdapter, 或ExceptionHandlerExceptionResolver ,给容器中放一个 WebMvcRegistrations 组件即可
 - 如果想全面接管 Spring MVC,@Configuration 标注一个配置类,并加上 @EnableWebMvc 注解,实现 WebMvcConfigurer 接口
 
静态资源
默认规则
静态资源映射
静态资源映射规则在WebMvcAutoConfiguration中进行了定义
- /webjars/** 的所有路径资源都在 classpath:/META-INF/resources/webjars/
 - /** 的所有路径资源都在 classpath:/META-INF/resources/、classpath:/resources/、classpath:/static/、classpath:/public/
 - 所有静态资源都定义了缓存规则 。【浏览器访问过一次,就会缓存一段时间】,但此功能参数无默认值
- period:缓存间隔。默认0s
 - cacheControl:缓存控制。默认无
 - useLastModified :是否使用 lastModified头。默认false;
 
 
静态资源缓存
如前面所述
- 所有静态资源都定义了缓存规则 。【浏览器访问过一次,就会缓存一段时间】,但此功能参数无默认值
- period:缓存间隔。默认0s
 - cacheControl:缓存控制。默认无
 - useLastModified :是否使用 lastModified头。默认false;
 
 
欢迎页
欢迎页规则在 WebMvcAutoConfiguration 中进行了定义:
- 在静态资源 目录下找 index.html
 - 没有就在templates 下找 index 模板页
 
Favicon
- 在静态资源目录下找 favicon.ico
 
缓存实验
            
            
              XML
              
              
            
          
          #1、spring.web:
#   1、配置国际化的区域信息
#   2、配置静态资源策略(设置静态路径地址、是否开启静态资源的映射、静态资源的处理链、静态资源的缓存规则)
# 开启静态资源映射规则
spring.web.resources.add-mappings=true
# 设置缓存
spring.web.resources.cache.period=3600
# 缓存详细的合并项控制,会覆盖period配置
# 浏览器第一次请求服务器,服务器告诉浏览器此资源缓存7200s,7200s以内的所有此资源的访问不用发给服务器请求,7200s以后再发请求给服务器
spring.web.resources.cache.cachecontrol.max-age=7200
# 使用资源的 最后一次修改时间,来对比服务器和浏览器的资源是否相同,相同就会返回304
spring.web.resources.cache.use-last-modified=true
        自定义静态资源规则
自定义静态资源路径、自定义缓存规则
配置方式
spring.mvc:静态资源访问前缀路径
- 静态资源目录
 - 静态资源缓存策略
 
            
            
              XML
              
              
            
          
          #1、spring.web:
#   1、配置国际化的区域信息
#   2、配置静态资源策略(设置静态路径地址、是否开启静态资源的映射、静态资源的处理链、静态资源的缓存规则)
# 开启静态资源映射规则
spring.web.resources.add-mappings=true
# 设置缓存
spring.web.resources.cache.period=3600
# 缓存详细的合并项控制,会覆盖period配置
# 浏览器第一次请求服务器,服务器告诉浏览器此资源缓存7200s,7200s以内的所有此资源的访问不用发给服务器请求,7200s以后再发请求给服务器
spring.web.resources.cache.cachecontrol.max-age=7200
# 可以设置共享缓存
spring.web.resources.cache.cachecontrol.cache-public=true
# 使用资源的 最后一次修改时间,来对比服务器和浏览器的资源是否相同,相同就会返回304
spring.web.resources.cache.use-last-modified=true
spring.web.resources.static-locations=classpath:/a/,classpath:/b/,classpath:/resources/,classpath:/static/
#2、spring.mvc
## 2.1、自定义webjars访问的路径前缀
spring.mvc.webjars-path-pattern=/wj/**
## 2.2、自定义静态资源访问路径的前缀
spring.mvc.static-path-pattern=/static/**
        代码方式
- 容器中只要有一个WebMvcConfigurer组件,配置的底层行为都会生效
 - @EnableWebMvc // 禁用boot的默认配置
 
            
            
              java
              
              
            
          
          @Configuration // 这是一个配置类
public class MyConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 保留以前默认的功能
        WebMvcConfigurer.super.addResourceHandlers(registry);
        // 自定义
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/a/", "classpath:/b/")
                .setCacheControl(CacheControl.maxAge(1180, TimeUnit.SECONDS));
    }
}
        
            
            
              java
              
              
            
          
          @Configuration //这是一个配置类,给容器中放一个 WebMvcConfigurer 组件,就能自定义底层
public class MyConfig  /*implements WebMvcConfigurer*/ {
    @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));
            }
        };
    }
}
        路径匹配
Spring5.3 之后加入了更多的请求路径匹配的实现策略
以前只支持AbtPathMatcher 策略,现在提供了PathPatternParser策略,并且可以让我们指定使用哪种策略
Ant风格路径用法
Ant风格的路径模式语法具有以下规则:
- 
*:表示任意数量的字符
 - 
?:表示任意一个字符
 - 
**:表示任意数量的目录
 - 
{}:表示一个命名的模式占位符
 - 
\]:表示**字符集合** ,例如 ***\[a-z \]***表示小写字母
 - 
*.html 匹配任意名称,扩展名为 .html的文件。
 - 
/folder1/*/*.java 匹配在folder1 目录下的任意两级目录下的***.java***文件。
 - 
/folder2/**/*.jsp 匹配在folder2 目录下任意目录深度的***.jsp***文件。
 - 
/{type}/{id}.html 匹配任意文件名为***{id}.html*** ,在任意命名的***{type}***目录下的文件。
 
注意:
Ant 风格的路径模式语法中的特殊字符需要转义,如:
- 要匹配文件路径中的星号,则需要转义为***\\****。
 - 要匹配文件路径中的问号,则需要转义为***\\?***。
 
模式切换
AntPathMather 与 PathPatternParser
- PathPatternParser 在 jmh 基准测试下,有 6~8 倍吞吐量提升,降低 30%~40%空间分配率
 - PathPatternParser 兼容 AntPathMatcher语法,并支持更多类型的路径模式
 - PathPatternParser "**" 多段匹配 的支持仅允许在模式末尾使用
 
            
            
              java
              
              
            
          
          @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;
}
        总结:
- springboot默认使用的路径匹配规则,是由 PathPatternParser提供的
 - 如果路径中间需要有 **,那么就需要替换成 ant 风格路径
 
            
            
              java
              
              
            
          
          # 改变路径匹配策略:
# ant_path_matcher 老版策略;
# path_pattern_parser 新版策略;
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
        内容协商
一套系统适配多端数据返回

多端内容适配
默认规则
SpringBoot多端内容适配
- 基于请求头内容协商 :(默认开启)
- 客户端向服务端发送请求,携带HTTP标准的Accept请求头
 - Accept :application/json 、test/xml 、text/yaml
 - 服务端根据客户端请求头期望的数据类型 进行动态返回
 
 - 基于请求参数内容协商:(需要开启)
- 发送请求 GET/projects/spring-boot?format=json
 - 匹配到 @GetMapping("/projects/springboot")
 - 根据参数协商 ,优先返回json类型数据**【需要开启参数匹配设置】**
 - 发送请求 GET/projects/spring-boot?forat=xml 优先返回 xml 类型数据
 
 
效果演示
请求同一个接口,可以返回json和xml不同格式数据
引入支持写出xml内容依赖
            
            
              XML
              
              
            
          
          <dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
        标注注解
            
            
              java
              
              
            
          
          @JacksonXmlRootElement  // 可以写出为xml文档
@Data
public class Person {
    private Long id;
    private String userName;
    private String email;
    private Integer age;
}
        开启基于请求参数的内容协商
            
            
              XML
              
              
            
          
          # 开启基于请求参数的内容协商功能。 默认参数名:format。 默认此功能不开启
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商时使用的参数名。默认是 format
spring.mvc.contentnegotiation.parameter-name=type
        效果


自定义内容返回
增加yaml返回支持
导入依赖
            
            
              XML
              
              
            
          
          <!--支持返回yaml格式数据-->
<dependency>
	<groupId>com.fasterxml.jackson.dataformat</groupId>
	<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
        把对象写出成YAML
            
            
              java
              
              
            
          
          public static void main(String[] args) throws JsonProcessingException {
    Person person = new Person();
    person.setId(1);
    person.setUserName("张三");
    person.setEmail("zhangsan@qq.com");
    person.setAge(20);
    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);
}
        编写配置
            
            
              java
              
              
            
          
          # 增加一种新的内容类型
spring.mvc.contentnegotiation.media-types.yaml=application/yaml
        增加 HttpMessageConverter组件,专门负责把对象写出为YAML格式
            
            
              java
              
              
            
          
          @Bean
public WebMvcConfigurer webMvcConfigurer() {
    WebMvcConfigurer webMvcConfigurer = new WebMvcConfigurer() {
        
        @Override // 配置一个能把对象转为yaml的messageConverter
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            converters.add(new MyYamlHttpMessageConverter());
        }
    };
    return webMvcConfigurer;
}
        思考:如何增加其他
- 配置媒体类型的支持:
- spring.mvc.contentnegotiation.media-types.yaml=application/yaml、
 
 - 编写对应的HttpMessageConverter ,要告诉springboot这个 HttpMessageConverter 支持的媒体类型
- super(new MediaType("application", "yaml", Charset.forName("UTF-8")));
 
 - 按照3的示例
- 把MessageConverter的组件加入到底层
 - 容器中放一个WebMvcConfigurer 组件,并配置底层的MessageConverter
- converters.add(new MyYamlHttpMessageConverter());
 
 
 
HttpMessageConverter的示例写法
            
            
              java
              
              
            
          
          public class MyYamlHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
    private ObjectMapper objectMapper = null; // 把对象转成yaml
    public MyYamlHttpMessageConverter() {
        super(new MediaType("application", "yaml", Charset.forName("UTF-8")));
//        // 媒体类型
//        MediaType mediaType = new MediaType("application", "yaml", Charset.forName("UTF-8"));
//        // 相当于告诉springboot这个message支持哪种媒体类型
//        super(mediaType);
        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 // @RequestBody
    protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }
    @Override // @ResponseBody 把对象怎么写出去
    protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        // try-with写法:自动关流
        try(OutputStream os = outputMessage.getBody()) {
            this.objectMapper.writeValue(os, o);
        }
    }
}
        内容协商原理 - HttpMessageConverter
- HttpMessageConverter怎么工作?
 - 定制 HttpMessageConverter来实现多端内容协商
 - 编写 WebConfigurer 提供的 configureMessageConverters 底层,修改底层的 MessageConverter
 
@ResponseBody 由 HttpMessageConverter处理
标注了***@ResponseBody*** 的返回值将会由支持它的 HttpMessageConverter 写给浏览器
- 如果controller方法的返回值标注了 @ResponseBody 注解
- 请求进来先来到 DispatcherServlet 的 doDispatcher() 进行处理
 - 找到一个 HandlerAdapter适配器,利用适配器执行目标方法
 - 使用@RequestMapping注解,HandlerAdapter 找到的 适配器就是 RequestMappingHandlerAdapter ,接下来就是 RequestMappingHandlerAdapter 调用 invokeHandlerMethod() 来执行目标方法
 - 目标方法执行之前,准备好两个东西
- HandlerMethodArgumentResolver:参数解析器,确定目标方法每个参数值
 - HandlerMethodReturnValueHandler:返回值处理器,确定目标方法的返回值该怎么处理
 
 - RequestMappingHandlerAdapter 里面的 invokeAndHandler() 真正执行目标方法
 - 目标方法执行完成,会返回返回值对象
 - 这时候就需要找到一个合适的返回值处理器 HandlerMethodReturnValueHandler
 - 最终找到RequestResponseBodyMethodProcessor 能处理标注了 @ResponseBody注解的方法
 - RequestResponseBodyMethodProccess 调用 writeWithMessgaeConverters,利用 MessageConverter 把返回值写出去
 
 
上面解释:@ResponseBody 由 HttpMessageConverter 处理
- HttpMessageConverter 会先进行内容协商
- 遍历所有的 MessageConverter ,看谁支持这种内容类型的数据
 - 默认 MessageConverter有以下
 
- 最终因为要 json 所以 MappingJackson2HttpMessageConverter 支持写出 json
 - jackson 用 ObjectMapper把对象写出去
 
 
WebMvcAutoCnfiguration 提供几种默认 HttpMessageConverters
EnableWebConfiguration 通过 addDefaultHttpMessageConverters 添加了默认的 MessageConverter;如下:
- ByteArrayHttpMessageConverter:支持字节数据读写
 - StringHttpMessageConverter:支持字符串读写
 - ResourceHttpMessageConverter:支持资源读写
 - ResourceRegionHttpMessageConverter:支持分区资源写出
 - AllEncompassingFormHttpMessageConverter:支持表单xml/json读写
 - MappingJackson2HttpMessageConverter:支持请求响应体json读写
 
默认8个:

系统提供的默认的 MessageConverter 功能有限,仅用于 json 或者普通返回数据。若想要额外增加新的内容协商,就必须增加新的 HttpMessageConverter
模板引擎
由于SpringBoot 使用了嵌入式Servlet容器 ,所以JSP 默认时不能使用的
如果需要服务端页面渲染 ,优先考虑使用模板引擎

模板引擎 页面默认放在src/main/resources/templates
SpringBoot包含以下模板引擎的自动配置
- FreeMarker
 - Groovy
 - Thymeleaf
 - Mustache
 
Thymeleaf官网 :Thymeleaf
如果想要有提示,那么就需要在 html 标签的后面加上 xmlns:th="http://www.thymeleaf.org"
            
            
              html
              
              
            
          
          <!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>
        Thymeleaf整合
            
            
              XML
              
              
            
          
          <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 的页面
 
 
基础语法
核心用法
th:xxx :动态渲染指定的 html 标签属性值、或者th指令(遍历、判断等)
- th:text :标签体内文本值渲染
- th:utext:不会转义,显示为html原本的样子
 
 - th:属性:标签指定属性渲染
 - th:attr:标签任意属性渲染
 - th:if 、th:each......:其他th指令
 - 例如:
 
            
            
              java
              
              
            
          
          @Controller // 适配服务端渲染  前后端不分离模式
public class WelcomeController {
    /**
     * 利用模板引擎跳转到指定页面
     * @return
     */
    @GetMapping("/welcome")
    public String hello(@RequestParam("name") String name,
                        Model model) {
        // 模板的逻辑视图名
        // 物理视图 = 前缀 + 逻辑视图名 + 后缀
        // 真实地址 = classpath:/templates/welcome.html
        // 把需要给页面共享的数据放到model中
        String text = "<span style='color:red'>" + name + "</span>";
        model.addAttribute("msg", text);
        model.addAttribute("name", name);
        // 路径是动态的
        model.addAttribute("imgUrl", "/3.jpg");
        // 数据库查出的样式
        model.addAttribute("style", "width: 200px");
        model.addAttribute("show", "true");
        return "welcome";
    }
}
        
            
            
              html
              
              
            
          
          <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>哈哈哈哈<span th:text="${msg}"></span></h1>
<hr>
<!--th:text 替换标签体的内容-->
<!--th:utext 替换标签体的内容,不会转移html标签,真正显示为html该有的样式-->
<h1 th:text="${msg}">哈哈</h1>
<h1 th:utext="${msg}">哈哈</h1>
<hr>
<!--利用工具类转大写-->
<h1 th:text="${#strings.toUpperCase(name)}"></h1>
<hr>
<!--字符串拼接-->
<h1 th:text="|前缀  ${name}  后缀|"></h1>
<hr>
<!--th:任意html属性  动态替换任意属性的值-->
<img th:src="@{${imgUrl}}" style="width: 300px"/>
<hr>
<img style="width: 300px" th:attr="src=@{${imgUrl}}, style=${style}"/>
</body>
<hr>
<!--th:其他指令-->
<img th:src="@{${imgUrl}}" th:style="${style}" th:if="${show}" />
<hr>
<!--@{} 专门用来取路径,动态调整路径-->
<img th:src="@{static/1.jpg}" />
</html>
        表达式 :用来动态取值
- *{}*:变量取值;使用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生成工具
 
语法示例
表达式
- 变量取值:${...}
 - url取值:@{...}
 - 国际化消息:#{...}
 - 变量选择:*{...}
 - 片段引用:~{...}
 
常见
- 文本:'one text' 、'another one'
 - 数字:0 ,34 ,3.0 ,12.3,...
 - 布尔:true 、false
 - null:null
 - 变量名:one 、sometext 、main...
 
文本操作
- 拼串:+
 - 文本替换:| The name is ${name} |
 
布尔操作
- 二进制运算:and 、or
 - 取反:! 、not
 
比较运算
- 比较:> 、< 、<= 、>= (gt 、lt 、ge 、le)
 - 等值运算:== 、!= (eq 、ne)
 
条件运算
- if-then:(if)?(then)
 - if-then-else:(if)?(then):(else)
 - default:(value)?:(defaultValue)
 
特殊语法
- 无操作:_
 
所有以上都可以嵌套组合
            
            
              java
              
              
            
          
          'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))
        属性设置
- th:herf="@{/product/list}"
 - th:attr="class=${active}"
 - th:attr="src=@{/images/gtvglogo.png},title=${logo},alt=#{logo}"
 - th:checked="${user.active}"
 
            
            
              html
              
              
            
          
          <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}" />
        遍历
语法:th:each="元素名,迭代状态 :${集合}"
            
            
              html
              
              
            
          
          <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:是否最后一个元素
 
判断
th:if
            
            
              html
              
              
            
          
          <a
  href="comments.html"
  th:href="@{/product/comments(prodId=${prod.id})}"
  th:if="${not #lists.isEmpty(prod.comments)}"
  >view</a>
        th:switch
            
            
              html
              
              
            
          
          <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>
        属性优先级
- 片段
 - 遍历
 - 判断
 
            
            
              html
              
              
            
          
          <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                            |
行内分析
[[......]] or [(......}]
            
            
              html
              
              
            
          
          <p>Hello, [[${session.user.name}]]!</p>
        变量选择
            
            
              html
              
              
            
          
          <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>
        等同于
            
            
              html
              
              
            
          
          <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
        模板布局
- 定义模板:th:fragment
 - 引用模板:~{模板名 ::片段名}
 - 插入模板:th:insert 、th:replace
 
            
            
              html
              
              
            
          
          // html文件名也叫footer,片段名叫copy
<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>
        devtools
            
            
              XML
              
              
            
          
          <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-devtools</artifactId>
</dependency>
        - 修改页面后;ctrl+F9刷新效果;
 - java代码的修改,如果 devtools热启动了,可能会引起一些bug,难以排查
 
国际化
国际化的自动配置参照 MessageSourceAutoConfiguration
实现步骤:
- Spring Boot 在类路径根下查找messages资源绑定文件。文件名为:messages.properties
 - 多语言可以定义多个消息文件,命名为messages_区域代码.properties 。如:
- messages.properties:默认
 - messages_zh_CN.properties:中文环境
 - messages_en_US.properties:英语环境
 
 - 在程序中 可以自动注入 MessageSource组件,获取国际化的配置项值
 - 在页面中 可以使用表达式 **#{}**获取国际化的配置项值
 
            
            
              java
              
              
            
          
          @Autowired  //国际化取消息用的组件
MessageSource messageSource;
@GetMapping("/haha")
public String haha(HttpServletRequest request){
    Locale locale = request.getLocale();
    //利用代码的方式获取国际化配置文件中指定的配置项的值
    String login = messageSource.getMessage("login", null, locale);
    return login;
}
        错误处理
默认机制
错误处理的自动配置 都在ErrorMvcAutoConfiguration中,两大核心机制:
- SpringBoot 会自适应处理错误 ,响应页面 或JSON数据
 - SpringMVC的错误处理机制 依然保留,MVC处理不了 ,才会交给boot进行处理
 

发生错误以后,转发给/error路径,SpringBoot在底层写好一个 BasicErrorController的组件,专门处理这个请求
            
            
              java
              
              
            
          
          @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) //返回HTML
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  //返回 ResponseEntity, JSON
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);
}
        - 错误页面是这么解析到的
 
            
            
              java
              
              
            
          
          //1、解析错误的自定义视图地址
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//2、如果解析不到错误页面的地址,默认的错误页就是 error
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
        容器中专门有一个错误视图解析器(在ErrorMvcAutoConfiguration)
            
            
              java
              
              
            
          
          @Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
    return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}
        SpringBoot解析自定义错误页的默认规则(在DefaultErrorViewResolver)
            
            
              java
              
              
            
          
          	@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; 提供了默认白页功能(在ErrorMvcAutoConfiguration)
            
            
              java
              
              
            
          
          @Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
    return this.defaultErrorView;
}
        在ErrorMvcAuoConfiguration中封装了JSON格式的错误信息
            
            
              java
              
              
            
          
          	@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
 
 
 - 如果发生了500、404、503、403 这些错误
 - 如果模板引擎路径templates下有 error.html页面,就直接渲染
 
自定义错误响应
自定义Json响应
使用@ControllerAdvice + @ExceptionHandler 进行统一异常处理
自定义页面响应
根据boot的错误页面规则,自定义页面模板
最佳实战
- 前后分离
- 后台发生的所有错误,使用**@ControllerAdvice** + @ExceptionHandler进行统一异常处理
 
 - 服务端渲染
- 不可预知的一些,HTTP码表示的服务器或客户端错误
- 给classpath:/templates/error/ 下面,放常用精确的错误码页面。500.html ,404.html
 - 给classpath:/templates/error/ 下面,放通用模糊匹配的错误码页面。 5xx.html ,4xx.html
 
 - 发生业务错误
- 核心业务 的每一种错误,都应该由代码控制,跳转到自己定制的错误页
 - 通用业务 ,classpath:/templates/error.html 页面,显示错误信息
 
 
 - 不可预知的一些,HTTP码表示的服务器或客户端错误
 
页面,JSON,可用的Model数据如下

嵌入式容器
Servlet容器 :管理、运行Servlet组件 (Servlet、Filter、Listener)的环境,一般指服务器
自动配置原理
- SpringBoot 默认嵌入Tomcat作为Servlet容器。
 - 自动配置类是ServletWebServerFactoryAutoConfiguration 、EmbeddedWebServerFactoryCustomizerAutoConfiguration
 
- 自动配置类开始分析功能。xxxxAutoConfiguration
 
            
            
              java
              
              
            
          
          @AutoConfiguration(
    after = {SslAutoConfiguration.class}
)
@AutoConfigureOrder(Integer.MIN_VALUE)
@ConditionalOnClass({ServletRequest.class})
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@EnableConfigurationProperties({ServerProperties.class})
@Import({BeanPostProcessorsRegistrar.class, ServletWebServerFactoryConfiguration.EmbeddedTomcat.class, ServletWebServerFactoryConfiguration.EmbeddedJetty.class, ServletWebServerFactoryConfiguration.EmbeddedUndertow.class})
public class ServletWebServerFactoryAutoConfiguration {
}
        - ServletWebServerFactoryAutoConfiguration 自动配置了嵌入式容器场景
 - 绑定了 ServerProperties 配置类,所有和服务器有关的配置 server
 - ServletWebServerFactoryAutoConfiguration 导入了 嵌入式的三大服务器 Tomcat 、Jetty 、Undertow
- 在ServletWebServerFactoryConfiguration 中,导入 Tomcat 、Jetty 、Undertow都有条件注解。系统中有这个类才行(也就是导了包)
 - 默认 Tomcat 配置生效。给容器中放 TomcatServletWebServerFactory
 - 都给容器中 ServletWebServerFactory 放了一个 web服务器工厂(造web服务器的)
 - web服务器工厂 都有一个功能 ,getWebServer获取web服务器
 - 在getWebServer 方法中会创建服务器,比如:TomcatServletWebServerFactory 的 getWebServer创建了 tomcat。
 
 - 那么什么时候 ServletWebServerFactory 会创建 webServer 出来。
- 在 ServletWebServerApplicationContext 中 有一个 createWebServer 的方法,里面调用了 getWebServer 方法,而 onRefresh() 方法会调用 createWebServer方法
 - 也就是说 Spring 容器刷新(启动的时候),会预留一个时机,刷新子容器。
 - refresh() 容器刷新 十二大步的刷新子容器会调用 onRefresh()
 
 
            
            
              java
              
              
            
          
          	@Override
	protected void onRefresh() {
		super.onRefresh();
		try {
			createWebServer();
		}
		catch (Throwable ex) {
			throw new ApplicationContextException("Unable to start web server", ex);
		}
	}
        
- Web场景的Spring容器启动,在onRefresh的时候,会调用创建web服务器的方法。
 - Web服务器的创建是通过WebServerFactory搞定的。容器中又会根据导了什么包条件注解,启动相关的 服务器配置,默认EmbeddedTomcat 会给容器中放一个 TomcatServletWebServerFactory,导致项目启动,自动创建出Tomcat。
 
自定义

切换服务器
            
            
              XML
              
              
            
          
          <properties>
    <servlet-api.version>3.1.0</servlet-api.version>
</properties>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <!-- Exclude the Tomcat dependency -->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- Use Jetty instead -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
        最佳实践
用法:
- 修改server下的相关配置就可以修改服务器参数
 - 通过给容器中放一个ServletWebServerFactory ,来禁用掉SpringBoot默认放的服务器工厂,实现自定义嵌入任意服务器。
 
全面接管SpringMVC
- SpringBoot 默认配置了 SpringMVC 的所有常用特性
 - 如果我们需要全面接管 SpringMVC 的所有配置并禁用默认配置 ,仅需要编写一个 WebMvcConfigurer 配置类,并标注 @EnableWebMvc 即可
 - 全手动模式:
- @EnableWebMvc:禁用默认配置
 - WebMvcConfigurer组件:定义MVC的底层行为
 
 
WebMvcAutoConfiguration 到底自动配置了哪些规则
SpringMVC自动配置场景给我们配置了如下所有默认行为
- WebMvcAutoConfiguration web场景的自动配置类
- 
支持 RESTful 的 filter:HiddenHttpMethodFilter
 - 
支持非POST请求,请求体携带数据:FormContentFilter
 - 
导入 EnableWebMvcConfiguration :
- 
RequestMappingHandlerAdapter
- RequestMappingHandlerAdapter 负责调用由 RequestMappingHandlerMapping确定的处理器方法。它处理方法的执行,包括参数解析和返回值处理
 
 - 
WelcomePageHandlerMapping:欢迎页功能支持(模板引擎目录、静态资源目录放index.html),项目访问的时候就默认展示这个页面
 - 
RequestMappingHandlerMapping
- 主要职责是将进入的 HTTP 请求映射到适当的处理器方法。处理器方法通常是用 @RequestMapping 注解标记的控制器方法
 
 - 
ExeptionHandlerExceptionResolver:默认的异常解析器
- 通过 @ExceptionHandler 注解和 @ControllerAdvice 注解提供了一种灵活且强大的方式来处理异常
 
 - 
LocaleResolver:国际化解析器
- 允许应用程序根据用户的区域设置动态调整语言和格式
 
 - 
ThemeResolver:主题解析器
- 为开发者提供了管理和切换主题的能力
 
 - 
FlashMapManager:临时数据共享
- 管理重定向时候的参数传递
 
 - 
FormattingConversionService:数据格式化、类型转化
*java# 自定义日期格式 spring.mvc.format.date=yyyy-MM-dd spring.mvc.format.time=HH:mm:ss - 
Validator:数据校验
- JSR303提供的数据校验功能,JSR303提供了一系列校验注解
 
 - 
WebBindingInitializer:请求参数的封装与绑定
 - 
ContentNegotiationManager:内容协商管理器
 
 - 
 - 
WebMvcAutoConfigurationAdapter 配置生效,它是一个 WebMvcConfigurer ,定义 mvc底层的组件,他事先会导入 EnableWebMvcConfiguration
- 定义好 WebMvcConfigurer 底层组件默认功能;所有功能详见下文列表
 - 视图解析器:InternalResourceViewResolver
- 适用于使用 JSP 作为视图技术的 Spring MVC 项目
 - 需要在配置文件中进行相应配置。通常,需要设置前缀和后缀来确定 JSP 页面的基本路径和文件扩展名
 - spring.mvc.view.prefix
 - spring.mvc.view.suffix
 
 - 视图解析器:BeanNameViewResolver
- 把返回的逻辑视图名称 去匹配 定义好的视图bean 对象
 - 例如:在某一个类上加上 @Component("aaa"),BeanNameViewResolver会根据返回的逻辑视图名称去匹配定义好的视图 bean 对象
 
 - 内容协商解析器:ContentNegotiatingViewResolver
- 根据客户端请求的媒体类型(MediaType)来选择合适的视图进行响应
 
 - 请求上下文过滤器:RequestContextFilter
- 
在 Web 应用程序中为每个请求创建并公开一个 RequestContext。这个上下文对象包含了请求、响应、Servlet 配置和其他相关信息
 - 
比如前端接收到一个请求,根据路径给到后端处理,这个时候后端的对应的方法中调用了 service 层的一个方法,那么 service 层的那个方法应该如何接收到前端传回来的参数呢?
- 按照之前的方法,就是直接传参数给 service层的方法
 - 但是用 RequestContextFilter可以在任意位置获取当前请求
 
java@Service public class AService { public void a(){ // 当前请求的路径 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); // 任意位置通过 ServletRequestAttributes 获取到当前请求和响应的信息 HttpServletRequest request = attributes.getRequest(); HttpServletResponse response = attributes.getResponse(); String requestURI = request.getRequestURI(); } } 
 - 
 
 - 
静态资源链规则:ResourceChainResourceHandlerRegistrationCustomizer
- 用来配置资源链的属性,包括压缩、版本控制和应用缓存
 
 - 
ProblemDetailsExceptionHandler :错误详情
- SpringMVC内部场景异常被它捕获
 
 
 - 
 
@EnableWebMvc 禁用默认行为
- @EnableWebMvc 给容器中导入 DelegatingWebMvcConfiguration 组件,它继承了 WebMvcConfigurationSupport
 - WebMvcAutoConfiguration 有一个核心的条件注解 @ConditionalOnMissingBean({WebMvcConfigurationSupport.class}) ,容器中没有WebMvcConfigurationSupport ,WebMvcAutoConfiguration才生效
 - @EnableWebMvc 导入了 WebMvcConfigurationSupport 导致 WebMvcAutoConfiguration失效,禁用了默认行为
 
- @EnableWebMVC 禁用了 Mvc 的自动配置
 - WebMvcConfigurer 定义 SpringMVC 底层组件的功能类
 
WebMvcConfigurer 功能
定义扩展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 业务逻辑的直接视图页渲染                    | 无 <mvc:view-controller>                                                                                |
| configureViewResolvers             | ViewResolverRegistry                    | 视图解析器:逻辑视图转为物理视图                                             | ViewResolverComposite                                                                                    |
| addResourceHandlers                | ResourceHandlerRegistry                 | 静态资源处理:静态资源路径映射、缓存控制                                         | ResourceHandlerRegistry                                                                                  |
| configureDefaultServletHandling    | DefaultServletHandlerConfigurer         | 默认 Servlet:可以覆盖 Tomcat 的DefaultServlet。让DispatcherServlet拦截/ | 无                                                                                                        |
| configurePathMatch                 | PathMatchConfigurer                     | 路径匹配:自定义 URL 路径匹配。可以自动为所有路径加上指定前缀,比如 /api                    | 无                                                                                                        |
| configureAsyncSupport              | AsyncSupportConfigurer                  | 异步支持:                                                        | TaskExecutionAutoConfiguration                                                                           |
| addCorsMappings                    | CorsRegistry                            | 跨域:                                                          | 无                                                                                                        |
| addArgumentResolvers               | List<HandlerMethodArgumentResolver>   | 参数解析器:                                                       | mvc 默认提供                                                                                                 |
| addReturnValueHandlers             | List<HandlerMethodReturnValueHandler> | 返回值解析器:                                                      | mvc 默认提供                                                                                                 |
| configureHandlerExceptionResolvers | List<HandlerExceptionResolver>        | 异常处理器:                                                       | 默认 3 个 ExceptionHandlerExceptionResolver ResponseStatusExceptionResolver DefaultHandlerExceptionResolver |
| getMessageCodesResolver            | 无                                       | 消息码解析器:国际化使用                                                 | 无                                                                                                        |
最佳实战
SpringBoot 已经默认配置好了Web开发场景常用功能,我们直接使用即可
三种方式
|----------|---------------------------------------------------------------------|------------------------------|-------------------------------------|
| 方式       | 用法                                                                                                || 效果                                  |
| 全自动  | 直接编写控制器逻辑                                                           |                              | 全部使用自动配置默认效果                    |
| 手自一体 | @Configuration + 配置WebMvcConfigurer+ 配置 WebMvcRegistrations | 不要标注 @EnableWebMvc | 保留自动配置效果 手动设置部分功能 定义MVC底层组件 |
| 全手动  | @Configuration + 配置WebMvcConfigurer                           | 标注 @EnableWebMvc   | 禁用自动配置效果 全手动设置              |
总结:
给容器中写一个配置类 @Configuration 实现 WebMvcConfigurer 但是不要标注 @EnableWebMvc注解,实现手自一体的效果
两种模式
前后端分离模式:@RestController 响应JSON数据
前后不分离模式:@Controller + Thymeleaf模板引擎
Web新特性
ProblemDetails
RFC 7807: RFC 7807: Problem Details for HTTP APIs
是一种错误信息返回的新格式
原理
            
            
              java
              
              
            
          
          @Configuration(
    proxyBeanMethods = false
)
// 配置过一个属性 spring.mvc.problemdetails.enabled=true
@ConditionalOnProperty(
    prefix = "spring.mvc.problemdetails",
    name = {"enabled"},
    havingValue = "true"
)
static class ProblemDetailsErrorHandlingConfiguration {
    ProblemDetailsErrorHandlingConfiguration() {
    }
    @Bean
    @ConditionalOnMissingBean({ResponseEntityExceptionHandler.class})
    @Order(0)
    ProblemDetailsExceptionHandler problemDetailsExceptionHandler() {
        return new ProblemDetailsExceptionHandler();
    }
}
        ProblemDetailsExceptionHandler 是一个 @ControllerAdvice 集中处理系统异常
处理以下异常
            
            
              java
              
              
            
          
          @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
            
            
              java
              
              
            
          
          {
    "timestamp": "2024-12-20T08:45:26.225+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:267)\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:127)\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:509)\r\n\tat org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1283)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1064)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:978)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\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:164)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\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:164)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\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:164)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)\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:344)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)\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:1190)\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:63)\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:applicatio/problem+json + 额外扩展返回

            
            
              java
              
              
            
          
          {
    "type": "about:blank",
    "title": "Method Not Allowed",
    "status": 405,
    "detail": "Method 'POST' is not supported.",
    "instance": "/list"
}
        函数式Web
SpringMVC 5.2 以后 允许我们使用函数式 的方式,定义Web的请求处理流程
函数式接口
Web请求处理的方式:
- @Controller + @RequestMapping :耦合式 (路由、业务耦合)
 - 函数式Web:分离式(路由、业务分离)
 
场景
场景:User RESTful - CRUD
- GET /user/1 获取1号用户
 - GET /users 获取所有用户
 - POST /user 请求体携带JSON,新增一个用户
 - PUT /user/1 请求体携带JSON,修改1号用户
 - DELETE /user/1删除1号用户
 
核心类
- RouterFunction
 - RequestPredicate
 - ServerRequest
 - ServerResponse
 
示例
            
            
              java
              
              
            
          
          package com.ling.web.config;
import com.ling.web.biz.UserBizHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RequestPredicates;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;
/**
 * @program: spring-boot-3
 * @author: lingsuu
 * @create: 2024-12-22 10:52
 */
/**
 * ● GET /user/1  获取1号用户
 * ● GET /users   获取所有用户
 * ● POST /user  请求体携带JSON,新增一个用户
 * ● PUT /user/1 请求体携带JSON,修改1号用户
 * ● DELETE /user/1 删除1号用户
 */
@Configuration
public class WebFunctionConfig {
    /**
     * 函数式Web的步骤
     * 1. 给容器中放一个Bean,类型是RouterFunction<ServerResponse>
     * 2. 每个业务准备一个自己的Handler
     * 核心四大随心
     * 1. RouterFunction:定义路由信息,发什么请求,谁来处理
     * 2. RequestPredicates:定义请求规则:请求谓词,请求方式(GET、POST、PUT、DELETE),请求参数
     * 3. ServerResponse:封装响应的完整数据
     * 4. ServerRequest:封装请求的完整数据
     */
    @Bean
    public RouterFunction<ServerResponse> userRoute(UserBizHandler userBizHandler/*这个会被自动注入进来*/) {
        return RouterFunctions.route() // 开始定义路由信息
                .GET("/user/{id}", RequestPredicates.accept(MediaType.ALL),userBizHandler::getUser)
                .GET("users",userBizHandler::getUsers)
                .POST("/user", RequestPredicates.accept(MediaType.APPLICATION_JSON), userBizHandler::saveUser)
                .PUT("/user/{id}", RequestPredicates.accept(MediaType.APPLICATION_JSON), userBizHandler::updateUser)
                .DELETE("/user/{id}", userBizHandler::deleteUser)
                .build();
    }
}
        
            
            
              java
              
              
            
          
          package com.ling.web.biz;
import com.ling.web.bean.Person;
import jakarta.servlet.ServletException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import org.yaml.snakeyaml.events.Event;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
 * @program: spring-boot-3
 * @author: lingsuu
 * @create: 2024-12-22 11:13
 * @description: 专门处理User有关的业务
 */
@Slf4j
@Component
public class UserBizHandler {
    /**
     * 查询指定id的用户
     * @param request
     * @return
     */
    public ServerResponse getUser(ServerRequest request) throws Exception {
        String id = request.pathVariable("id");
        log.info("查询[{}]用户信息,数据库正在检索", id);
        // 业务处理
        Person person = new Person(1, "张三", "aa@qq.com", 20, "admin");
        // 构造响应
        return ServerResponse
                .ok()
                .body(person);
    }
    /**
     * 获取所有用户
     * @param request
     * @return
     * @throws Exception
     */
    public ServerResponse getUsers(ServerRequest request) throws Exception {
        log.info("查询所有用户信息,数据库正在检索");
        // 业务处理
        List<Person> list = Arrays.asList(new Person(1, "张三", "aa@qq.com", 20, "admin")
                , new Person(2, "李四", "bb@qq.com", 21, "user")
                , new Person(3, "王五", "cc@qq.com", 22, "user"));
        // 构造响应
        return ServerResponse
                .ok()
                .body(list); // 凡是body中的对象,就是以前@ResponseBody原理,利用 HttpMessageConverter 写出为json
    }
    /**
     * 保存用户
     * @param serverRequest
     * @return
     */
    public ServerResponse saveUser(ServerRequest serverRequest) throws ServletException, IOException {
        // 提取请求体
        Person body = serverRequest.body(Person.class);
        log.info("保存用户信息完成:{}", body);
        return ServerResponse.ok().build();
    }
    /**
     * 更新用户
     * @param serverRequest
     * @return
     */
    public ServerResponse updateUser(ServerRequest serverRequest) throws ServletException, IOException {
        Person body = serverRequest.body(Person.class);
        log.info("更新用户信息完成:{}", body);
        return ServerResponse.ok().build();
    }
    /**
     * 删除用户
     * @param serverRequest
     * @return
     */
    public ServerResponse deleteUser(ServerRequest serverRequest) {
        String id = serverRequest.pathVariable("id");
        log.info("删除[{}]用户信息完成", id);
        return ServerResponse.ok().build();
    }
}