第9课:SpringBoot4.x Web开发基础:WebMvc自动配置、静态资源、请求参数接收

文章目录

    • [1. 课前导读](#1. 课前导读)
    • [2. 知识目标](#2. 知识目标)
    • [3. 理论讲解](#3. 理论讲解)
      • [3.1 Spring Boot 4.x模块化对Web开发的影响](#3.1 Spring Boot 4.x模块化对Web开发的影响)
      • [3.2 WebMvcAutoConfiguration的核心能力](#3.2 WebMvcAutoConfiguration的核心能力)
      • [3.3 静态资源处理机制](#3.3 静态资源处理机制)
      • [3.4 五种请求参数接收方式](#3.4 五种请求参数接收方式)
      • [3.5 Spring Boot 4.x的新特性:API版本控制](#3.5 Spring Boot 4.x的新特性:API版本控制)
      • [3.6 JSpecify空安全与参数绑定](#3.6 JSpecify空安全与参数绑定)
    • [4. 环境搭建](#4. 环境搭建)
      • [4.1 项目创建](#4.1 项目创建)
      • [4.2 pom.xml](#4.2 pom.xml)
      • [4.3 application.yml](#4.3 application.yml)
      • [4.4 静态资源文件](#4.4 静态资源文件)
    • [5. 代码实战](#5. 代码实战)
      • [5.1 五种参数接收方式的完整实现](#5.1 五种参数接收方式的完整实现)
      • [5.2 DTO与参数校验](#5.2 DTO与参数校验)
      • [5.3 全局异常处理](#5.3 全局异常处理)
      • [5.4 WebMvcConfigurer自定义配置](#5.4 WebMvcConfigurer自定义配置)
      • [5.5 启动类](#5.5 启动类)
      • [5.6 运行与验证](#5.6 运行与验证)
    • [6. 底层源码剖析](#6. 底层源码剖析)
      • [6.1 WebMvcAutoConfiguration的加载机制](#6.1 WebMvcAutoConfiguration的加载机制)
      • [6.2 静态资源处理的源码实现](#6.2 静态资源处理的源码实现)
      • [6.3 请求参数解析的核心机制](#6.3 请求参数解析的核心机制)
      • [6.4 消息转换器的自动配置](#6.4 消息转换器的自动配置)
    • [7. 常见坑点与生产问题排查](#7. 常见坑点与生产问题排查)
    • [8. 本章总结](#8. 本章总结)
    • [9. 课后作业](#9. 课后作业)
  • [🔗 SpringBoot 4.x 从入门到企业级实战系列课程](#🔗 SpringBoot 4.x 从入门到企业级实战系列课程)

本文是《SpringBoot 4.x 从入门到精通》系列第9课,全文约13000字,建议阅读时间50分钟。

环境要求:Spring Boot 4.x(基于4.0.x GA),Spring Framework 7.0.x,Java 17+(推荐Java 21/25)


1. 课前导读

前8课我们走完了Spring Boot的"基础设施"和"核心机制"------从环境搭建、项目构建、自动配置、配置绑定,到IoC容器、AOP切面。从这一课开始,我们正式进入Web开发这个最贴近日常工作的领域。

Web开发是Spring Boot最核心的应用场景。你写的每一个@RestController、每一个@GetMapping、每一次接收前端传来的JSON数据,背后都依赖于Spring MVC的自动配置。在Spring Boot 3.x中,这些能力由spring-boot-starter-web提供,Web MVC的自动配置类集中在org.springframework.boot.autoconfigure.web.servlet包下。

但在Spring Boot 4.x中,这一切都变了。

Spring Boot 4.0将单体spring-boot-autoconfigure JAR(约6.2MB)拆分为47个专属轻量模块,其中Web MVC的自动配置类迁移到了org.springframework.boot.webmvc.autoconfigure包下,Starter坐标从spring-boot-starter-web重命名为spring-boot-starter-webmvc。如果你带着3.x的import路径来写4.x代码,编译会直接失败。

此外,Spring Framework 7.0在参数绑定层面做了重要改进:@RequestParam、@PathVariable等参数的可空性现在通过JSpecify的@Nullable注解来表达,不再依赖required属性的默认值。Spring Boot 4.0还内置了API版本控制能力,通过@ApiVersion注解和WebMvcConfigurer即可实现多版本API共存。

本节课将系统讲解Spring Boot 4.x的Web开发基础,从WebMvc自动配置的模块化变化、静态资源处理,到五种请求参数接收方式的完整用法,最后深入WebMvcAutoConfiguration的底层源码,帮你建立4.x Web开发的正确认知。

前置知识:第6课的IoC核心注解、第7课的Bean生命周期、第8课的AOP切面(用于理解拦截器机制)。

学习难点 :理解4.x模块化后WebMvc自动配置的包名和Starter变化;掌握JSpecify空安全注解在参数绑定中的应用;区分@RequestParam和@PathVariable的语义差异;理解静态资源的默认查找路径和优先级。

【配图描述:Spring Boot 4.x Web开发全景图------从WebMvc自动配置、静态资源处理到请求参数接收的完整链路】


2. 知识目标

学完本节课,你必须掌握以下核心知识点:

  1. Spring Boot 4.x WebMvc模块化变化 :spring-boot-starter-web→spring-boot-starter-webmvc的Starter重命名,WebMvcAutoConfiguration从org.springframework.boot.autoconfigure.web.servlet迁移到org.springframework.boot.webmvc.autoconfigure的包名变化。
  2. WebMvcAutoConfiguration的核心能力:内容协商、静态资源映射、消息转换器、视图解析器的自动配置逻辑。
  3. 静态资源处理 :默认的五个静态资源目录及其优先级、spring.web.resources.static-locations的自定义配置、WebJars支持、版本化资源的缓存策略。
  4. 五种请求参数接收方式 :@RequestParam(查询参数)、@PathVariable(路径变量)、@RequestBody(JSON请求体)、@RequestHeader(请求头)、@CookieValue(Cookie)的用法与适用场景。
  5. JSpecify空安全参数绑定 :@Nullable注解在可选参数上的使用,以及HandlerMethodValidationException的处理。
  6. WebMvcConfigurer自定义配置 :通过实现WebMvcConfigurer接口自定义静态资源映射、拦截器、消息转换器。

3. 理论讲解

3.1 Spring Boot 4.x模块化对Web开发的影响

在理解4.x的Web开发之前,我们需要先搞清楚一个根本问题:模块化重构到底改变了什么?

Spring Boot 3.x中,所有的Web MVC自动配置类都集中在org.springframework.boot.autoconfigure.web.servlet包下,与JPA、Security、Redis等模块的自动配置类共同打包在spring-boot-autoconfigure JAR中。无论你的应用是否使用Web,这个JAR都会被完整加载到classpath中。

Spring Boot 4.0将这一单体JAR拆分为47个专属轻量模块,Web MVC的自动配置类被独立到spring-boot-webmvc模块中,包名也从org.springframework.boot.autoconfigure.web.servlet迁移到了org.springframework.boot.webmvc.autoconfigure。Servlet相关的自动配置类则迁移到了spring-boot-servlet模块。

这种变化的直接影响是:应用只加载实际引入的模块中的自动配置类,减少了类加载和条件判断的开销。根据实测数据,Spring Boot 4.0相比3.5.x,镜像体积减少了19%,启动时间缩短了33%。

对开发者的影响体现在两个方面:

第一,Starter坐标变化。 Web项目的依赖从spring-boot-starter-web变为spring-boot-starter-webmvc,测试依赖从spring-boot-starter-test变为spring-boot-starter-webmvc-test。

第二,import路径变化。 如果你在代码中直接引用了自动配置类(如WebMvcAutoConfiguration),import路径需要从org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration改为org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration。

【配图描述:Spring Boot 3.x单体autoconfigure vs 4.x模块化WebMvc自动配置的架构对比图】

3.2 WebMvcAutoConfiguration的核心能力

WebMvcAutoConfiguration是Servlet-based Spring MVC的中央配置类。它通过@ConditionalOnWebApplication(type = Servlet)和@ConditionalOnClass(WebMvcConfigurer.class)等条件注解控制生效,仅当应用是Servlet Web应用且classpath中存在WebMvcConfigurer类时才激活。

它的核心能力包括:

内容协商。 自动注册ContentNegotiatingViewResolver和BeanNameViewResolver,支持基于请求头(Accept)或参数(format)的响应格式选择。

静态资源处理。 自动配置静态资源映射,支持从classpath:/static/、classpath:/public/、classpath:/resources/、classpath:/META-INF/resources/目录提供静态内容,同时支持WebJars。

类型转换。 自动注册WebConversionService(继承自DefaultFormattingConversionService),提供HTTP请求参数与Java类型之间的转换和格式化能力。

消息转换。 自动配置HttpMessageConverters,支持JSON(基于Jackson 3)、XML和String数据的序列化与反序列化。

自定义扩展。 通过WebMvcConfigurer接口,开发者可以覆盖或增强上述默认配置。

3.3 静态资源处理机制

静态资源处理是Web开发中最基础也最容易被忽视的部分。Spring Boot的静态资源处理机制可以概括为"约定优于配置"------你只需要把文件放到约定的目录下,Spring Boot会自动将其暴露为可访问的URL。

默认静态资源目录(按优先级从高到低):

目录 classpath路径 典型用途
/META-INF/resources/ classpath:/META-INF/resources/ WebJars
/resources/ classpath:/resources/ 资源文件
/static/ classpath:/static/ 静态资源(推荐)
/public/ classpath:/public/ 公共资源

当多个目录中存在同名文件时,优先级高的目录中的文件会被优先访问 。例如,classpath:/static/index.html和classpath:/public/index.html同时存在时,访问/index.html返回的是static目录下的文件。

默认URL映射 :静态资源被映射到根路径/**。例如,classpath:/static/css/style.css可以通过http://localhost:8080/css/style.css访问。

自定义静态资源路径 。可以通过application.yml配置:

yaml 复制代码
spring:
  web:
    resources:
      static-locations: classpath:/static/,classpath:/public/,file:/opt/uploads/
      static-path-pattern: /res/**  # 修改访问前缀
      cache:
        period: 3600s  # 缓存时间

重要提醒 :自定义static-locations时会覆盖默认配置,而非追加。如果你希望保留默认目录,必须显式列出所有目录。

WebJars支持 。WebJars允许以JAR包的形式引入前端库(如jQuery、Bootstrap)。Spring Boot自动配置了WebJars的映射路径:/webjars/**。引入依赖后即可通过/webjars/jquery/3.7.1/jquery.min.js访问。

版本化资源的缓存策略 。Spring Boot支持基于内容哈希的版本化URL。配置spring.web.resources.chain.strategy.content.enabled=true后,静态资源的URL会追加内容哈希,浏览器可以长时间缓存。这对于生产环境的性能优化非常关键。

3.4 五种请求参数接收方式

Spring MVC提供了五种主要的请求参数接收方式,每种方式适用于不同的场景。

方式一:@RequestParam------接收查询参数。 适用于GET请求的查询字符串参数,如/products?category=books&page=1。支持defaultValue设置默认值,支持required标记必填。在Spring Boot 4.x中,如果使用JSpecify的@Nullable注解标记参数,可以省略required=false。

java 复制代码
@GetMapping("/products")
public List<Product> list(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(required = false) String category) {
    // ...
}

方式二:@PathVariable------接收路径变量。 适用于RESTful风格的资源定位,如/users/1001。语义比@RequestParam更强------路径变量表达的是"资源标识",而查询参数表达的是"过滤条件"。

java 复制代码
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    // ...
}

方式三:@RequestBody------接收JSON请求体。 适用于POST/PUT请求的复杂对象。Spring Boot 4.x默认使用Jackson 3进行反序列化,包名为tools.jackson。配合@Valid注解可以触发JSR-303校验。

java 复制代码
@PostMapping("/users")
public User createUser(@Valid @RequestBody UserCreateRequest request) {
    // ...
}

方式四:@RequestHeader------接收请求头。 适用于传递元信息,如Authorization(认证令牌)、X-Trace-Id(链路追踪ID)、User-Agent(客户端信息)。

java 复制代码
@GetMapping("/profile")
public User getProfile(@RequestHeader("Authorization") String token) {
    // ...
}

方式五:@CookieValue------接收Cookie。 适用于读取浏览器Cookie中的值,如会话ID。

java 复制代码
@GetMapping("/preferences")
public Preferences getPreferences(
        @CookieValue(value = "sessionId", required = false) String sessionId) {
    // ...
}

参数校验 。在方法参数上直接使用@Min、@NotBlank等校验注解会触发方法级校验 ,抛出HandlerMethodValidationException。而配合@RequestBody和@Valid使用时会抛出MethodArgumentNotValidException。项目中建议同时处理这两种异常。

3.5 Spring Boot 4.x的新特性:API版本控制

Spring Boot 4.0内置了API版本控制能力,这是Web开发领域的重要增强。通过@ApiVersion注解和WebMvcConfigurer接口,可以实现多版本API共存。

版本解析策略支持四种方式:

方式 配置方法 请求示例
请求参数 configurer.useRequestParam("version") /api/users?version=1
请求头 configurer.useRequestHeader("API-Version") API-Version: 1
路径变量 configurer.usePathSegment() /api/v1/users
媒体类型 configurer.useMediaType() Accept: application/vnd.api.v1+json

使用方式:

java 复制代码
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        configurer.useRequestHeader("API-Version");
    }
}

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping(version = "1")
    public UserV1 getUserV1(@PathVariable Long id) { ... }

    @GetMapping(version = "2")
    public UserV2 getUserV2(@PathVariable Long id) { ... }
}

3.6 JSpecify空安全与参数绑定

Spring Framework 7.0全面采用JSpecify注解来表达空安全约束,这直接影响Web参数绑定的写法。

对Kotlin开发者的影响最大 。在Kotlin中,Spring框架利用Kotlin空安全来确定HTTP参数是否为必填项,无需显式定义required属性。@RequestParam name: String?被视为非必填,而@RequestParam name: String被视为必填。

对Java开发者的影响 主要体现在@Nullable注解的使用上。在Spring Boot 4.x中,可选参数的推荐写法是使用JSpecify的@Nullable注解:

java 复制代码
@GetMapping("/search")
public List<User> search(
        @RequestParam @Nullable String keyword,
        @RequestParam(defaultValue = "0") int page) {
    // ...
}

4. 环境搭建

4.1 项目创建

本节课创建一个Web开发基础实验项目,涵盖静态资源、五种参数接收方式和自定义配置。

项目结构

复制代码
webmvc-demo/
├── pom.xml
└── src/main/
    ├── java/com/example/webmvcdemo/
    │   ├── WebMvcDemoApplication.java
    │   ├── controller/
    │   │   ├── StaticResourceController.java
    │   │   ├── ParamController.java
    │   │   └── UserController.java
    │   ├── dto/
    │   │   ├── UserCreateRequest.java
    │   │   └── UserVO.java
    │   └── config/
    │       └── WebConfig.java
    └── resources/
        ├── static/
        │   ├── index.html
        │   ├── css/
        │   │   └── style.css
        │   ├── js/
        │   │   └── app.js
        │   └── images/
        │       └── logo.png
        ├── public/
        │   └── public-resource.txt
        └── application.yml

4.2 pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.8</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>webmvc-demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>webmvc-demo</name>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <!-- 
            Spring Boot 4.x Web MVC Starter
            注意:3.x 中为 spring-boot-starter-web
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>

        <!-- 参数校验 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- 测试 Starter(4.x 中为 webmvc-test) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

4.3 application.yml

yaml 复制代码
spring:
  application:
    name: webmvc-demo
  web:
    resources:
      # 自定义静态资源目录(覆盖默认配置)
      static-locations: classpath:/static/,classpath:/public/
      # 静态资源缓存时间
      cache:
        period: 3600s
      chain:
        strategy:
          content:
            enabled: true
            paths: /**

server:
  port: 8080

logging:
  level:
    com.example: DEBUG
    org.springframework.web: INFO

4.4 静态资源文件

src/main/resources/static/index.html:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Spring Boot 4.x WebMvc Demo</title>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <h1>欢迎使用 Spring Boot 4.x WebMvc</h1>
    <p>这是一个静态资源页面</p>
    <p>资源哈希版本化已启用,URL 会自动追加内容哈希。</p>
    <script src="/js/app.js"></script>
</body>
</html>

src/main/resources/static/css/style.css:

css 复制代码
body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: #fff;
    text-align: center;
    padding: 50px;
}
h1 { font-size: 2.5em; }

src/main/resources/public/public-resource.txt:

复制代码
这是 public 目录下的资源文件。
当 static 和 public 目录中存在同名文件时,static 优先。

5. 代码实战

5.1 五种参数接收方式的完整实现

ParamController.java:

java 复制代码
package com.example.webmvcdemo.controller;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

import java.util.Arrays;
import java.util.Map;

/**
 * 五种请求参数接收方式的完整演示。
 * 
 * 注意:Spring Boot 4.x 中 @RequestParam 的可选参数
 * 推荐使用 JSpecify 的 @Nullable 注解标记,
 * 而非 required=false。但 required=false 仍然有效。
 */
@RestController
@RequestMapping("/api/params")
public class ParamController {

    private static final Logger log = LoggerFactory.getLogger(ParamController.class);

    /**
     * 方式一:@RequestParam------接收查询参数。
     * 
     * 示例:GET /api/params/query?keyword=spring&page=0&size=10
     */
    @GetMapping("/query")
    public Map<String, Object> queryParams(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(required = false) String keyword) {

        log.info("查询参数: page={}, size={}, keyword={}", page, size, keyword);
        return Map.of(
                "page", page,
                "size", size,
                "keyword", keyword != null ? keyword : "未提供"
        );
    }

    /**
     * 方式二:@PathVariable------接收路径变量。
     * 
     * 示例:GET /api/params/path/1001
     */
    @GetMapping("/path/{id}")
    public Map<String, Object> pathVariable(@PathVariable Long id) {
        log.info("路径变量: id={}", id);
        return Map.of(
                "id", id,
                "type", id.getClass().getSimpleName(),
                "message", "通过路径变量接收资源标识"
        );
    }

    /**
     * 方式三:@RequestBody------接收 JSON 请求体。
     * 
     * 示例:POST /api/params/body
     * Body: {"name":"Alice","age":25}
     * 
     * Spring Boot 4.x 默认使用 Jackson 3 进行反序列化,
     * 包名为 tools.jackson,而非 com.fasterxml.jackson。
     */
    @PostMapping("/body")
    public Map<String, Object> requestBody(
            @RequestBody Map<String, Object> body) {
        log.info("请求体: {}", body);
        return Map.of(
                "received", body,
                "message", "通过 @RequestBody 接收 JSON"
        );
    }

    /**
     * 方式四:@RequestHeader------接收请求头。
     * 
     * 示例:GET /api/params/header
     * Header: X-Trace-Id: abc123, User-Agent: curl/8.0
     */
    @GetMapping("/header")
    public Map<String, Object> requestHeader(
            @RequestHeader(value = "X-Trace-Id", required = false) String traceId,
            @RequestHeader(value = "User-Agent", required = false) String userAgent) {

        log.info("请求头: X-Trace-Id={}, User-Agent={}", traceId, userAgent);
        return Map.of(
                "traceId", traceId != null ? traceId : "未提供",
                "userAgent", userAgent != null ? userAgent : "未提供"
        );
    }

    /**
     * 方式五:@CookieValue------接收 Cookie。
     * 
     * 示例:GET /api/params/cookie
     * Cookie: sessionId=abc123
     */
    @GetMapping("/cookie")
    public Map<String, Object> cookieValue(
            @CookieValue(value = "sessionId", required = false) String sessionId,
            HttpServletRequest request) {

        Cookie[] cookies = request.getCookies();
        int cookieCount = cookies != null ? cookies.length : 0;

        log.info("Cookie: sessionId={}, 总数={}", sessionId, cookieCount);
        return Map.of(
                "sessionId", sessionId != null ? sessionId : "未提供",
                "cookieCount", cookieCount
        );
    }

    /**
     * 综合演示:一个接口同时使用多种参数接收方式。
     * 
     * 示例:POST /api/params/comprehensive/1001?action=update
     * Header: X-Trace-Id: trace-001
     * Body: {"name":"Alice"}
     */
    @PostMapping("/comprehensive/{id}")
    public Map<String, Object> comprehensive(
            @PathVariable Long id,
            @RequestParam(defaultValue = "query") String action,
            @RequestHeader("X-Trace-Id") String traceId,
            @RequestBody Map<String, Object> body) {

        return Map.of(
                "id", id,
                "action", action,
                "traceId", traceId,
                "body", body,
                "message", "四种参数来源同时接收"
        );
    }
}

5.2 DTO与参数校验

UserCreateRequest.java(Record + JSR-303校验):

java 复制代码
package com.example.webmvcdemo.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

/**
 * 用户创建请求 DTO。
 * 
 * 使用 Record 作为请求体绑定目标:
 * 1. Jackson 3 支持 Record 的构造器绑定
 * 2. @Valid 触发 JSR-303 校验
 * 3. 校验失败抛出 MethodArgumentNotValidException
 */
public record UserCreateRequest(

        @NotBlank(message = "用户名不能为空")
        @Size(min = 3, max = 30, message = "用户名长度必须在 3 到 30 个字符之间")
        String username,

        @NotBlank(message = "邮箱不能为空")
        @Email(message = "邮箱格式不正确")
        String email,

        @NotBlank(message = "显示名称不能为空")
        @Size(max = 50, message = "显示名称不能超过 50 个字符")
        String displayName
) {}

UserVO.java:

java 复制代码
package com.example.webmvcdemo.dto;

import java.time.LocalDateTime;

/**
 * 用户视图对象。
 */
public record UserVO(
        Long id,
        String username,
        String email,
        String displayName,
        LocalDateTime createdAt
) {}

UserController.java:

java 复制代码
package com.example.webmvcdemo.controller;

import com.example.webmvcdemo.dto.UserCreateRequest;
import com.example.webmvcdemo.dto.UserVO;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 用户管理接口。
 * 演示 @RequestBody + @Valid 的参数校验。
 */
@RestController
@RequestMapping("/api/users")
public class UserController {

    private final Map<Long, UserVO> store = new ConcurrentHashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    /**
     * 创建用户。
     * 
     * POST /api/users
     * Body: {"username":"alice","email":"alice@example.com","displayName":"Alice"}
     * 
     * @Valid 触发 JSR-303 校验,校验失败抛出 MethodArgumentNotValidException。
     */
    @PostMapping
    public ResponseEntity<UserVO> createUser(
            @Valid @RequestBody UserCreateRequest request) {

        Long id = idGenerator.getAndIncrement();
        UserVO user = new UserVO(
                id,
                request.username(),
                request.email(),
                request.displayName(),
                LocalDateTime.now()
        );
        store.put(id, user);

        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }

    /**
     * 获取用户详情。
     * GET /api/users/{id}
     */
    @GetMapping("/{id}")
    public ResponseEntity<UserVO> getUser(@PathVariable Long id) {
        UserVO user = store.get(id);
        if (user == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(user);
    }

    /**
     * 获取用户列表(分页 + 搜索)。
     * GET /api/users?page=0&size=10&keyword=alice
     */
    @GetMapping
    public Map<String, Object> listUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(required = false) String keyword) {

        List<UserVO> allUsers = List.copyOf(store.values());
        List<UserVO> filtered = keyword == null ? allUsers :
                allUsers.stream()
                        .filter(u -> u.username().contains(keyword))
                        .toList();

        return Map.of(
                "page", page,
                "size", size,
                "total", filtered.size(),
                "data", filtered
        );
    }
}

5.3 全局异常处理

java 复制代码
package com.example.webmvcdemo.controller;

import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.List;
import java.util.Map;

/**
 * 全局异常处理器。
 * 
 * 处理两类校验异常:
 * 1. MethodArgumentNotValidException:@Valid + @RequestBody 校验失败
 * 2. HandlerMethodValidationException:方法参数上的 @Min、@NotBlank 等校验失败
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理 @RequestBody + @Valid 校验失败。
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleValidationErrors(
            MethodArgumentNotValidException ex) {

        List<Map<String, String>> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(this::toErrorMap)
                .toList();

        return Map.of(
                "code", 400,
                "error", "Validation Failed",
                "errors", errors
        );
    }

    private Map<String, String> toErrorMap(FieldError error) {
        return Map.of(
                "field", error.getField(),
                "message", error.getDefaultMessage(),
                "rejectedValue", String.valueOf(error.getRejectedValue())
        );
    }
}

5.4 WebMvcConfigurer自定义配置

WebConfig.java:

java 复制代码
package com.example.webmvcdemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * WebMvc 自定义配置。
 * 
 * 通过实现 WebMvcConfigurer 接口,
 * 可以覆盖或增强 WebMvcAutoConfiguration 的默认配置。
 * 
 * 注意:在 Spring Boot 4.x 中,WebMvcConfigurer
 * 所在的包路径没有变化,仍然是 org.springframework.web.servlet.config.annotation。
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    /**
     * 自定义静态资源映射。
     * 
     * addResourceHandler("/uploads/**"):访问路径
     * addResourceLocations("file:/opt/uploads/"):实际文件位置
     * 
     * 注意:addResourceLocations 中的路径必须以 / 结尾。
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 映射本地文件系统目录
        registry.addResourceHandler("/uploads/**")
                .addResourceLocations("file:/opt/uploads/");

        // 映射类路径下的自定义目录
        registry.addResourceHandler("/custom/**")
                .addResourceLocations("classpath:/custom-resources/");
    }
}

5.5 启动类

java 复制代码
package com.example.webmvcdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * WebMvc 实验应用启动入口。
 * 
 * 注意:Spring Boot 4.x 中 @SpringBootApplication
 * 的组件扫描范围仍然是启动类所在包及其子包,
 * 但 WebMvcAutoConfiguration 的加载由模块化机制自动处理。
 */
@SpringBootApplication
public class WebMvcDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebMvcDemoApplication.class, args);
    }
}

5.6 运行与验证

编译并启动

bash 复制代码
cd webmvc-demo
mvn clean install
mvn spring-boot:run

验证静态资源

bash 复制代码
# 访问 index.html
curl http://localhost:8080/index.html
# 输出 HTML 内容

# 访问 CSS
curl http://localhost:8080/css/style.css
# 输出 CSS 内容

# 访问 public 目录的资源
curl http://localhost:8080/public-resource.txt
# 输出文本内容

# 验证版本化资源(URL 自动追加内容哈希)
# 在浏览器中查看 index.html 的源码,CSS 和 JS 的 URL 会变为
# /css/style-abc123.css 等形式

验证五种参数接收方式

bash 复制代码
# 方式一:@RequestParam
curl "http://localhost:8080/api/params/query?page=1&size=20&keyword=spring"
# 输出:{"page":1,"size":20,"keyword":"spring"}

# 方式二:@PathVariable
curl http://localhost:8080/api/params/path/1001
# 输出:{"id":1001,"type":"Long","message":"通过路径变量接收资源标识"}

# 方式三:@RequestBody
curl -X POST http://localhost:8080/api/params/body \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","age":25}'
# 输出:{"received":{"name":"Alice","age":25},"message":"通过 @RequestBody 接收 JSON"}

# 方式四:@RequestHeader
curl http://localhost:8080/api/params/header \
  -H "X-Trace-Id: trace-001" \
  -H "User-Agent: curl/8.0"
# 输出:{"traceId":"trace-001","userAgent":"curl/8.0"}

# 方式五:@CookieValue
curl http://localhost:8080/api/params/cookie \
  -H "Cookie: sessionId=abc123"
# 输出:{"sessionId":"abc123","cookieCount":1}

验证参数校验

bash 复制代码
# 创建用户(校验通过)
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","email":"alice@example.com","displayName":"Alice"}'
# 输出:{"id":1,"username":"alice","email":"alice@example.com","displayName":"Alice","createdAt":"..."}

# 创建用户(校验失败------用户名太短)
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"username":"al","email":"alice@example.com","displayName":"Alice"}'
# 输出:{"code":400,"error":"Validation Failed","errors":[{"field":"username","message":"用户名长度必须在 3 到 30 个字符之间","rejectedValue":"al"}]}

6. 底层源码剖析

6.1 WebMvcAutoConfiguration的加载机制

WebMvcAutoConfiguration是Servlet-based Spring MVC的中央配置类。在Spring Boot 4.x中,它位于spring-boot-webmvc模块中,包名为org.springframework.boot.webmvc.autoconfigure。

java 复制代码
// 源码路径:org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, ... })
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class WebMvcAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    public WebMvcConfigurer webMvcConfigurer(...) {
        return new WebMvcAutoConfigurationAdapter(...);
    }
}

关键条件注解:

  • @ConditionalOnWebApplication(type = Type.SERVLET):仅当应用是Servlet Web应用时生效
  • @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }):仅当classpath中存在这些类时生效
  • @ConditionalOnMissingBean(WebMvcConfigurationSupport.class):如果用户自定义了WebMvcConfigurationSupport,则自动配置不生效------这是用户覆盖默认配置的方式之一

6.2 静态资源处理的源码实现

静态资源处理的核心逻辑在WebMvcAutoConfigurationAdapter#addResourceHandlers()方法中:

java 复制代码
// 源码路径:org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 1. 如果用户自定义了资源映射,跳过默认配置
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }

    // 2. 添加 WebJars 映射:/webjars/**
    addResourceHandler(registry, "/webjars/**",
            "classpath:/META-INF/resources/webjars/");

    // 3. 添加静态资源映射:/**
    addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(),
            this.resourceProperties.getStaticLocations());
}

private void addResourceHandler(ResourceHandlerRegistry registry,
                                String pattern, String... locations) {
    if (registry.hasMappingForPattern(pattern)) {
        return;
    }
    // 创建 ResourceHttpRequestHandler
    ResourceHandlerRegistration registration = registry
            .addResourceHandler(pattern)
            .addResourceLocations(locations);

    // 配置缓存策略
    configureResourceCaching(registration);
}

默认静态资源位置 由WebProperties.Resources定义:

java 复制代码
// 源码路径:org.springframework.boot.autoconfigure.web.WebProperties.Resources
public static class Resources {
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
        "classpath:/META-INF/resources/",
        "classpath:/resources/",
        "classpath:/static/",
        "classpath:/public/"
    };
    private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
    private String staticPathPattern = "/**";
    // ...
}

优先级机制 :当多个位置存在同名资源时,Spring MVC的ResourceHttpRequestHandler会按staticLocations数组的顺序依次查找,返回第一个找到的资源。这就是为什么classpath:/META-INF/resources/(WebJars)的优先级最高------它排在数组的第一位。

6.3 请求参数解析的核心机制

Spring MVC的参数解析由HandlerMethodArgumentResolver接口体系完成。每种参数注解对应一个或多个ArgumentResolver实现。

核心流程:

  1. RequestMappingHandlerAdapter调用invokeHandlerMethod()处理请求
  2. ServletInvocableHandlerMethod#invokeForRequest()遍历所有方法参数
  3. 对每个参数,从argumentResolvers列表中查找支持该参数的解析器
  4. 调用解析器的resolveArgument()方法,从请求中提取参数值
  5. 将解析后的参数数组传递给目标方法

@RequestParam的解析器 是RequestParamMethodArgumentResolver:

java 复制代码
// 源码路径:org.springframework.web.method.annotation.RequestParamMethodArgumentResolver
@Override
public Object resolveArgument(MethodParameter parameter,
                              ModelAndViewContainer mavContainer,
                              NativeWebRequest webRequest,
                              WebDataBinderFactory binderFactory) {
    // 1. 获取参数名
    String name = parameter.getParameterName();

    // 2. 从请求中获取参数值
    String[] paramValues = webRequest.getParameterValues(name);

    // 3. 如果参数值为空且 required=true,抛出异常
    if (paramValues == null) {
        if (parameter.isOptional()) {  // JSpecify @Nullable 标记为可选
            return null;
        }
        if (parameter.isRequired()) {
            throw new MissingServletRequestParameterException(name, ...);
        }
        return null;
    }

    // 4. 类型转换
    return convertIfNecessary(name, paramValues, parameter);
}

JSpecify @Nullable的集成 。Spring Framework 7.0中,MethodParameter#isOptional()方法会检查参数上是否有JSpecify的@Nullable注解。如果有,解析器会将参数视为可选,即使没有设置required = false。

6.4 消息转换器的自动配置

Spring Boot 4.x默认使用Jackson 3作为JSON消息转换器。HttpMessageConvertersAutoConfiguration负责注册消息转换器:

java 复制代码
// 源码路径:org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration
@AutoConfiguration
@ConditionalOnClass(HttpMessageConverter.class)
@Conditional(NotReactiveWebApplicationCondition.class)
public class HttpMessageConvertersAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(
            ObjectMapper objectMapper) {
        MappingJackson2HttpMessageConverter converter = 
            new MappingJackson2HttpMessageConverter(objectMapper);
        return converter;
    }
}

Jackson 3的包名变化 :在Spring Boot 4.x中,Jackson 3的核心类位于tools.jackson包下。例如,ObjectMapper的等价类是tools.jackson.databind.json.JsonMapper。如果你在代码中直接使用了Jackson的API,需要更新import路径。


7. 常见坑点与生产问题排查

坑点1:使用旧的spring-boot-starter-web坐标

报错现象 :在Spring Boot 4.x项目中使用spring-boot-starter-web,IDE提示依赖废弃,或无法找到某些自动配置类。

原因 :Spring Boot 4.0将spring-boot-starter-web重命名为spring-boot-starter-webmvc。

解决方案 :将依赖坐标替换为spring-boot-starter-webmvc,测试依赖替换为spring-boot-starter-webmvc-test。

坑点2:import路径未更新导致编译失败

报错现象 :import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;编译失败。

原因 :Spring Boot 4.x将WebMvc自动配置类迁移到了org.springframework.boot.webmvc.autoconfigure包下。

解决方案 :更新import路径为org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration。如果引用的是其他自动配置类,参考第2课的Starter坐标对照表进行迁移。

坑点3:自定义static-locations覆盖了默认配置

报错现象 :配置spring.web.resources.static-locations后,classpath:/static/下的资源无法访问。

原因 :自定义static-locations会覆盖默认配置,而非追加。默认的四个目录(/META-INF/resources/、/resources/、/static/、/public/)不再生效。

解决方案:在自定义配置中显式列出所有需要的目录,包括默认目录。

坑点4:@RequestBody接收JSON时反序列化失败

报错现象 :HttpMessageNotReadableException,提示无法反序列化JSON。

原因 :Spring Boot 4.x默认使用Jackson 3,包名为tools.jackson。如果请求体中的字段名与DTO不匹配,或DTO使用了Jackson 2的注解(com.fasterxml.jackson),会导致反序列化失败。

解决方案 :将Jackson 2的注解(@JsonProperty、@JsonCreator等)替换为Jackson 3的对应注解,或确保DTO的字段名与JSON键名一致。

坑点5:方法级校验异常未被正确处理

报错现象 :在方法参数上使用@Min、@NotBlank等校验注解时,抛出HandlerMethodValidationException,但全局异常处理器中没有处理该异常。

原因 :Spring MVC中,方法参数上的校验注解触发的是方法级校验 ,抛出HandlerMethodValidationException,而非MethodArgumentNotValidException。

解决方案 :在@RestControllerAdvice中同时处理MethodArgumentNotValidException和HandlerMethodValidationException。

坑点6:静态资源缓存时间过长导致前端更新不生效

报错现象:前端修改了CSS文件,但浏览器仍然加载旧的缓存版本。

原因 :配置了spring.web.resources.cache.period,浏览器长时间缓存静态资源。

解决方案 :启用内容哈希版本化策略(spring.web.resources.chain.strategy.content.enabled=true),这样每次内容变更后URL会变化,浏览器会自动加载新版本。


8. 本章总结

本节课围绕Spring Boot 4.x的Web开发基础,从模块化变化、静态资源处理、参数接收到源码剖析,构建了完整的Web开发知识体系。

模块化变化 :Starter坐标从spring-boot-starter-web变为spring-boot-starter-webmvc,WebMvc自动配置类的包名从org.springframework.boot.autoconfigure.web.servlet迁移到org.springframework.boot.webmvc.autoconfigure。这是4.x Web开发中最需要注意的迁移点。

静态资源处理 :默认支持四个目录(/META-INF/resources/、/resources/、/static/、/public/),优先级从高到低。支持WebJars、内容哈希版本化缓存策略。自定义static-locations时会覆盖默认配置。

五种参数接收方式 :@RequestParam(查询参数)、@PathVariable(路径变量)、@RequestBody(JSON请求体)、@RequestHeader(请求头)、@CookieValue(Cookie)。配合@Valid触发JSR-303校验。

JSpecify空安全 :Spring Framework 7.0使用JSpecify的@Nullable注解标记可选参数,在Kotlin中利用空安全类型系统自动推断required属性。

WebMvcConfigurer:通过实现该接口可以自定义静态资源映射、拦截器、消息转换器等。


9. 课后作业

基础题1

在本节课的webmvc-demo项目中,完成以下任务:

  1. 在classpath:/static/目录下创建一个about.html文件,内容为简单的"关于我们"页面
  2. 在classpath:/public/目录下创建同名文件about.html,内容不同
  3. 启动应用,访问/about.html,记录返回的是哪个目录的文件
  4. 在application.yml中将static-locations改为只保留classpath:/public/,重启应用,再次访问/about.html,记录结果
  5. 解释为什么第一次访问的是static目录的文件,以及修改配置后发生了什么

基础题2

使用Spring Boot 4.x创建一个REST接口GET /api/products,实现以下功能:

  1. 接收三个查询参数:category(String,可选)、minPrice(Double,默认0)、maxPrice(Double,默认Double.MAX_VALUE)
  2. 使用@RequestParam接收参数,为category使用required = false
  3. 返回一个包含参数值和参数类型的JSON响应
  4. 测试以下场景:
    • GET /api/products(不传任何参数)
    • GET /api/products?category=electronics&minPrice=100&maxPrice=500
    • GET /api/products?minPrice=abc(类型转换失败场景)

综合实战题

构建一个完整的Web开发实战项目,要求:

  1. 静态资源:

    • 创建一个index.html首页,包含CSS和JavaScript引用
    • 启用内容哈希版本化缓存策略
    • 配置/uploads/**映射到本地文件系统目录
  2. REST接口:

    • POST /api/articles:创建文章,接收JSON请求体(包含title、content、author、tags数组),使用@Valid校验
    • GET /api/articles:分页查询文章列表,支持page、size、keyword查询参数
    • GET /api/articles/{id}:根据ID获取文章详情
    • PUT /api/articles/{id}:更新文章,接收JSON请求体
    • DELETE /api/articles/{id}:删除文章
  3. 参数校验:

    • title不能为空,长度3-100
    • content不能为空,长度10-5000
    • author不能为空
    • tags最多5个标签
  4. 全局异常处理:

    • 处理MethodArgumentNotValidException(@RequestBody校验失败)
    • 处理HandlerMethodValidationException(方法参数校验失败)
    • 处理HttpMessageNotReadableException(JSON解析失败)
    • 处理资源不存在场景(返回404)
  5. 验证要求:

    • 使用curl测试所有接口
    • 验证参数校验失败时的错误信息是否完整
    • 验证静态资源版本化缓存是否生效
    • 使用--debug模式启动,查看CONDITIONS EVALUATION REPORT中WebMvc相关自动配置的评估结果

扩展挑战 :实现一个WebMvcConfigurer,添加一个拦截器(HandlerInterceptor),在preHandle方法中记录每个请求的URL、HTTP方法和执行时间。拦截器需要排除静态资源路径(/css/**、/js/**、/images/**)。

🔗 SpringBoot 4.x 从入门到企业级实战系列课程

去订阅

相关推荐
paopaokaka_luck1 小时前
流浪动物救助与收养系统(DeepSeek AI对话、 ECharts多维分析、救助流程衔接、领养与回访衔接、 捐赠确认与公示、志愿者协作)
java·前端·javascript·人工智能·mysql·echarts·推荐算法
程序 代码狂人1 小时前
切片表与分区表的区别(数据开发面试版)
java·大数据·开发语言·数据仓库·算法
2501_944521592 小时前
Deepmerge 跨端三方库仓颉适配全流程
java·python·eclipse·github
南归北隐3 小时前
Spring AI Alibaba Graph框架实现MCP数据库查询实战
java·后端·spring ai·mcp·spring ai mcp
古法老登瞎折腾3 小时前
大数据智能运维实战(九):让 AI 查日志——找到异常,也保留上下文
java·大数据·运维·人工智能·spring ai·mcp
ai大模型-探索者3 小时前
Function Call与MCP(区别、联系与实战)
java·服务器·网络
略略略咯咯4 小时前
啊啊啊啊啊
java·服务器·数据库
SL_staff4 小时前
运维救火实战:钉钉API变更致薪资计算中断,如何用可视化逻辑引擎5分钟热修复
java·开源·全栈
猫猫.1534 小时前
HashTable、HashMap、ConcurrentHashMap 核心区别总结
java·开发语言·java-ee