SpringBoot-Web开发

SpringBoot-Web开发

SpringBoot非常适合Web应用程序开发。你可以通过使用嵌入式Tomcat、Jetty、Undertow或Netty来创建一个自包含的HTTP服务器。大多数Web应用程序都会使用 spring-boot-starter-web模块来快速搭建和运行。你也可以使用spring-boot-starter-webflux模块来构建响应式Web应用程序。

注:本篇文章主要讲述的是大多数Web应用程序使用的spring-boot-starter-web模块来快速搭建和运行

Web场景

如果想基于servlet的web应用程序,可以利用Spring Boot为Spring MVC或Jersey提供的自动配置

整合web场景

引入web的场景启动器

    <!-- 所有springboot项目都必须继承自 spring-boot-starter-parent -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.9</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

自动配置

在引入SpringBoot相关依赖后,都会在启动类中添加@SpringBootApplication注解。

@SpringBootApplication注解中包含了@EnableAutoConfiguration注解,其作用为开启自动配置。

@EnableAutoConfiguration注解中通过@Import注解导入了一个选择器:AutoConfigurationImportSelector。

当Spring Boot启动时,AutoConfigurationImportSelector会去读取classpath下所有的jar包中的特定文件,其路径为INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

这个特定文件列出了所有可能需要自动配置的类的全限定名,这些配置类添加了条件注解,即只有classpath中出现指定的类或者配置参数为true等条件才会生效。

在引入的web的场景启动器(spring-boot-starter-web)后前缀为org.springframework.boot.autoconfigure.web.servlet的配置类便能够生效。

1

这些配置类生效后,绑定了一堆配置项:

  • SpringMVC的所有配置:spring.mvc
  • Web场景通用配置:spring.web
  • 文件上传配置:spring.servlet.multipart
  • 服务器的配置:server

2

3

默认效果

SpringBoot为Spring MVC提供了自动配置适应于大多数应用程序。它替代了@EnableWebMvc的需求,且两者不能同时使用。除了Spring MVC的默认配置外,自动配置还提供了以下特性

  • 包含了ContentNegotiatingViewResolver和BeanNameViewResolver组件,方便视图解析
  • 默认的静态资源处理机制:静态资源放在static文件夹下即可直接访问
  • 自动注册了Converter,GenericConverter,Formatter组件,适配常见数据类型转换和格式化需求
  • 支持HttpMessageConverters,可以方便返回json等数据类型
  • 注册MessageCodesResolver,方便国际化及错误消息处理
  • 支持静态index.html
  • 自动使用ConfigurableWebBindingInitializer,实现消息处理、数据绑定、类型转化、数据校验等功能
如果你想保留SpringBoot的MVC自动配置,同时添加更多MVC自定义配置(interceptors-拦截器,formatters-格式化器,view controllers-视图控制器等),可以添加一个实现WebMvcConfigurer接口的配置类(带有@Configuration注解),但不要标注@EnableWebMvc。

如果你想提供RequestMappingHandlerMapping、RequestMappingHandlerAdapter或ExceptionHandlerExceptionResolver的自定义实例,同时仍然保留SpringBoot的MVC自动配置,可以声明一个WebMvcRegistrations类型的Bean,用它来提供这些组件的自定义实例。这些自定义实例会接收SpringMVC进一步初始化和配置。若要参与后续处理,应使用WebMvcConfigurer。

如果你不想使用自动配置,想要完全掌控Spring MVC,可以添加一个标注了@EnableWebMvc的配置类,实现WebMvcConfigurer接口。

Web场景中三种典型的配置方式

在SpringBoot的Web开发场景中,根据对MVC框架控制力度的不同,存在以下三种典型配置方式

方式一:扩展自动配置(推荐)-WebMvcConfigurer

保留SpringBoot全部自动配置,仅做增量式扩展

@Configuration
public class MyWebConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/api/**");
    }
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:3000");
    }
    
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new DateFormatter("yyyy-MM-dd"));
    }
}

适用场景:添加拦截器、跨域配置、视图控制器、格式化器等。是日常开发90%场景的最佳选择

方式二:替换核心组件-WebMvcRegistrations

保留SpringBoot自动配置,但替换MVC三大核心组件的实例

@Configuration
public class MyWebRegistrationsConfig implements WebMvcRegistrations {
    
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        // 自定义 HandlerMapping,例如添加全局路径前缀
        RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
        mapping.setPathPrefixes(Collections.singletonMap("/api", HandlerTypePredicate.forAnnotation(RestController.class)));
        return mapping;
    }
    
    @Override
    public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
        // 自定义 HandlerAdapter
        return new MyCustomHandlerAdapter();
    }
    
    @Override
    public ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
        // 自定义异常解析器
        return new MyCustomExceptionResolver();
    }
}

适用场景:需要深度定制请求映射策略、方法执行适配器或全局异常处理机制,同时享受SpringBoot的静态资源、消息转换器等自动配置

方式三:完全手动控制-@EnableWebMvc

彻底禁用SpringBoot的MVC自动配置,完全由开发者接管

@Configuration
@EnableWebMvc
public class ManualWebConfig implements WebMvcConfigurer {
    
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("/WEB-INF/views/", ".jsp");
    }
    
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
    
    // 必须手动配置所有内容:静态资源、消息转换器、异常处理等
}

适用场景:将遗留的纯SpringMVC项目迁移到SpringBoot,对MVC生命周期有极特殊的定制需求,完全脱离Boot约定,按传统Spring方式构建

WebMvcAutoConfiguration原理

WebMvcAutoConfiguration是Spring Boot中关于Spring MVC整合的核心自动配置类。它的主要目的是在用户没有进行任何自定义配置的情况下,提供一套开箱即用的Spring MVC默认配置;同时又保留了用户完全覆盖默认配置或仅扩展部分配置的灵活性

生效条件

4

原理解析:
@ConditionalOnWebApplication(type = Type.SERVLET) :
必须是基于Servlet的Web应用才生效(响应式WebFlux不生效)

@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) :
类路径下必须存在Servlet、DispatcherServlet和WebMvcConfigurer,即引入了spring-webmvc依赖即可满足

@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) :
最关键的注解,容器中不能存在WebMvcConfigurationSupport类型的Bean。这意味着如果开发者手动加了@EnableWebMvc或者自己定义了WebMvcConfigurationSupport的子类,这个自动配置类将全部失效

@AutoConfiguration(after = {DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,ValidationAutoConfiguration.class }) : 
它必须在Servlet容器(如Tomcat)自动配置完成之后才执行,因为MVC需要Servlet容器作为基础

两个Filter

  • hiddenHttpMethodFilter:页面表单提交Rest请求(GET、POST、PUT、DELETE)
  • formContentFilter:表单内容Filter,GET(数据放URL后面)、POST(数据放请求体)请求可以携带数据,PUT、DELETE的请求体数据会被忽略

5

核心内部结构

WebMvcAutoConfigurationAdapter(适配器,实现WebMvcConfigurer)

最核心的内部类,它实现了WebMvcConfigurer接口,用于向Spring MVC提供默认的回调配置

6

WebMvcConfigurer接口

提供了配置SpringMVC底层的所有组件入口

7

属性绑定
@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })

WebMvcProperties.class :spring.mvc配置文件
WebProperties.class :spring.web配置文件    

EnableWebMvcConfiguration(桥架核心底层配置)

这个内部类继承自DelegatingWebMvcConfiguration,而DeglegatingWebMvcConfiguration又继承自WebMvcConfigurationSupport

8

//SpringBoot自己会给容器放入WebMvcConfigurationSupport组件
//我们如果自己放了WebMvcConfigurationSupport组件,SpringBoot的WebMvcAutoConfiguration都会失效
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {...}
原理解析:
WebMvcConfigurationSupport是Spring MVC底层最重量级的类,它负责向Spring容器注册各种核心Bean(如:RequestMappingHandlerMapper、RequestMappingHandlerAdapter、ExceptionHanlderExceptionResolver等)。

DelegatingWebMvcConfiguration的作用是拦截WebMvcConfigurationSupport中的方法,将这些方法的执行委托给容器中所有的WebMvcConfigurer实现(包括WebMvcAutoConfigurationAdpter和用户自定义的WebMvcConfigurer)

这样设计的好处是:Spring Boot既有底层的核心Bean,又把配置项通过WebMvcConfigurer接口暴露出来,方便聚合。

其他辅助内部类

ResourceChainCustomizerConfiguration :配置静态资源的缓存策略链。

WelcomePageHandlerMapping :专门处理根路径/映射到index.html的映射器

静态资源

静态资源规则源码

SpringBoot通过WebMvcAutoConfigurationAdapter类自动配置了默认的静态资源处理

9

  1. 规则一:访问/webjars/**路径就去classpath:/META-INF/resources/webjars/下找资源
  2. 规则二:访问/**路径就去静态资源默认的四个位置找资
    1. classpath:/META-INF/resources/
    2. classpath:/resources/
    3. classpath:/static/
    4. classpath:/public/
  3. 规则三:静态资源默认有缓存规则的设置
    1. 所有缓存的设置,直接通过配置文件:spring.web
    2. period:缓存间隔(多久不用找服务器获取新的资源)。默认没有(即0s),以s为单位
    3. cacheControl:HTTP缓存控制,默认无
    4. useLastModified:是否使用最后一次修改。配合HTTP Cache规则,默认false

欢迎页规则源码

SpringBoot通过EnableWebMvcConfiguration类配置了处理应用的欢迎页(即根路径/的访问请求)

10

欢迎页的查询逻辑:

  1. 查找静态资源目录下的index.html,SpringBoot会遍历默认的静态资源文件夹,寻找是否存在index.html文件,如果找到则返回该Resource
  2. 如果静态资源目录下没找到index.html,SpringBoot会通过TemplateAvilabilityProviders检查类路径下是否存在模版引擎(如Thymeleaf、Freemarker),并且是否存在名为index的模版文件(如templates/index.html)

自定义Favicon(图标)

与其他静态资源一样,SpringBoot会在配置的静态内容位置检查favicon.ico。如果存在这样的文件,它将自动作为应用程序的网站图标

静态资源规则测试示例

刚才在分析静态资源规则源码时,提到了三个规则,以及欢迎页与自定义图标,,这里通过代码来测试一下

引入webjars的依赖

    <!-- 所有springboot项目都必须继承自 spring-boot-starter-parent -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.9</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--lombok简化JavaBean开发,自动生成构造器、getter/seter、自动生成Builder模式等-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>org.webjars.npm</groupId>
            <artifactId>vue-echarts</artifactId>
            <version>7.0.3</version>
        </dependency>
    </dependencies>

    <!-- SpringBoot应用打包插件 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

静态资源路径下添加图片、index.html、favicon.ico

11

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>welcome page</title>
</head>
<body>
<h1>this is index page</h1>
</body>
</html>

配置文件中添加缓存规则配置

server.port=18190
# 一、spring.web:
# 1.配置国际化的区域信息
# 2.静态资源策略(开启、处理链、缓存)
# 开启静态资源映射规则 默认为true-开启
spring.web.resources.add-mappings=true
# 设置缓存
# spring.web.resources.cache.period=3600
# 缓存详细合并项控制,会覆盖period配置
# 浏览器第一次请求服务器,服务器会告诉浏览器此资源缓存7200秒,7200秒以内的所有此资源访问不用发给服务器请求,超过7200秒后发请求给服务器
spring.web.resources.cache.cachecontrol.max-age=7200
# 使用资源 通过last-modified时间,对比服务器和浏览器的资源是否相同没有变化,相同返回304
spring.web.resources.cache.use-last-modified=true

启动web应用程序访问相应的请求

首页:
http://127.0.0.1:18190/

访问静态资源:
http://127.0.0.1:18190/webjars/vue-demi/0.13.11/bin/vue-demi-fix.js
http://127.0.0.1:18190/1.jpeg
http://127.0.0.1:18190/2.jpeg
http://127.0.0.1:18190/3.jpeg
http://127.0.0.1:18190/4.jpeg

通过查看响应头可以看到最大超时时间为7200秒

12

自定义静态资源规则

有两种方式自定义静态资源规则,1.配置参数,2.代码

通过配置参数修改静态资源规则

spring.mvc:静态资源访问前缀路径的修改

spring.web:静态资源目录、静态资源缓存策略

server.port=18190
# 一、spring.web:
# 1.配置国际化的区域信息
# 2.静态资源策略(开启、处理链、缓存)
# 开启静态资源映射规则 默认为true-开启
spring.web.resources.add-mappings=true
# 设置缓存
# spring.web.resources.cache.period=3600
# 缓存详细合并项控制,会覆盖period配置
# 浏览器第一次请求服务器,服务器会告诉浏览器此资源缓存7200秒,7200秒以内的所有此资源访问不用发给服务器请求,超过7200秒后发请求给服务器
spring.web.resources.cache.cachecontrol.max-age=7200
# 使用资源 通过last-modified时间,对比服务器和浏览器的资源是否相同没有变化,相同返回304
spring.web.resources.cache.use-last-modified=true
# 自定义静态资源文件夹位置,还是用默认的值
spring.web.resources.static-locations=classpath:/META-INF/resources/, classpath:/resources/, classpath:/static/, classpath:/public/

# 二、sping.mvc
# 1.自定义webjars路径前缀
spring.mvc.webjars-path-pattern=/wj/**
# 2.自定义静态资源访问路径前缀
#spring.mvc.static-path-pattern=/static/**

通过代码的形式修改静态资源规则

代码的形式有两种方式,一种是通过实现WebMvcConfigurer接口的addResourceHandlers方法,另一种是在容器中放入一个WebMvcConfigurer组件

方式一:配置类实现WebMvcConfigurer接口的addResourceHandlers方法(推荐)
//@EnableWebMvc //会禁用boot的默认配置
//通过实现WebMvcConfigurer的方式
@Configuration
public class ResourceHandlerMethodOneConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //保留以前的规则,即使不加下面这一行也会保留以前的规则
//        WebMvcConfigurer.super.addResourceHandlers(registry);
        //自己新增规则
        registry.addResourceHandler("/static-one/**")
                .addResourceLocations("classpath:/a/")
                .setCacheControl(CacheControl.maxAge(1110, TimeUnit.SECONDS));
    }
}
方式二:配置类在容器中放入一个WebMvcConfigurer组件
//通过注册容器的方式
@Configuration
public class ResourceHandlerMethodTwoConfig {

    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addResourceHandlers(ResourceHandlerRegistry registry) {
                //自己新增规则
                registry.addResourceHandler("/static-two/**")
                        .addResourceLocations("classpath:/a/")
                        .setCacheControl(CacheControl.maxAge(1120, TimeUnit.SECONDS));

            }
        };
    }
}

路径匹配

Spring5.3之后引入了新的请求路径匹配策略,以前只支持AntPathMatcher策略,现在提供了PathPatternParser策略,并在springBoot3.x中成为默认

路径匹配的核心语法(Ant风格)

无论是早期的AntPathMatcher还是现在的PathPatternParser,Spring默认都支持Ant风格的通配符语法

  • "*":表示任意数量的字符
  • "?":表示任意一个字符
  • "**":表示任意数量的目录
  • "{name}":支持路径变量,匹配一段路径并捕获,其名为name的变量
  • "{name:正则表达式}":支持路径匹配,带正则约束的路径变量,如/user/{id:\d+}只匹配数字
  • "[]":表示字符集合,例如[a-z]表示小写字母

新老路径匹配模式对比

路径匹配模式 实现机制 优点 缺点
AntPathMatcher 基于纯字符串的拼接和递归比对,将URL和Pattern都当作字符串,逐字符/逐通配符进行匹配 功能灵活,不仅用于Web路径,还可以用于Classpath下的资源查找 性能较差,不够严谨
PathPatternParser 将URL和Pattern预先解析为树状/链表结构,通过遍历数据结构进行匹配 性能极高,规则更严谨 只能用于Web层的URL匹配,不支持Classpath资源匹配
注:这里的规则严不严谨是指是否允许 /**/ 出现在模式中间,如果出现在中间在逻辑上容易产生歧义,AntPathMatcher允许/**/出现在模式中间,而PathPatternParser则只允许**出现在末尾,如/api/**

新老路径匹配模式切换示例

springBoot3.x版本默认使用新策略PathPatternParser,但可以通过修改spring.mvc中的配置来切换路径匹配策略

配置参数

# 改变路径匹配策略:
# ant_path_matcher 老版策略
# path_pattern_parser 新版策略即默认配置,spring5.3之后加入了新的请求路径匹配的实现策略,效率更高
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

控制层代码

/**
 * 验证路径匹配规则的控制层接口
 */
@Slf4j
@RestController
public class PathMatchController {
    // {}表示这里是一个路径变量,p1是表示变量名,表示将路径变量的值接收,可以通过@PathVariable注解接收该值

    /**
     * path_pattern_parser策略就可以解析的请求
     *
     * @param request 请求体
     * @param p1      路径变量
     * @return 请求URL
     */
    @GetMapping("/a*/b?/{p1:[a-f]+}/**")
    public String testRequestOne(HttpServletRequest request, @PathVariable("p1") String p1) {
        log.info("路径变量p1:{}", p1);
        return request.getRequestURI();
    }

    // /**/表示任意目录深度
    // ant_path_matcher策略才可以解析的请求,即在中间位置加/**/

    /**
     * ant_path_matcher策略才可以解析的请求
     *
     * @param request 请求体
     * @param p1      路径变量
     * @return 请求URL
     */
    @GetMapping("/a*/b?/**/{p1:[a-f]+}/**")
    public String testRequestTwo(HttpServletRequest request, @PathVariable("p1") String p1) {
        log.info("路径变量p1:{}", p1);
        return request.getRequestURI();
    }
}

内容协商

内容协商是SpringBoot(及SpringMVC)中一个非常核心且优雅的机制。简单来说,它就是客户端和服务端就"返回数据的格式"达成一致的过程。主要围绕HTTP的Accept请求头和xx响应体的格式(如JSON、XML、HTML等)展开

13

内容协商的两种方向

  1. 基于请求头(主流推荐):默认开启,客户端通过设置Accept请求头告诉服务器想要的格式,如Accept: application/json、text/xml、text/yaml。服务器根据客户端请求头期望的数据类型进行动态返回
  2. 基于请求参数(辅助):需要开启配置,发送请求如/person?format=json,根据参数协商,优先返回json类型数据

内容协商示例

引入支持对象写出xml内容的依赖

        <!--引入支持对象写出xml内容依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
        </dependency>

POJO类

@JacksonXmlRootElement //可以写出为xml文档
@Data
public class Person {
    private Long id;
    private String name;
    private int age;
}

控制层接口

@Slf4j
@RestController
public class ContentNegotiationController {
    /**
     * 默认支持把对象写为json,因为默认web场景导入了jackson处理json的包:jackson-core
     * jackson也支持把数据写为xml,但需要导入xml相关依赖
     * @return person对象
     */
    @GetMapping("/person")
    public Person getPerson() {
        Person person = new Person();
        person.setId(1L);
        person.setName("小明");
        person.setAge(18);
        return person;
    }
}

配置开启基于请求参数内容协商的功能的参数

# 开启基于请求参数的内容协商功能,默认参数名为:format
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商使用的参数名,默认为format
spring.mvc.contentnegotiation.parameter-name=type

测试发起请求

基于请求头内容协商的请求测试:
发起GET类型的HTTP请求,请求头中Accept参数的值分别为application/json、text/xml

测试结果为返回值分别为json格式、xml格式的对象

14

基于请求参数内容协商的请求测试:
发起GET类型的HTTP请求,请求URL中添加type参数,参数值分别为json、xml

测试结果为返回值分别为json格式、xml格式的对象

15

自定义格式内容返回

自定义格式内容返回的方式和之前讲到的自定义静态资源规则类似,由于WebMvcConfigurer接口提供了用于消息转换的configureMessageConverters方法,因此可以通过实现WebMvcConfigurer接口的onfigureMessageConverters方法来自定义格式内容。下面以新增yaml格式返回作为示例

引入支持yaml格式转换的依赖

        <!--引入支持对象写出yaml内容依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-yaml</artifactId>
        </dependency>

测试类:通过YAML生成器将对象转换为yaml格式

/**
 * 测试对象输出YAML格式
 */
public class YamlPrintTest {
    public static void main(String[] args) throws JsonProcessingException {
        Person person = new Person();
        person.setId(1L);
        person.setName("Alex");
        person.setAge(18);
//        ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
        // YAML写法禁用开头---的特性
        YAMLFactory yamlFactory = new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
        ObjectMapper objectMapper = new ObjectMapper(yamlFactory);
        String personStr = objectMapper.writeValueAsString(person);
        System.out.println(personStr);
    }
}

新增一种媒体类型

# 增加一种内容类型
spring.mvc.contentnegotiation.media-types.yaml=text/yaml

编写HttpMessageConverter用于返回值的格式转换

/**
 * YAML消息转换器
 */
public class YamlHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

    private ObjectMapper objectMapper = null;

    public YamlHttpMessageConverter() {
        //声明这个MessageConverter支持哪种媒体类型
        super(new MediaType("text", "yaml", Charset.forName("UTF-8")));
        YAMLFactory yamlFactory = new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
        this.objectMapper = new ObjectMapper(yamlFactory);
    }


    @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 returnValue, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        //try-with写法,自动关流
        try(OutputStream os = outputMessage.getBody()) {
            this.objectMapper.writeValue(os, returnValue);
        }
    }
}

实现WebMvcConfigurer接口的onfigureMessageConverters方法

//通过实现WebMvcConfigurer的方式
@Configuration
public class ResourceHandlerMethodOneConfig implements WebMvcConfigurer {
    //配置一个能把对象转换为yaml的消息转换器
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new YamlHttpMessageConverter());
    }
}

测试结果

请求头Accept参数值为text/yaml,发送GET请求后返回值为yaml的格式

16

内容协商原理

Spring MVC处理内容协商的核心组件是ContentNegotiationManager,它的执行流程如下:

  1. 客户端发起请求:携带Accept: text/yaml
  2. DispatcherServlet拦截请求,交由ContentNegotiationManager解析客户端想要的媒体类型
  3. 遍历HttpMessageConverter:SpringBoot会遍历所有注册的消息转换器,寻找能把Java对象转换为yaml且支持text/yaml的转换器
  4. 匹配并响应:找到合适的转换器后,将Java对象序列化为XML返回给客户端
  5. 匹配失败(406):如果服务器没有能处理text/yaml的转换器,则返回HTTP状态码 406 Not Acceptable

默认提供的HttpMessageConverter

SpringBoot通过HttpMessageConvertersAutoConfiguration自动配置了一组HttpMessageConverter

  1. ByteArrayHttpMessageConverter:支持字节数据读写
  2. StringHttpMessageConverter:支持字符串读写
  3. ResourceHttpMessageConverter:支持资源读写
  4. ResourceRegionHttpMessageConverter:支持分区资源写出
  5. AllEncompassingFromHttpMessageConverter:支持表单xml/json读写
  6. MappingJackson2HttpMessageConverter:支持请求响应体Json读写
注:系统默认提供的MessageConverter功能有限,仅用于json或者普通返回数据。额外增加新的内容协商功能,必须增加新的HttpMessageConverter

模版引擎

模版引擎是一种用于将动态数据与静态模版结合,最终生成文档(通常是HTML,也可以是XML、邮件内容等)的工具。

它的核心思想是:模版+数据=输出。通过将表现层(视图)和业务逻辑(控制器)分离,使得开发人员可以专注于各自的工作,提高代码的可维护性

Java-Web开发的两种模式

  1. 前后端分离模式:@RestController响应JSON数据
  2. 前后端不分离模式:@Controller + Thymeleaf模版引擎

17

SpringBoot包含以下模版引擎的自动配置

  • FreeMarker
  • Groovy
  • Thymeleaf
  • Mustache

Thymeleaf整合

现代Java Web的绝对主流,其特点是自然模版,模版在脱离服务器运行时,仍然可以作为一个纯正的HTML文件在浏览器中打开和预览。是SpringBoot官方推荐的默认模版引擎

引入Thymeleaf场景启动器依赖

        <!--引入Thymeleaf依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

引入了Thymeleaf场景启动器依赖后的自动配置原理

  1. 开启了org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration的该类的自动配置
  2. 属性绑定在ThymeleafProperties中,对应配置文件spring.thymeleaf内容
  3. 所有的模版页面默认在classpath:/templates/文件夹下,找后缀名为.html的页面

Thymeleaf常用配置

# 配置thymeleaf场景配置
# 默认查找资源的前缀
spring.thymeleaf.prefix=classpath:/templates/
# 默认查找的资源的后置
spring.thymeleaf.suffix=.html
# 是否开启缓存,开发期间关闭,上线后开启
spring.thymeleaf.cache=false

Thymeleaf的基础语法

前提-声明Thymeleaf命名空间

将html页面中常规的普通静态HTML说明修改,默认格式如下:
<html lang="en">

修改后:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
声明了Thymeleaf命名空间,告诉引擎这个HTML是Thymeleaf模版

好处:
1.避免解析失效,有些环境,省略xmlns:th 会导致Thymeleaf不识别th:*,模版变为纯静态HTML。
2.IDE优化:IDEA的Thymeleaf插件靠xmlns:th 来识别这是Thymeleaf模版,从而提供:th:*补全、表达式${}的高亮、错误提示等。
3.可读性:看到xmlns:th就知道这是Thymeleaf模版,而不是普通HTML文件。

核心用法

th:xxx

动态渲染指定的html标签属性值,或者th指令(遍历、判断等)

  • th:text :标签体内文本值渲染,th:utext:不会转义,显示html原本的样子
  • th:属性 :标签指定属性渲染
  • th:attr :标签任意属性渲染
  • th:if :th指令
表达式

用来动态取值

  • ${} :变量取值,使用model共享给页面的值直接用${}
  • @{} :URL路径
  • #{} :国际化消息
  • ~{} :片段引用
  • *{} :变量选择,需要配合th:object绑定对象
系统工具或内置对象
  • param : 请求参数对象
  • session :session对象
  • #message :国际化消息
  • #dates : 日期工具
  • #numbers :数字操作工具
  • #strings :字符串操作
  • #objects :对象操作

语法操作

不同类型
  • 文本 : 'a'
  • 数字 : 0,3.0,1.25
  • 布尔 : true,false
  • null : null
  • 变量名 : var1
文本操作
  • 字符串拼接 : +
  • 文本替换 : | he name is ${name}|
比较运算
  • 比较 : gt-大于,lt-小于,ge-大于等于,le-小于等于
  • 等值运算 :eq-等于,ne-不等于
条件运算
  • if ? then
  • if ? then else
  • value ? defaultValue

语法操作示例

控制层接口
/**
 * 测试服务端渲染页面的控制层接口
 */
@Controller//适配服务端渲染,前后端不分离模式
public class ThymeleafStartController {

    /**
     * 利用模版引擎跳转到指定页面
     * @return
     */
    @GetMapping("/grammer")
    public String grammer(@RequestParam("name") String name, Model model) {
        model.addAttribute("name", name);
        String text = "<span style='color:red'>" + name + "</span>";
        model.addAttribute("text", text);
        model.addAttribute("imageUrl","/4.jpeg");
        model.addAttribute("style","with: 400px");
        model.addAttribute("show",true);
        return "grammer";
    }
}
html文件
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>grammer</title>
</head>
<body>
<h1>hello:<span th:text="name"></span></h1>
<hr/>
th:text: 替换标签体的内容:会转义
th:utext: 替换标签体的内容:不会转义html标签,显示html该有的样式
<h1 th:text="${text}"></h1>
<h1 th:utext="${text}"></h1>
转大写
<h1 th:text="${#strings.toUpperCase(name)}"></h1>
字符串拼接
<h1 th:text="${'前缀'+ name + '后缀'}"></h1>
<h1 th:text="|前缀${name}后缀|"></h1>
<hr/>
th:任意html属性
<img th:src="${imageUrl}" style="width:300px"/>
<br/>
th:attr: 任意属性指定
<img th:attr="src=${imageUrl},style=${style}">
<br/>
th:其他指令
<img th:src="@{${imageUrl}}" th:style="${style}" th:if="${show}">
<br/>
 @{} 专门用来取各种路径,添加项目路径前缀
</body>
</html>

遍历与判断

语法
遍历语法: th:each="元素名,迭代状态:${集合}"

其中迭代状态也可以不写,迭代状态有以下属性:
index :当前遍历元素的索引,从0开始
count :当前遍历元素的索引,从1开始
size :需要遍历元素的总数量
current :当前正在遍历的元素对象
even/odd :是否偶数/奇数行
first : 是否第一个元素
last :是否最后一个元素

判断分为两种
th:if 和th:switch
示例
控制层接口
/**
 * 测试服务端渲染页面的控制层接口
 */
@Controller//适配服务端渲染,前后端不分离模式
public class ThymeleafStartController {
    @GetMapping("/list")
    public String list(Model model) {
        List<Person> list = Arrays.asList(
                new Person(1L,"小李",17),
                new Person(2L,"小红",20),
                new Person(3L,"小王",21),
                new Person(4L,"小明",16),
                new Person(5L,"小刘",22)
        );
        model.addAttribute("persons", list);
        return "list";
    }
}
html文件-list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>list</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head>
<body>
<table class="table">
    <thead>
    <tr>
        <th scope="col">#</th>
        <th scope="col">id</th>
        <th scope="col">姓名</th>
        <th scope="col">按钮</th>
        <th scope="col">年龄</th>
        <th scope="col">迭代状态</th>
    </tr>
    </thead>
    <tbody>
    <tr th:each="person,status: ${persons}">
        <th scope="row" th:text="${person.id}"></th>
        <td th:if="${person.id}==1" th:text="'第一'"></td>
        <td th:if="${person.id}!=1" th:text="${person.id}"></td>
        <td th:text="${person.name}"></td>
        <td th:switch="${person.name}">
            <button th:case="小李" type="button" class="btn btn-success">小李</button>
            <button th:case="小红" type="button" class="btn btn-danger">小红</button>
        </td>
        <td th:text="| ${person.age} - ${person.age >=18? '成年':'未成年'}|"></td>
        <td>
            index(遍历元素索引,从0开始): [[${status.index}]] <br/>
            count(遍历元素索引,从1开始): [[${status.count}]] <br/>
            size(需要遍历的总数量): [[${status.size}]] <br/>
            current(当前遍历的元素对象): [[${status.current}]] <br/>
            even/odd(是否为偶数行): [[${status.even}]] <br/>
            first(是否为第一个): [[${status.first}]] <br/>
            last(是否为最后一个): [[${status.last}]] <br/>
        </td>
    </tr>
    </tbody>
</table>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
        crossorigin="anonymous"></script>
</body>
</html>

属性优先级

优先级(从高到低) 特性 属性
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:atrappend
6 指定属性修改 th:value,th:href,th:src
7 文本值 th:text,th:utext
8 片段指定 th:fragment
9 片段移除 th:remove

行内写法

[[...]]或[(...)]

<p> this is [[${name}]] </p>

变量选择

<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>

等同于

<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
示例
控制层接口
@Controller
public class IndexController {
    @GetMapping("/")
    public String index() {
        return "index";
    }
}
模版文件-common.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>通用</title>
</head>
<body>
<!--抽取的片段,名字叫myheader-->
<header th:fragment="myheader" class="p-3 text-bg-dark">
    <div class="container">
        <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start"><a href="/"
                                                                                                            class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
            <svg class="bi me-2" width="40" height="32" role="img" aria-label="Bootstrap">
                <use xlink:href="#bootstrap"></use>
            </svg>
        </a>
            <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
                <li><a href="#" class="nav-link px-2 text-secondary">Home</a></li>
                <li><a href="#" class="nav-link px-2 text-white">Features</a></li>
                <li><a href="#" class="nav-link px-2 text-white">Pricing</a></li>
                <li><a href="#" class="nav-link px-2 text-white">FAQs</a></li>
                <li><a href="#" class="nav-link px-2 text-white">About</a></li>
            </ul>
            <form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search"><input type="search"
                                                                                       class="form-control form-control-dark text-bg-dark"
                                                                                       placeholder="Search..."
                                                                                       aria-label="Search"></form>
            <div class="text-end">
                <button type="button" class="btn btn-outline-light me-2">Login</button>
                <button type="button" class="btn btn-warning">Sign-up</button>
            </div>
        </div>
    </div>
</header>
</body>
</html>
html文件-index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head>
<body>
<!--引用模版myheader-->
<div th:replace="~{common :: myheader}"></div>

<main class="container">
    <a th:href="@{/list}">展示列表</a>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
        crossorigin="anonymous"></script>
</body>
</html>

开启热启动

需要引入相关依赖devtools

        <!--引入热启动功能依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
修改页面后:ctrl + F9刷新效果

国际化

SpringBoot通过MessageSourceAutoConfiguration类为国际化功能做了大量自动装配工作

国际化的自动配置逻辑

  1. 判断是否存在用户自定义的MessageSource Bean,如果有则SpringBoot不进行干预,使用用户的配置
  2. 如果没有则创建一个ResourceBundleMessageSource
  3. 读取application.yml或application.properties中以spring.messages开头的配置

基于自动配置实现国际化功能

  1. SpringBoot在类路径下查找messages资源绑定文件。文件名为:messages.properties
  2. 多语言可以定义多个消息文件,命名为messages_区域代码.properties。如:
    1. messages.properties : 默认
    2. messages_zh_CN.properties : 中文环境
    3. message_en_US.properties : 英文环境
  3. 在程序中可以通过自动注入MessageSource组件,获取国际化的配置项值
  4. 在页面中可以使用表达式#{}获取国家化的配置项值

国际化功能示例

消息文件

messages.properties

login=Login
sign=Sign-Up
home=Home
list=List

messages_en_US.properties

login=Login
sign=Sign-Up
home=Home
list=List

message_zh_CN.properties

login=登录
sign=注册
home=首页
list=列表

html文件

common.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>通用</title>
</head>
<body>
<h1 th:text="#{login}"></h1>
<h1 th:text="#{sign}"></h1>
<h1 th:text="#{home}"></h1>
<h1 th:text="#{list}"></h1>
</body>
</html>

控制层接口

/**
 * 国际化配置文件信息获取控制层接口
 */
@Controller
public class MessageController {
    @Autowired
    private MessageSource messageSource;

    @GetMapping("/common")
    public String common() {
        return "common";
    }

    @GetMapping("/message")
    @ResponseBody
    public String message(HttpServletRequest request) {
        Locale locale = request.getLocale();
        //利用代码方式获取国际化配置文件中指定配置项的值
        return messageSource.getMessage("login", null, locale);
    }
}

错误处理机制

默认的错误处理机制

错误处理的自动配置在ErrorMvcAutoConfiguration中,两大核心机制:

  1. SpringMVC的错误处理机制依然保留,当SpringMVC处理不了时,才会交给SpringBoot进行处理
  2. SpringBoot会自适应处理错误,响应页面或JSON数据

18

ErrorMvcAutoConfiguration源码分析

当SpringMVC处理不了时,会转发给/error路径,SpringBoot在底层写好了一个BasicErrorController的组件专门处理这个请求

19

20

BasicErrorController类针对/error提供了两个接口,根据MediaType类型,返回HTML格式或者ResponseEntity(JSON)格式。

对于返回HTML格式的接口-errorHtml()方法逻辑:
1.解析错误的自定义视图地址
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
2.如果解析不到错误页面地址,默认给一个error的错误页
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);

ErrorMvcAutoConfiguration中专门有一个错误视图解析器,用来解析自定义错误页

21

22

ErrorMvcAutoConfiguration也提供了默认名为error的view(白页)与封装了JSON格式的错误信息

23

SpringBoot解析自定义错误页的规则

  • 如果发生了500、404等错误
    • 如果有模版引擎:默认在classpath:/templates/error/路径下寻找精确码.html文件
    • 如果没有模版引擎:在静态资源路径下寻找精确码.html文件
  • 当匹配不到精确码.html文件时,就去找5xx.html、4xx.html这种文件进行模糊匹配
    • 如果有模版引擎:默认在classpath:/templates/error/路径下寻找5xx.html、4xx.html这种文件
    • 如果没有模版引擎,在静态资源路径下寻找5xx.html、4xx.html这种文件
  • 如果匹配不到5xx.html、4xx.html这种文件,则检查是否有模版引擎,如果有则在classpath:/templates/路径下寻找error.html文件,如果有则直接渲染,否则会返回默认提供的error.html文件,也就是白页

24

自定义错误响应

  1. 自定义json响应:使用@ControllerAdvice + @ExceptionHandler进行统一异常处理
  2. 自定义页面响应:根据SpringBoot的错误页面解析规则,添加html文件

错误响应可以获取到的model数据

25

自定义错误响应示例

后台异常处理类

/**
 * 集中处理所有@Controller发送的错误
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    /**
     * @ExceptionHanlder 表示一个方法处理错误,默认只能处理这个类发送的指定错误
     * @param e Exception
     * @return String
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public String handlerException(Exception e) {
        return "统一处理这个异常 the exception reason is " + e.getMessage();
    }
}

错误处理的html文件

/templates/error/500.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>500</title>
</head>
<body>
如果模版引擎下有精确码则返回500.html
<br/>
错误堆栈:
[[${trace}]]
</body>
</html>
/templates/error/4xx.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>4xx</title>
</head>
<body>
如果模版引擎下有精确码没有,有模糊码则返回4xx.html
</body>
</html>
/templates/error.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Error</title>
</head>
<body>
如果模版引擎下有error页面就直接渲染
</body>
</html>

嵌入式容器

在传统的Java Web开发中,我们通常需要将应用程序打包成WAR文件,然后部署到独立的外部Servlet容器中(如Tomcat、Jetty、Undertow等)。而SpringBoot引入了嵌入式容器的概念,彻底改变了这一模式

嵌入式容器的概念

简单来说,嵌入式容器就是把Web服务器(如Tomcat)直接作为一个软件库嵌入到你的SpringBoot应用程序中。你的应用程序本身就是一个独立可执行JAR包,里面包含了Web服务器。启动main方法时,Web服务器随之启动,不需要预先安装和配置外部容器。

自动配置原理

  • SpringBoot默认嵌入Tomcat作为Servlet容器
  • 自动配置类是:ServletWebServerFactoryAutoConfiguration,EmbeddedWebServerFactoryCustomizerAutoConfiguration

26

  1. ServletWebServerFactoryAutoConfiguration自动配置了嵌入式容器场景
  2. 绑定了ServerProperties配置类,所有和服务器有关的配置:server
  3. ServletWebServerFactoryAutoConfiguration导入了嵌入式的三大服务器:Tomcat、Jetty、Undertow
    1. 导入Tomcat、Jetty、Undertow都有条件注解。系统中有这个类才行(即导入对应依赖包)
    2. 默认Tomcat配置生效。给容器中放TomcatServletWebServerFactory
    3. 都给容器中ServletWebServerFactory放了一个Web服务器工厂(用来构建Web服务器)
    4. web服务器工厂都有一个功能,getWebServer获取web服务器
    5. TomcatServletWebServerFactory创建了tomcat
  4. ServletWebServerApplicationContext该ioc容器,启动的时候会调用创建web服务器
  5. Spring容器刷新启动时,会预留一个时机去刷新子容器-onRefresh()

27

自定义嵌入式Servlet容器

默认为Tomcat,可以通过排除tomcat,引入其他容器来进行自定义Servlet容器,下面是从tomcat切换为jetty的示例

    <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>

全面接管SpringMVC

  • SpringBoot默认配置好了SpringMVC的所有常用特性
  • 如果我们需要全面接管SpringMVC的所有配置并禁用默认配置,仅需要编写一个WebMvcConfigurer配置类,并标注@EnableMvc注解即可

WebMvcAutoConfiguration配置的规则

  • WebMvcAutoConfiguration-web场景的自动配置类
    • 支持RESTful的filter-HiddenHttpMethodFilter
    • 支持非POST请求,请求体携带数据的filter-FormContentFilter
    • 静态内部类WebMvcAutoConfigurationAdapter,其实现了WebMvcConfigurer接口,定义Spring MVC底层组件
      • 定义好WebMvcConfigurer底层组件默认功能
      • 视图解析器-InternalResourceViewResolver
      • 视图解析器-BeanNameViewResolver,视图名(controller方法中返回值字符串)就是组件名
      • 内容协商解析器-ContentNegotiatingViewResolver
      • 请求上下文过滤器-RequestContextFilter:任意位置直接获取当前请求
    • 静态内部类EnableWebMvcConfiguration,其继承DelegatingWebMvcConfiguration类
      • RequestMappingHandlerAdapter
      • WelcomePageHandlerMapping:欢迎页功能支持(模版引擎目录、静态资源目录放index.html),项目访问/ 默认展示该页面
      • RequestMappingHandlerMapping:找每个请求由谁处理的映射关系
      • ExceptionHandlerExceptionResolver:默认的异常解析器
      • LocaleResolver:国际化解析器
      • ThemeResolver:主题解析器
      • FlashMapManager:临时数据共享
      • FormattingConversionService:数据格式化、类型转换
      • Validator:数据校验JSR303提供的数据校验功能
      • WebBindingInitializer:请求参数的封装与绑定
      • ContentNegotiationManager:内容协商管理器
    • 静态内部类ProblemDetailsErrorHandlingConfiguration
      • ProblemDetailsExceptionHandler-错误详情处理,SpringMVC内部场景异常被它捕获

WebMvcConfigurer功能

方法名 输入参数 功能 备注
addFormatters FormatterRegistry 格式化器:支持属性上@NumberFormat和@DatetimeFormat的数据类型转换
getValidator 无 数据校验:校验Controller上使用@Valide注解标注的参数的合法性 需要导入starter-validator依赖
addInterceptors InterceptorRegistry 拦截器:拦截收到的所有请求
configureContentNegotiation ContentNegotiationConfigurer 内容协商:支持多种数据格式返回。需要配置支持这种类型的HttpMessageConverter 默认支持json
configureMessageConverters List<HttpMessageConverter<?>> 消息转换器:标注@ResponseBody的返回值会利用MessageConverter转换出去 默认支持byte,string,multipart,resource,json格式
addViewControllers ViewControllerRegistry 视图映射:直接将请求路径与物理视图映射。用于无java业务逻辑的直接视图页面渲染
configureViewResulvers ViewResolverRegistry 视图解析器:逻辑视图转为物理视图
addResourceHandlers ResourceHandlerRegistry 静态资源处理:静态资源路径映射、缓存控制
configureDefaultServletHanding DefaultServletHandlerConfigurer 默认Servlet:可以覆盖Tomcat的DefaultServlet。让DispatcherServlet拦截/
configurePathMatch PathMatchConfigurer 路径匹配:自定义URL路径匹配。可以自动为所有路径加上指定前缀,比如/api
configureAsyncSupport AsyncSupportConfigurer 异步支持
addCorsMappings CorsRegistry 跨域
addArgumentResolvers List<HandlerMethodArgumentResolver> 参数解析器
addReturnValueHandlers List<HandlerMethodReturnValueHandler> 返回值解析器
configureHandlerExceptionResolvers List<HandlerExceptionResulver> 异常处理器
getMessageCodesResolver 消息吗解析器:国际化使用

@EnableWebMvc禁用默认原理

  1. @EnableWebMvc注解给容器导入了DelegationWebMvcConfiguration组件,其继承WebMvcConfigurationSupport类
  2. WebMvcAutoConfiguration有一个核心的条件注解,@ConditionalOnMissingBean(WebMvcConfigurationSupport.class),当容器中没有WebMvcConfigurationSupport,WebMvcAutoConfiguration才生效
  3. @EnableWebMvc注解注解导入了WebMvcConfigurationSupport导致WebMvcAutoConfiguration失效,从而禁用了默认行为

28引入EnableWebMvc导致MVC默认配置失效的原因

Web新特性

Problemdetails

新增错误信息返回格式:RFC 7807

RFC 7807: RFC 7807: Problem Details for HTTP APIs | RFC Editor

原理

29

  1. ProblemDetailsExceptioHandler是一个@ControllerAdbice集中处理系统异常的组件
  2. 如果系统出现以下异常,SpringBoot支持以RFC 7807规范方式返回错误数据

30

示例

开启异常处理开关(默认关闭)
# springboot3新特性-Problemdetails 默认关闭
spring.mvc.problemdetails.enabled=true
调用异常接口前后区别
未开启的异常处理

31

开启后的异常处理

32

函数式Web

SpringMVC 5.2以后允许使用函数式的方式,定义Web的请求处理流程,即函数式接口

Web请求处理的两种方式

  1. @Controller + @RequestMapping:耦合式(路由、业务耦合)
  2. 函数式Web:分离式(路由、业务分离)

函数式Web使用示例

Web路由配置类
/**
 * 函数式Web
 * 场景:User - CRUD
 * GET /user/1  获取1号⽤户
 * GET /users   获取所有⽤户
 * POST /user  请求体携带JSON,新增⼀个⽤户
 * PUT /user/1 请求体携带JSON,修改1号⽤户
 * DELETE /user/1 删除1号⽤户
 */
@Configuration
public class WebFunctionConfig {
    /**
     * 1.给容器中放一个Bean:类型是RouterFunction<ServerResponse>
     * 
     * 核心四大对象
     * 1.RouterFunction: 定义路由信息:发什么请求,谁来处理
     * 2.RequestPredicate: 定义请求,请求谓语:请求方式、请求参数
     * 3.ServerRequest: 封装请求完整数据
     * 4.ServerResponse: 封装响应完整数据
     */

    @Bean
    public RouterFunction<ServerResponse> userRoutes(UserBizHandler userBizHandler /*这个会被自动注入进来*/) {
        return RouterFunctions.route() //开始定义路由信息
                .GET("/user/{id}", RequestPredicates.accept(MediaType.ALL), userBizHandler::getUser)
                .GET("/users", userBizHandler::getAllUsers)
                .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();
    }


}

业务类
/**
 * 专门处理User有关的业务类
 */
@Slf4j
@Service
public class UserBizHandler {
    /**
     * 查询指定id的用户
     *
     * @param serverRequest 请求
     * @return serverResponse
     */
    public ServerResponse getUser(ServerRequest serverRequest) {
        //业务处理
        String id = serverRequest.pathVariable("id");
        log.info("查询[{}]用户信息", id);
        Person person = new Person(1L, "Alex", 56);
        //构造响应
        return ServerResponse.ok().body(person);
    }

    /**
     * 获取所有用户
     *
     * @param serverRequest 请求
     * @return serverResponse
     */
    public ServerResponse getAllUsers(ServerRequest serverRequest) {

        List<Person> personList = Arrays.asList(new Person(1L, "Alex", 56), new Person(2L, "Bob", 65));
        log.info("查询所有用户信息完成");
        return ServerResponse.ok().body(personList);
    }

    /**
     * 保存用户信息
     *
     * @param serverRequest 请求
     * @return serverResponse
     */
    public ServerResponse saveUser(ServerRequest serverRequest) throws ServletException, IOException {
        Person body = serverRequest.body(Person.class);
        log.info("保存的用户信息:{}", body);
        return ServerResponse.ok().build();
    }

    /**
     * 更新用户信息
     *
     * @param serverRequest 请求
     * @return serverResponse
     */
    public ServerResponse updateUser(ServerRequest serverRequest) throws ServletException, IOException {
        Person body = serverRequest.body(Person.class);
        log.info("更新的用户信息:{}", body);
        return ServerResponse.ok().build();
    }

    /**
     * 删除用户信息
     *
     * @param serverRequest 请求
     * @return serverResponse
     */
    public ServerResponse deleteUser(ServerRequest serverRequest) throws ServletException, IOException {
        Person body = serverRequest.body(Person.class);
        log.info("删除的用户信息:{}", body);
        return ServerResponse.ok().build();
    }
}

参考资料

https://docs.spring.io/spring-boot/3.5/reference/web/index.html

https://docs.spring.io/spring-boot/3.5/reference/web/servlet.html

https://www.thymeleaf.org/

posted @ 2026-06-19 14:12  柯南。道尔  阅读(67)  评论(0)    收藏  举报