文章目录
-
- [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. 知识目标
学完本节课,你必须掌握以下核心知识点:
- 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的包名变化。 - WebMvcAutoConfiguration的核心能力:内容协商、静态资源映射、消息转换器、视图解析器的自动配置逻辑。
- 静态资源处理 :默认的五个静态资源目录及其优先级、
spring.web.resources.static-locations的自定义配置、WebJars支持、版本化资源的缓存策略。 - 五种请求参数接收方式 :
@RequestParam(查询参数)、@PathVariable(路径变量)、@RequestBody(JSON请求体)、@RequestHeader(请求头)、@CookieValue(Cookie)的用法与适用场景。 - JSpecify空安全参数绑定 :
@Nullable注解在可选参数上的使用,以及HandlerMethodValidationException的处理。 - 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实现。
核心流程:
RequestMappingHandlerAdapter调用invokeHandlerMethod()处理请求ServletInvocableHandlerMethod#invokeForRequest()遍历所有方法参数- 对每个参数,从
argumentResolvers列表中查找支持该参数的解析器 - 调用解析器的
resolveArgument()方法,从请求中提取参数值 - 将解析后的参数数组传递给目标方法
@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项目中,完成以下任务:
- 在
classpath:/static/目录下创建一个about.html文件,内容为简单的"关于我们"页面 - 在
classpath:/public/目录下创建同名文件about.html,内容不同 - 启动应用,访问
/about.html,记录返回的是哪个目录的文件 - 在
application.yml中将static-locations改为只保留classpath:/public/,重启应用,再次访问/about.html,记录结果 - 解释为什么第一次访问的是
static目录的文件,以及修改配置后发生了什么
基础题2
使用Spring Boot 4.x创建一个REST接口GET /api/products,实现以下功能:
- 接收三个查询参数:
category(String,可选)、minPrice(Double,默认0)、maxPrice(Double,默认Double.MAX_VALUE) - 使用
@RequestParam接收参数,为category使用required = false - 返回一个包含参数值和参数类型的JSON响应
- 测试以下场景:
GET /api/products(不传任何参数)GET /api/products?category=electronics&minPrice=100&maxPrice=500GET /api/products?minPrice=abc(类型转换失败场景)
综合实战题
构建一个完整的Web开发实战项目,要求:
-
静态资源:
- 创建一个
index.html首页,包含CSS和JavaScript引用 - 启用内容哈希版本化缓存策略
- 配置
/uploads/**映射到本地文件系统目录
- 创建一个
-
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}:删除文章
-
参数校验:
title不能为空,长度3-100content不能为空,长度10-5000author不能为空tags最多5个标签
-
全局异常处理:
- 处理
MethodArgumentNotValidException(@RequestBody校验失败) - 处理
HandlerMethodValidationException(方法参数校验失败) - 处理
HttpMessageNotReadableException(JSON解析失败) - 处理资源不存在场景(返回404)
- 处理
-
验证要求:
- 使用curl测试所有接口
- 验证参数校验失败时的错误信息是否完整
- 验证静态资源版本化缓存是否生效
- 使用
--debug模式启动,查看CONDITIONS EVALUATION REPORT中WebMvc相关自动配置的评估结果
扩展挑战 :实现一个WebMvcConfigurer,添加一个拦截器(HandlerInterceptor),在preHandle方法中记录每个请求的URL、HTTP方法和执行时间。拦截器需要排除静态资源路径(/css/**、/js/**、/images/**)。