Spring MVC 备忘清单

本备忘清单旨在快速理解 Spring MVC 框架的核心概念,提供了在 Spring Boot 环境下构建 RESTful API 的最常用注解、配置和最佳实践,供您参考。

Web 核心

Spring Boot 与 Spring MVC 的关系

  • Spring MVC: 一个成熟的 Web 框架,提供了构建 Web 应用的全套组件和架构模式。
  • Spring Boot: 一个集成与简化框架。它通过 spring-boot-starter-web 依赖,自动配置了 Spring MVC 的核心组件(如 DispatcherServletHandlerMapping 等),使开发者能专注于业务代码。

pom.xml 关键依赖:

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

第一个 API 接口

@RestController@GetMapping 的组合构成了最基础的 API 接口。

// @RestController 是 @Controller 和 @ResponseBody 的组合注解。
// 1. @Controller:将类声明为 Spring IoC 容器中的一个控制器 Bean。
// 2. @ResponseBody:告知 Spring MVC,此类所有方法返回的都是数据,
//    需要直接写入 HTTP 响应体,通常是 JSON 格式。
@RestController
public class HelloController {

    // @GetMapping("/hello") 将 HTTP GET 请求映射到此方法。
    // 它是 @RequestMapping(method = RequestMethod.GET, path = "/hello") 的简写。
    @GetMapping("/hello")
    public String sayHello() {
        return "Hello, Spring MVC!";
    }
}

DispatcherServlet 核心流程

所有请求都由 DispatcherServlet (前端控制器) 统一调度,流程简化如下:

  1. 接收请求: Tomcat 等服务器将请求转交给 DispatcherServlet
  2. 查找处理器: DispatcherServlet 询问 HandlerMapping (处理器映射器),找到能处理当前请求的 Controller 方法。
  3. 调用方法: 通过 HandlerAdapter (处理器适配器) 调用目标 Controller 方法。
  4. 处理返回: Controller 方法执行并返回数据。
  5. 写入响应: HttpMessageConverter (消息转换器) 将返回的 Java 对象序列化为 JSON 等格式,写入 HTTP 响应体。

请求映射 @RequestMapping

组合映射

在类级别和方法级别上组合使用 @RequestMapping,为一组接口定义公共的父路径。

@RestController
@RequestMapping("/users") // 定义父路径
public class UserController {

    // 完整访问路径: /users/all
    @GetMapping("/all")
    public String getAllUsers() {
        return "返回所有用户列表";
    }

    // 完整访问路径: /users/{id}
    @GetMapping("/{id}")
    public String getUserById(@PathVariable Long id) {
        return "查询用户: " + id;
    }
}

路径变量 @PathVariable

用于从 URL 路径中提取动态值。

/**
 * {id} 是一个路径占位符。
 * @PathVariable("id") 会将 URL 中占位符 {id} 的实际值,
 * 绑定到方法的 Long id 参数上。
 * 如果方法参数名与占位符名称相同,可以简写为 @PathVariable Long id。
 */
@GetMapping("/{id}")
public String getUserById(@PathVariable("id") Long id) {
    return "正在查询 ID 为: " + id + " 的用户";
}

模糊匹配 (Ant 风格)

@RequestMapping 支持使用 Ant 风格的通配符进行模糊路径匹配。

  • ?: 匹配任意单个字符。
  • *: 匹配任意数量的字符 (不含 /)。
  • **: 匹配任意数量的字符 (可含 /)。
// 匹配 /ant/testA, /ant/testB
@GetMapping("/ant/test?")
public String testAnt1() { return "Ant-style: ?"; }

// 匹配 /ant/testABC, 但不匹配 /ant/test/abc
@GetMapping("/ant/test*")
public String testAnt2() { return "Ant-style: *"; }

// 匹配 /ant/any/path
@GetMapping("/ant/**")
public String testAnt3() { return "Ant-style: **"; }

重要: 在 Spring Boot 3.x 中,出于安全考虑,不推荐在路径中间使用 **

精准匹配 (params & headers)

根据请求中是否包含特定参数或请求头来路由请求。

// 只有当请求 URL 中包含参数 "version" 时才匹配
// 例如: /precise?version=1
@GetMapping(value = "/precise", params = "version")
public String testParams() {
    return "Match with param 'version'";
}

// 只有当请求头中包含 "X-API-VERSION" 时才匹配
@GetMapping(value = "/precise", headers = "X-API-VERSION")
public String testHeaders() {
    return "Match with header 'X-API-VERSION'";
}

获取请求参数

URL 参数 @RequestParam

用于获取 URL 查询参数 (? 之后的部分)。

@GetMapping("/search")
public String searchUsers(
    @RequestParam("keyword") String keyword,
    @RequestParam(value = "page", required = false, defaultValue = "1") Integer page) {
    
    return "关键词: " + keyword + ", 页码: " + page;
}
  • value: 参数名。
  • required: 是否必需,默认为 true
  • defaultValue: 默认值。

请求体数据 @RequestBody

用于将请求体中的 JSON 或 XML 数据反序列化为 Java 对象 (POJO)。

// 1. 定义一个 DTO (Data Transfer Object)
@Data
public class UserSaveDTO {
    private String username;
    private String password;
}

// 2. 在 Controller 方法中使用 @RequestBody
@PostMapping("/create")
public String createUser(@RequestBody UserSaveDTO user) {
    return "成功创建用户: " + user.toString();
}

一个方法中,@RequestBody 注解最多只能使用一次。

POJO 自动封装

当方法参数是一个没有 @RequestBody 注解的 POJO 时,Spring MVC 会自动将同名的 URL 查询参数或表单参数绑定到 POJO 的属性上。

// 请求 URL: /filter?username=lisi&email=lisi@example.com
@GetMapping("/filter")
public String filterUsers(UserQuery query) {
    // Spring MVC 会自动将请求参数设置到 query 对象中
    return "根据条件筛选用户: " + query.toString();
}

// UserQuery.java
@Data
public class UserQuery {
    private String username;
    private String email;
}

其他参数注解

注解作用示例
@PathVariable从 URL 路径 (/users/{id}) 中获取@PathVariable Long id
@RequestHeader请求头 (Headers) 中获取@RequestHeader("User-Agent") String ua
@CookieValueCookie 中获取@CookieValue("session-id") String sid

分层架构

领域对象模型

为了实现各层解耦和保障数据安全,推荐使用不同的对象模型。

类型全称约定包名核心职责
POPersistent Objectentity持久化对象。与数据库表结构一一对应。
DTOData Transfer Objectdto数据传输对象。用于接收前端传递的请求数据。
VOView Objectvo视图对象。用于返回给前端的展示数据,可隐藏敏感字段。
QOQuery Objectdto/query查询对象。一种特殊的DTO,用于封装复杂的查询条件。

对象转换

在 Service 层进行 PO、DTO、VO 之间的转换是核心业务之一。

场景: 将 User (PO) 转换为 UserVO (VO)。

// PO
@Data
public class User {
    private Long id;
    private String username;
    private Integer status; // 1-正常, 2-禁用
}

// VO
@Data
public class UserVO {
    private Long id;
    @Alias("username") // Hutool 注解,解决 username -> name 的名称不一致问题
    private String name;
    private String statusText;
}

// Service 层转换逻辑
private UserVO convertToVO(User user) {
    UserVO userVO = new UserVO();
    // 1. 使用 BeanUtil 拷贝同名/有@Alias注解的属性
    BeanUtil.copyProperties(user, userVO);
    // 2. 手动处理需要逻辑转换的属性
    if (user.getStatus() != null) {
        userVO.setStatusText(user.getStatus() == 1 ? "正常" : "已禁用");
    }
    return userVO;
}

三层架构

标准的后端项目通常采用三层架构,职责清晰。

. 📂 src/main/java/com/example
├── 📂 controller      <- Controller层: Web入口, 参数校验, 调用Service
├── 📂 service         <- Service层: 业务核心, 事务管理, 组合Mapper
│   └── 📂 impl
└── 📂 mapper          <- Mapper层: 数据持久层, 与数据库交互

数据校验 Validation

引入依赖

pom.xml 中必须添加 validation 启动器。

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

在 DTO 中添加注解

使用 jakarta.validation.constraints.* 下的注解为 DTO 字段添加校验规则。

@Data
public class UserSaveDTO {
    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;
}

常用校验注解

注解作用
@NotNull验证对象不为 null
@NotBlank验证字符串不为 null 且不为空白
@NotEmpty验证集合、数组、字符串不为 null 且长度 > 0
@Size验证大小或长度在指定范围内
@Min / @Max验证数字在最小值/最大值之间
@Email验证字符串为合法的邮箱格式
@Pattern使用正则表达式验证字符串

激活校验

在 Controller 方法中,使用 @Validated 注解激活对 DTO 的校验。

@RestController
@Validated // 可在类上添加,以支持方法级别的参数校验
public class UserController {
    @PostMapping("/users")
    public void saveUser(@Validated @RequestBody UserSaveDTO dto) {
        // ... 如果校验失败,Spring MVC会抛出 MethodArgumentNotValidException
    }
}

校验失败的异常,应由全局异常处理器统一捕获并返回友好的错误信息。

分组校验

使用 @Validated 的分组功能,可以用一个 DTO 应对不同场景(如新增、修改)的校验规则。

  1. 定义分组接口:
    public interface ValidationGroups {
        interface Save {}
        interface Update {}
    }
    
  2. 在 DTO 中使用 groups 属性:
    public class UserEditDTO {
        @NotNull(groups = ValidationGroups.Update.class)
        private Long id;
        @NotBlank(groups = ValidationGroups.Save.class)
        private String username;
    }
    
  3. 在 Controller 中指定分组:
    @PostMapping
    public void saveUser(@Validated(ValidationGroups.Save.class) @RequestBody UserEditDTO dto) {}
    
    @PutMapping
    public void updateUser(@Validated(ValidationGroups.Update.class) @RequestBody UserEditDTO dto) {}
    

高级特性

全局异常处理

使用 @RestControllerAdvice@ExceptionHandler 统一处理全局异常,返回标准 Result 结构。

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 处理自定义业务异常
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<Result<Void>> handleBusinessException(BusinessException ex) {
        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(Result.error(ex.getResultCode()));
    }

    // 处理参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Result<Void>> handleValidationException(MethodArgumentNotValidException ex) {
        // 拼接错误信息
        String message = ex.getBindingResult().getFieldErrors().stream()
                .map(e -> e.getField() + ": " + e.getDefaultMessage())
                .collect(Collectors.joining("; "));
        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(Result.error(message));
    }

    // 兜底处理所有未知异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Result<Void>> handleUnknownException(Exception ex) {
        log.error("系统未知异常", ex);
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Result.error("系统异常,请联系管理员"));
    }
}

全局跨域配置 (CORS)

通过实现 WebMvcConfigurer 接口进行全局 CORS 配置,是解决跨域问题的最佳实践。

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 对所有路径生效
                .allowedOrigins("http://localhost:5173") // 允许的前端源
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true) // 允许发送 Cookie
                .maxAge(3600);
    }
}

文件上传

  1. 配置 (application.yml):
    spring:
      servlet:
        multipart:
          enabled: true
          max-file-size: 2MB
    app:
      upload:
        dir: D:/uploads/ # 上传目录
    
  2. Controller:
    @PostMapping("/upload")
    public Result<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
        String originalFilename = file.getOriginalFilename();
        // ... 生成唯一文件名 ...
        String newFileName = generateUniqueName(originalFilename);
        // ... 保存文件 ...
        file.transferTo(new File(uploadDir, newFileName));
        return Result.success(newFileName);
    }
    

文件下载与预览

  1. 静态资源映射 (WebConfig.java): 将 URL 路径映射到物理文件目录,以支持浏览器直接预览。

    @Value("${app.upload.dir}")
    private String uploadDir;
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/uploads/**")
                .addResourceLocations("file:" + uploadDir);
    }
    
  2. 强制下载 Controller:

    @GetMapping("/download/{filename}")
    public ResponseEntity<Resource> download(@PathVariable String filename) {
        File file = new File(uploadDir, filename);
        if (!file.exists()) {
            return ResponseEntity.notFound().build();
        }
        Resource resource = new FileSystemResource(file);
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
                .body(resource);
    }
    

API 安全

拦截器 (HandlerInterceptor)

拦截器允许在请求处理流程的关键节点执行通用逻辑,是实现权限校验的核心。

  1. 创建拦截器: 实现 HandlerInterceptor 接口。
    @Component
    public class AuthInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            // 校验逻辑...
            // 返回 true 放行,返回 false 拦截
            return true; 
        }
    }
    
  2. 注册拦截器 (WebConfig.java):
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/**") // 拦截所有路径
                .excludePathPatterns("/login", "/register"); // 排除特定路径
    }
    

JWT Token 认证

JWT (JSON Web Token) 是现代 API 中主流的无状态认证方案。

认证流程:

  1. 登录: 用户提供凭证调用登录接口。
  2. 签发 Token: 服务器验证成功后,生成一个包含用户信息的 JWT Token 并返回给客户端。
  3. 携带 Token: 客户端在后续每次请求的 Authorization 请求头中携带 Token (格式: Bearer <token>)。
  4. 拦截校验: 服务器端的 AuthInterceptor 拦截请求,验证 Token 的合法性。

登录接口 (签发 Token)

@PostMapping("/login")
public Result<String> login(@RequestBody LoginDTO dto) {
    // 1. 验证用户名密码...
    // 2. 验证成功,生成 Token
    Map<String, Object> payload = new HashMap<>();
    payload.put("userId", userId);
    payload.put("username", username);
    payload.put(JWTPayload.EXPIRES_AT, System.currentTimeMillis() + 1000 * 60 * 60 * 24); // 24小时过期

    String token = JWTUtil.createToken(payload, jwtSecret.getBytes());
    return Result.success(token);
}

拦截器 (校验 Token)

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    String token = request.getHeader("Authorization");
    if (StrUtil.isBlank(token) || !token.startsWith("Bearer ")) {
        throw new BusinessException(ResultCode.UNAUTHORIZED);
    }
    token = token.substring(7);

    // 使用 Hutool-JWT 验证 Token
    if (!JWTUtil.verify(token, jwtSecret.getBytes())) {
        throw new BusinessException(ResultCode.UNAUTHORIZED);
    }
    // 还可以进一步校验payload中的过期时间等
    JWTValidator.of(token).validateDate();

    return true;
}

SpringDoc 集成 JWT

让 Swagger UI 支持 Token 认证,方便测试。

  1. 定义安全方案:
    @Configuration
    @SecurityScheme(
        name = "bearerAuth", // 方案名称
        type = SecuritySchemeType.HTTP,
        scheme = "bearer",
        bearerFormat = "JWT"
    )
    public class SpringDocConfig {}
    
  2. 应用安全方案: 在需要认证的 Controller 上添加 @SecurityRequirement
    @RestController
    @RequestMapping("/users")
    @SecurityRequirement(name = "bearerAuth")
    public class UserController {
        // ...
    }
    

HttpMessageConverter 工作原理

核心概念

HttpMessageConverter (HTTP 消息转换器) 是 Spring MVC 的核心组件,负责在 HTTP 请求/响应体与 Java 对象之间进行序列化和反序列化。

  • @RequestBody: 触发反序列化HttpMessageConverter 调用 read() 方法,将请求体 (如 JSON) 转换为 Java 对象 (DTO)。
  • @ResponseBody: 触发序列化HttpMessageConverter 调用 write() 方法,将 Java 对象 (VO) 转换为响应体 (如 JSON)。

关键方法

方法作用
canRead() / canWrite()能力检测。判断转换器是否能处理指定的 Java 类型和媒体类型 (MIME Type)。
read() / write()执行转换。进行实际的读写操作。

常见实现

实现类核心职责
StringHttpMessageConverter处理 text/plain 类型的纯字符串。
MappingJackson2HttpMessageConverter绝对主力。处理 application/json,依赖 Jackson 库。
ByteArrayHttpMessageConverter处理 application/octet-stream 等二进制数据。
FormHttpMessageConverter处理 application/x-www-form-urlencoded 表单数据。

内容协商 (Content Negotiation)

内容协商是 Spring MVC 从众多转换器中智能选择一个的机制。

协商依据

  • 请求体 (Request): 依据请求头 Content-Type。它告诉服务器请求体的数据格式。
  • 响应体 (Response): 依据请求头 Accept。它告诉服务器客户端期望接收的数据格式。

选择流程 (@ResponseBody)

  1. Controller 方法执行后返回一个 Java 对象 (如 UserVO)。
  2. Spring MVC 启动内容协商,获取请求的 Accept 头 (如 application/json)。
  3. 遍历所有已注册的 HttpMessageConverter
  4. 对每个转换器调用 canWrite(UserVO.class, "application/json") 进行“招标”。
  5. MappingJackson2HttpMessageConverter 判断自己能处理 UserVOapplication/json,返回 true
  6. Spring MVC 锁定该转换器,停止遍历。
  7. 调用其 write() 方法,将 UserVO 序列化为 JSON 字符串并写入响应体。