SpringBoot3——Web开发

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页面,项目启动默认访问

为什么容器中放一个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
  • 绑定了配置文件的一堆配置项

默认效果

默认配置:

  1. 包含了ContentNegotiatingViewResolverBeanNameViewResolver 组件,方便视图解析
  2. 默认的静态资源处理机制 : 静态资源放在 static文件夹下即可直接访问
  3. 自动注册了 Converter,GenericConverter,Formatter 组件,适配常见数据类型转换和格式化需求
  4. 支持 HttpMessageConverters ,可以方便返回 json数据类型
  5. 注册 MessageCodesResolver ,方便国际化及错误消息处理
  6. 支持 静态 index.html
  7. 自动使用 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:静态资源访问前缀路径

spring.web

  • 静态资源目录
  • 静态资源缓存策略
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 风格的路径模式语法中的特殊字符需要转义,如:

  • 要匹配文件路径中的星号,则需要转义为***\\****。
  • 要匹配文件路径中的问号,则需要转义为***\\?***。

模式切换

AntPathMatherPathPatternParser

  • 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请求头
    • Acceptapplication/jsontest/xmltext/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

@ResponseBodyHttpMessageConverter处理

标注了***@ResponseBody*** 的返回值将会由支持它的 HttpMessageConverter 写给浏览器

  • 如果controller方法的返回值标注了 @ResponseBody 注解
    • 请求进来先来到 DispatcherServletdoDispatcher() 进行处理
    • 找到一个 HandlerAdapter适配器,利用适配器执行目标方法
    • 使用@RequestMapping注解,HandlerAdapter 找到的 适配器就是 RequestMappingHandlerAdapter ,接下来就是 RequestMappingHandlerAdapter 调用 invokeHandlerMethod() 来执行目标方法
    • 目标方法执行之前,准备好两个东西
      • HandlerMethodArgumentResolver:参数解析器,确定目标方法每个参数值
      • HandlerMethodReturnValueHandler:返回值处理器,确定目标方法的返回值该怎么处理
    • RequestMappingHandlerAdapter 里面的 invokeAndHandler() 真正执行目标方法
    • 目标方法执行完成,会返回返回值对象
    • 这时候就需要找到一个合适的返回值处理器 HandlerMethodReturnValueHandler
    • 最终找到RequestResponseBodyMethodProcessor 能处理标注了 @ResponseBody注解的方法
    • RequestResponseBodyMethodProccess 调用 writeWithMessgaeConverters,利用 MessageConverter 把返回值写出去

上面解释:@ResponseBodyHttpMessageConverter 处理

  • 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:ifth: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,...
  • 布尔:truefalse
  • null:null
  • 变量名:onesometextmain...
文本操作
  • 拼串:+
  • 文本替换:| The name is ${name} |
布尔操作
  • 二进制运算:andor
  • 取反:not
比较运算
  • 比较:><<=>=gtltgele
  • 等值运算:==!=eqne
条件运算
  • 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:insertth:replace
html 复制代码
// html文件名也叫footer,片段名叫copy
<footer th:fragment="copy">&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>&copy; 2011 The Good Thymes Virtual Grocery</footer>
    </div>

    <footer>&copy; 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
  • 如果模板引擎路径templates下有 error.html页面,就直接渲染

自定义错误响应

自定义Json响应

使用@ControllerAdvice + @ExceptionHandler 进行统一异常处理

自定义页面响应

根据boot的错误页面规则,自定义页面模板

最佳实战

  • 前后分离
    • 后台发生的所有错误,使用**@ControllerAdvice** + @ExceptionHandler进行统一异常处理
  • 服务端渲染
    • 不可预知的一些,HTTP码表示的服务器或客户端错误
      • classpath:/templates/error/ 下面,放常用精确的错误码页面。500.html404.html
      • classpath:/templates/error/ 下面,放通用模糊匹配的错误码页面。 5xx.html4xx.html
    • 发生业务错误
      • 核心业务 的每一种错误,都应该由代码控制,跳转到自己定制的错误页
      • 通用业务classpath:/templates/error.html 页面,显示错误信息

页面,JSON,可用的Model数据如下

嵌入式容器

Servlet容器 :管理、运行Servlet组件 (Servlet、Filter、Listener)的环境,一般指服务器

自动配置原理

  • SpringBoot 默认嵌入Tomcat作为Servlet容器。
  • 自动配置类是ServletWebServerFactoryAutoConfigurationEmbeddedWebServerFactoryCustomizerAutoConfiguration
  • 自动配置类开始分析功能。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 导入了 嵌入式的三大服务器 TomcatJettyUndertow
    • ServletWebServerFactoryConfiguration 中,导入 TomcatJettyUndertow都有条件注解。系统中有这个类才行(也就是导了包)
    • 默认 Tomcat 配置生效。给容器中放 TomcatServletWebServerFactory
    • 都给容器中 ServletWebServerFactory 放了一个 web服务器工厂(造web服务器的)
    • web服务器工厂 都有一个功能getWebServer获取web服务器
    • getWebServer 方法中会创建服务器,比如:TomcatServletWebServerFactorygetWebServer创建了 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}) ,容器中没有WebMvcConfigurationSupportWebMvcAutoConfiguration才生效
  • @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();
    }
}
相关推荐
计算机毕设VX:Fegn08952 分钟前
计算机毕业设计|基于springboot + vue图书商城系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
R.lin14 分钟前
Java 8日期时间API完全指南
java·开发语言·python
毕设源码-赖学姐20 分钟前
【开题答辩全过程】以 高校教学质量监控平台为例,包含答辩的问题和答案
java·eclipse
高山上有一只小老虎27 分钟前
翻之矩阵中的行
java·算法
火钳游侠41 分钟前
java单行注释,多行注释,文档注释
java·开发语言
曼巴UE544 分钟前
UE FString, FName ,FText 三者转换,再次学习,官方文档理解
服务器·前端·javascript
wanhengidc1 小时前
云手机的存储空间可以灵活扩展吗?
运维·服务器·科技·智能手机·云计算
code bean1 小时前
【CMake】为什么需要清理 CMake 缓存文件?深入理解 CMake 生成器切换机制
java·spring·缓存
selt7911 小时前
Redisson之RedissonLock源码完全解析
android·java·javascript
Danileaf_Guo1 小时前
256台H100服务器的RoCEv2无损与全互联算力网络建设方案
运维·服务器·网络