以下是为您精心撰写的 《Spring MVC 深度系统性解析:架构、原理、注解、校验、异常、文件与拦截器》 完整说明文档,涵盖您提出的所有核心内容,并附带一个真实可落地、企业级参考的综合性实战示例。
本指南不仅解释“是什么”,更深入剖析“为什么”和“如何用”,帮助您从“会用注解”进阶到“理解 Spring MVC 设计哲学”。
📜 Spring MVC 深度系统性解析:架构、原理、注解、校验、异常、文件与拦截器
目标:全面掌握 Spring MVC 的核心机制、处理流程、注解体系与实战设计,构建企业级 Web 应用能力
✅ 一、Spring MVC 是什么?它有什么作用?
1.1 定义
Spring MVC(Model-View-Controller) 是 Spring 框架中用于构建 Web 应用程序 的模块,它是一个基于 Servlet API 的、轻量级、松耦合、高度可扩展 的 Web 框架。
它实现了经典的 MVC 设计模式,将 Web 请求的处理分为三个核心组件:
| 组件 | 职责 |
|---|---|
| Model(模型) | 业务数据和状态(如 User, Order 对象) |
| View(视图) | 数据展示层(如 JSP、Thymeleaf、JSON、XML) |
| Controller(控制器) | 接收请求、调用服务、返回模型和视图名称 |
1.2 核心作用
| 作用 | 说明 |
|---|---|
| ✅ 统一请求处理 | 将 HTTP 请求映射到 Java 方法,替代原始 Servlet 的 doGet/doPost |
| ✅ 参数自动绑定 | 自动将请求参数(Query、Body、Header)绑定到方法参数或对象 |
| ✅ 数据格式化与转换 | 自动将字符串转为 Date、Integer、枚举等类型 |
| ✅ 视图解析 | 根据返回值自动选择视图(如跳转 JSP 或返回 JSON) |
| ✅ 数据校验 | 集成 JSR-303/JSR-380,实现声明式参数校验 |
| ✅ 全局异常处理 | 统一捕获并格式化异常,避免暴露敏感信息 |
| ✅ 文件上传下载 | 提供 MultipartFile 和 Resource 等便捷 API |
| ✅ 拦截器机制 | 在请求处理前后插入自定义逻辑(如登录校验、日志) |
| ✅ RESTful 支持 | 原生支持 @RestController,构建 JSON API |
✅ 一句话总结:
Spring MVC 让你用写 Java 方法的方式,来处理 HTTP 请求,彻底告别原始 Servlet 的样板代码。
✅ 二、Spring MVC 的实现原理与核心架构
2.1 核心架构图(简化版)
客户端 (Browser)
↓
[HTTP Request]
↓
┌───────────────────────┐
│ DispatcherServlet │ ← Spring MVC 的“总入口”(前端控制器)
└──────────┬────────────┘
↓
┌───────────────────────┐
│ HandlerMapping │ ← 根据 URL 找到对应的 Controller 方法
└──────────┬────────────┘
↓
┌───────────────────────┐
│ Controller │ ← 执行业务逻辑,返回 Model & ViewName
└──────────┬────────────┘
↓
┌───────────────────────┐
│ ViewResolver │ ← 根据 ViewName 找到实际视图(如 Thymeleaf 模板)
└──────────┬────────────┘
↓
┌───────────────────────┐
│ View │ ← 渲染数据,生成 HTML/JSON/XML
└──────────┬────────────┘
↓
┌───────────────────────┐
│ HTTP Response │ ← 返回给客户端
└───────────────────────┘
2.2 核心组件详解
| 组件 | 作用 | 关键类 |
|---|---|---|
| DispatcherServlet | 前端控制器,所有请求的入口,负责协调其他组件 | org.springframework.web.servlet.DispatcherServlet |
| HandlerMapping | 映射请求 URL 到具体的 Controller 方法 | RequestMappingHandlerMapping(最常用) |
| HandlerAdapter | 调用 Controller 方法,适配不同类型的控制器 | RequestMappingHandlerAdapter |
| Controller | 业务逻辑处理器,处理请求并返回模型和视图 | @Controller, @RestController |
| ViewResolver | 将逻辑视图名解析为物理视图(模板文件) | ThymeleafViewResolver, JsonView |
| View | 渲染模型数据,生成最终响应内容 | ThymeleafTemplate, MappingJackson2JsonView |
| HandlerInterceptor | 拦截器,在请求处理前后执行自定义逻辑 | 实现 HandlerInterceptor 接口 |
✅ 关键设计:
“前端控制器 + 多处理器”模式 —— 一个DispatcherServlet统一入口,多个HandlerMapping和HandlerAdapter分别处理不同类型的请求,实现高度解耦。
✅ 三、Spring MVC 请求处理流程(详细步骤)
✅ 重点:
- 所有请求都经过
DispatcherServlet,这是 Spring MVC 的“心脏”。@Controller方法不是直接被调用,而是通过HandlerAdapter动态反射调用。- 拦截器在步骤 2 和 10 之间执行(
preHandle、postHandle、afterCompletion)。
✅ 四、常用请求注解详解(请求映射与参数处理)
| 注解 | 类型 | 作用 | 示例 | 说明 |
|---|---|---|---|---|
@RequestMapping | 映射 | 通用请求映射 | @RequestMapping("/users") | 可指定 method, params, headers |
@GetMapping | 映射 | GET 请求映射 | @GetMapping("/users/{id}") | 等价于 @RequestMapping(method = GET) |
@PostMapping | 映射 | POST 请求映射 | @PostMapping("/users") | |
@PutMapping | 映射 | PUT 请求映射 | @PutMapping("/users/{id}") | |
@DeleteMapping | 映射 | DELETE 请求映射 | @DeleteMapping("/users/{id}") | |
@PatchMapping | 映射 | PATCH 请求映射 | @PatchMapping("/users/{id}") | 部分更新 |
@RequestParam | 参数 | 绑定查询参数或表单字段 | @RequestParam("name") String name | 必须存在,否则报错 |
@RequestParam(required = false) | 参数 | 可选参数 | @RequestParam(value = "age", required = false) Integer age | 值为 null 时不会报错 |
@RequestParam(defaultValue = "0") | 参数 | 默认值 | @RequestParam(defaultValue = "0") int page | 参数缺失时使用默认值 |
@PathVariable | 参数 | 绑定 URL 路径变量 | @PathVariable("id") Long id | 如 /users/1001 → id=1001 |
@RequestBody | 参数 | 绑定请求体(JSON/XML) | @RequestBody User user | 需配合 Content-Type: application/json |
@RequestHeader | 参数 | 绑定请求头 | @RequestHeader("Authorization") String token | |
@CookieValue | 参数 | 绑定 Cookie | @CookieValue("JSESSIONID") String sessionId | |
@ModelAttribute | 参数 | 绑定表单对象或预填充数据 | @ModelAttribute User user | 通常用于表单提交 |
@SessionAttribute | 参数 | 获取 Session 中的属性 | @SessionAttribute("user") User user | 需先存入 HttpSession |
@RequestPart | 参数 | 绑定 multipart 文件或表单字段 | @RequestPart("file") MultipartFile file | 用于文件上传 |
✅ 最佳实践:
- REST API 推荐:
@RestController+@GetMapping/@PostMapping+@RequestBody+@PathVariable- 表单提交推荐:
@Controller+@PostMapping+@ModelAttribute- 避免字段注入:永远使用
@RequestParam、@PathVariable显式绑定,不要直接用@Autowired注入请求参数
✅ 五、请求数据校验:JSR-303/JSR-380 + 自定义校验
5.1 什么是 JSR-303 / JSR-380?
- JSR-303:Java EE 6 标准,定义了 Bean 校验的 API(
javax.validation) - JSR-380:Java EE 8 标准,JSR-303 的升级版(
jakarta.validation),Spring Boot 2.3+ 默认使用 - Hibernate Validator:JSR-380 的官方参考实现
5.2 常用注解
| 注解 | 说明 | 示例 |
|---|---|---|
@NotNull | 非空(对象不为 null) | @NotNull private String name; |
@NotEmpty | 非空且长度 > 0(字符串、集合、数组) | @NotEmpty private List<String> roles; |
@NotBlank | 非空、非空白(字符串,去除空格后不为空) | @NotBlank private String email; |
@Size(min=2, max=10) | 长度校验 | @Size(min=6, max=20) private String password; |
@Email | 邮箱格式校验 | @Email private String email; |
@Min(18) | 数值 ≥ 最小值 | @Min(18) private Integer age; |
@Max(100) | 数值 ≤ 最大值 | @Max(100) private Integer score; |
@Pattern(regexp = "^[0-9]{11}$") | 正则校验 | @Pattern(regexp = "^[1-9]\\d{10}$") private String phone; |
@Valid | 嵌套校验 | @Valid private User user; |
5.3 使用方式
✅ 步骤1:在 Controller 方法上使用 @Valid
@PostMapping("/users")
public ResponseEntity<String> createUser(@Valid @RequestBody UserCreateRequest request, BindingResult bindingResult) {
// ✅ 如果校验失败,bindingResult 会包含错误信息
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body("校验失败:" + bindingResult.getAllErrors());
}
userService.save(request);
return ResponseEntity.ok("用户创建成功");
}
✅ 步骤2:在 DTO 类上标注校验注解
package com.example.dto;
import jakarta.validation.constraints.*;
import lombok.Data;
@Data
public class UserCreateRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在 2~20 之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码至少 6 位")
private String password;
@Min(value = 18, message = "年龄不能小于 18 岁")
@Max(value = 150, message = "年龄不能大于 150 岁")
private Integer age;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}
✅ 注意:
@Valid必须加在 方法参数 上,才能触发校验BindingResult必须紧跟在@Valid参数后,否则会抛出MethodArgumentNotValidException
5.4 自定义校验注解(高级)
✅ 步骤1:定义注解
package com.example.validator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.FIELD}) // 只能用于字段
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
@Constraint(validatedBy = MobileValidator.class) // 指定校验器
public @interface ValidMobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
✅ 步骤2:实现校验器
package com.example.validator;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
public class MobileValidator implements ConstraintValidator<ValidMobile, String> {
private static final String MOBILE_PATTERN = "^1[3-9]\\d{9}$";
private Pattern pattern;
@Override
public void initialize(ValidMobile constraintAnnotation) {
// 初始化(可选)
this.pattern = Pattern.compile(MOBILE_PATTERN);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果为 null,不校验(由 @NotNull 处理)
if (value == null || value.trim().isEmpty()) {
return true;
}
return pattern.matcher(value).matches();
}
}
✅ 步骤3:使用自定义注解
@ValidMobile(message = "请输入正确的中国大陆手机号")
private String phone;
✅ 优势:复用性强,团队统一规范,校验逻辑集中管理。
✅ 六、全局异常处理:@ExceptionHandler + @RestControllerAdvice
6.1 为什么需要全局异常处理?
- 避免每个 Controller 都写
try-catch - 统一返回格式(如
{code: 200, msg: "ok", data: {}}) - 防止敏感信息泄露(如数据库异常、堆栈信息)
- 统一记录错误日志
6.2 核心注解
| 注解 | 作用 | 适用场景 |
|---|---|---|
@ExceptionHandler | 方法级异常处理器 | 仅对当前 Controller 生效 |
@ControllerAdvice | 全局异常处理器(普通 Web) | 处理所有 Controller 的异常 |
@RestControllerAdvice | 全局 REST 异常处理器(推荐) | 处理所有 @RestController 的异常,自动加 @ResponseBody |
6.3 实战:统一响应格式 + 全局异常处理
package com.example.exception;
import com.example.dto.CommonResult;
import jakarta.validation.ConstraintViolationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* ✅ 全局 REST 异常处理器(推荐)
* 作用:统一返回格式、捕获所有异常、记录日志、避免堆栈暴露
*/
@RestControllerAdvice // ✅ 等价于 @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* ✅ 处理方法参数校验失败(@Valid 失败)
* 触发条件:@RequestBody 或 @ModelAttribute 上有 @Valid,但校验失败
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<CommonResult<?>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.warn("参数校验失败:{}", e.getMessage());
Map<String, String> errors = new HashMap<>();
for (FieldError error : e.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
CommonResult<?> result = CommonResult.fail(400, "参数校验失败", errors);
return ResponseEntity.badRequest().body(result);
}
/**
* ✅ 处理 JSR-380 单字段校验失败(如 @ValidMobile)
* 触发条件:@Valid 作用于单个对象,但校验失败
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<CommonResult<?>> handleConstraintViolationException(ConstraintViolationException e) {
log.warn("参数校验失败(ConstraintViolation):{}", e.getMessage());
Map<String, String> errors = new HashMap<>();
e.getConstraintViolations().forEach(violation ->
errors.put(violation.getPropertyPath().toString(), violation.getMessage())
);
CommonResult<?> result = CommonResult.fail(400, "参数校验失败", errors);
return ResponseEntity.badRequest().body(result);
}
/**
* ✅ 处理绑定异常(如类型转换失败:字符串传给 Integer)
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<CommonResult<?>> handleBindException(BindException e) {
log.warn("参数绑定失败:{}", e.getMessage());
Map<String, String> errors = new HashMap<>();
for (FieldError error : e.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
CommonResult<?> result = CommonResult.fail(400, "参数绑定失败", errors);
return ResponseEntity.badRequest().body(result);
}
/**
* ✅ 处理自定义业务异常(如用户不存在、余额不足)
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<CommonResult<?>> handleBusinessException(BusinessException e) {
log.warn("业务异常:{}", e.getMessage());
CommonResult<?> result = CommonResult.fail(e.getCode(), e.getMessage(), null);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
/**
* ✅ 处理其他所有未捕获的异常(如空指针、数据库连接失败)
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<CommonResult<?>> handleGenericException(Exception e) {
log.error("系统内部错误", e);
CommonResult<?> result = CommonResult.fail(500, "系统内部错误,请稍后再试", null);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
6.4 统一响应封装类
package com.example.dto;
import lombok.Data;
/**
* 统一响应格式(前后端约定)
* {
* "code": 200,
* "msg": "成功",
* "data": { ... }
* }
*/
@Data
public class CommonResult<T> {
private Integer code;
private String msg;
private T data;
public static <T> CommonResult<T> success(T data) {
return new CommonResult<>(200, "成功", data);
}
public static <T> CommonResult<T> fail(Integer code, String msg, T data) {
CommonResult<T> result = new CommonResult<>();
result.code = code;
result.msg = msg;
result.data = data;
return result;
}
public static <T> CommonResult<T> fail(String msg, T data) {
return fail(500, msg, data);
}
public static <T> CommonResult<T> fail(String msg) {
return fail(500, msg, null);
}
// 构造器
public CommonResult() {}
public CommonResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
6.5 自定义业务异常
package com.example.exception;
public class BusinessException extends RuntimeException {
private final Integer code;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public Integer getCode() {
return code;
}
}
✅ 使用场景:
if (user == null) { throw new BusinessException(1001, "用户不存在"); }
✅ 最终返回格式:
{ "code": 1001, "msg": "用户不存在", "data": null }
✅ 七、文件上传与下载
7.1 文件上传:MultipartFile
✅ 前端表单(HTML)
<form method="post" enctype="multipart/form-data" action="/upload">
<input type="file" name="file" required>
<input type="submit" value="上传">
</form>
✅ 后端 Controller
@PostMapping("/upload")
public ResponseEntity<CommonResult<String>> uploadFile(@RequestParam("file") MultipartFile file) {
// ✅ 校验文件
if (file.isEmpty()) {
return ResponseEntity.badRequest().body(CommonResult.fail("文件不能为空"));
}
// ✅ 校验文件类型(仅允许图片)
String contentType = file.getContentType();
if (!contentType.startsWith("image/")) {
return ResponseEntity.badRequest().body(CommonResult.fail("仅支持图片文件"));
}
// ✅ 校验文件大小(最大 5MB)
if (file.getSize() > 5 * 1024 * 1024) {
return ResponseEntity.badRequest().body(CommonResult.fail("文件大小不能超过 5MB"));
}
// ✅ 生成唯一文件名(防止重名)
String originalFilename = file.getOriginalFilename();
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = System.currentTimeMillis() + extension;
// ✅ 保存到服务器(生产环境建议使用 OSS、MinIO)
try {
file.transferTo(new File("/opt/uploads/" + fileName));
} catch (Exception e) {
log.error("文件上传失败", e);
return ResponseEntity.status(500).body(CommonResult.fail("文件上传失败"));
}
// ✅ 返回访问路径
String url = "http://localhost:8080/uploads/" + fileName;
return ResponseEntity.ok(CommonResult.success(url));
}
7.2 文件下载:Resource + InputStreamResource
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
File file = new File("/opt/uploads/" + fileName);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
try {
// ✅ 创建资源对象
Resource resource = new InputStreamResource(new FileInputStream(file));
// ✅ 设置响应头
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(file.length())
.body(resource);
} catch (Exception e) {
log.error("文件下载失败", e);
return ResponseEntity.status(500).build();
}
}
✅ 生产建议:
- 文件上传路径使用 独立存储(如 MinIO、阿里云 OSS)
- 下载使用 Nginx 静态代理,避免 Java 进程处理大文件
- 文件名做 URL 编码 防止中文乱码
✅ 八、拦截器(Interceptor):登录校验、日志、性能监控
8.1 实现 HandlerInterceptor
package com.example.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.UUID;
/**
* ✅ 登录校验拦截器 + 性能监控 + 请求日志
*/
@Component
public class AuthInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(AuthInterceptor.class);
/**
* ✅ 在 Controller 方法执行前调用
* 返回 true:继续执行
* 返回 false:中断请求,不执行 Controller
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
long startTime = System.currentTimeMillis();
String requestId = UUID.randomUUID().toString();
request.setAttribute("requestId", requestId); // 传递给后续组件
// ✅ 1. 记录请求日志
String uri = request.getRequestURI();
String method = request.getMethod();
String ip = getClientIpAddress(request);
log.info("[REQUEST] {} {} | IP: {} | ID: {}", method, uri, ip, requestId);
// ✅ 2. 登录校验:检查 Token(简化示例)
String token = request.getHeader("Authorization");
if (token == null || !token.equals("admin-token")) {
response.setStatus(401);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未登录或Token无效\"}");
return false; // ✅ 中断请求
}
// ✅ 3. 记录开始时间
request.setAttribute("startTime", startTime);
return true; // ✅ 放行
}
/**
* ✅ 在 Controller 方法执行后、视图渲染前调用
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, org.springframework.web.servlet.ModelAndView modelAndView) throws Exception {
// 可修改 Model 或 View
}
/**
* ✅ 在请求处理完成后调用(无论是否异常)
* 用于记录耗时、清理资源
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
Long startTime = (Long) request.getAttribute("startTime");
String requestId = (String) request.getAttribute("requestId");
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
log.info("[RESPONSE] 耗时: {}ms | ID: {}", duration, requestId);
}
if (ex != null) {
log.error("[ERROR] 请求处理异常 | ID: {}", requestId, ex);
}
}
// 获取真实客户端 IP
private String getClientIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
int index = ip.indexOf(",");
if (index != -1) {
return ip.substring(0, index);
}
return ip;
}
ip = request.getHeader("X-Real-IP");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
return ip;
}
return request.getRemoteAddr();
}
}
8.2 注册拦截器
package com.example.config;
import com.example.interceptor.AuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 配置拦截器
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// ✅ 注册拦截器
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**") // ✅ 拦截 /api/ 开头的所有请求
.excludePathPatterns("/api/login", "/api/register"); // ✅ 排除登录、注册接口
}
}
✅ 应用场景:
- 登录校验:拦截所有需要权限的接口
- 请求日志:记录访问路径、IP、耗时
- 性能监控:统计接口响应时间
- 请求追踪:通过
requestId链接日志、链路追踪
✅ 九、综合性实战示例:用户管理系统(完整整合)
🎯 功能清单
| 功能 | 使用技术 |
|---|---|
| 用户注册 | @PostMapping + @RequestBody + @Valid |
| 用户登录 | @PostMapping("/login")(无需校验) |
| 查询用户列表 | @GetMapping("/users") + 分页参数 |
| 修改用户 | @PutMapping("/users/{id}") + @PathVariable + @Valid |
| 删除用户 | @DeleteMapping("/users/{id}") |
| 文件上传 | @RequestParam("avatar") MultipartFile |
| 文件下载 | ResponseEntity<Resource> |
| 登录校验 | HandlerInterceptor |
| 全局异常 | @RestControllerAdvice |
| 统一响应 | CommonResult<T> |
| 日志记录 | @Slf4j + 拦截器 |
✅ DTO 类
// UserCreateRequest.java(同上)
// UserUpdateRequest.java
@Data
public class UserUpdateRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20)
private String username;
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 18, message = "年龄不能小于 18 岁")
private Integer age;
}
✅ Controller
package com.example.controller;
import com.example.dto.CommonResult;
import com.example.dto.UserCreateRequest;
import com.example.dto.UserUpdateRequest;
import com.example.exception.BusinessException;
import com.example.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public ResponseEntity<CommonResult<String>> register(@Valid @RequestBody UserCreateRequest request) {
userService.register(request);
return ResponseEntity.ok(CommonResult.success("注册成功"));
}
@PostMapping("/login")
public ResponseEntity<CommonResult<String>> login(@RequestParam String username, @RequestParam String password) {
if (!"admin".equals(username) || !"123456".equals(password)) {
throw new BusinessException(1001, "用户名或密码错误");
}
return ResponseEntity.ok(CommonResult.success("登录成功"));
}
@GetMapping
public ResponseEntity<CommonResult<List<String>>> list() {
List<String> users = userService.list();
return ResponseEntity.ok(CommonResult.success(users));
}
@PutMapping("/{id}")
public ResponseEntity<CommonResult<String>> update(@PathVariable Long id, @Valid @RequestBody UserUpdateRequest request) {
userService.update(id, request);
return ResponseEntity.ok(CommonResult.success("更新成功"));
}
@DeleteMapping("/{id}")
public ResponseEntity<CommonResult<String>> delete(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.ok(CommonResult.success("删除成功"));
}
@PostMapping("/upload")
public ResponseEntity<CommonResult<String>> uploadAvatar(@RequestParam("avatar") MultipartFile file) {
// 省略校验逻辑(见上文)
String fileName = "avatar_" + System.currentTimeMillis() + ".jpg";
try {
file.transferTo(new File("/opt/uploads/" + fileName));
} catch (IOException e) {
throw new RuntimeException("上传失败", e);
}
return ResponseEntity.ok(CommonResult.success("http://localhost:8080/uploads/" + fileName));
}
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> download(@PathVariable String fileName) {
File file = new File("/opt/uploads/" + fileName);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
InputStreamResource resource = new InputStreamResource(new FileInputStream(file));
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
}
✅ 测试建议:
- 用 Postman 测试
/api/users/register(传 JSON)- 测试
/api/users/upload(Form-data 上传文件)- 测试
/api/users/download/xxx.jpg- 测试
/api/users/update/1(传非法邮箱,看是否返回校验错误)- 测试
/api/users/login(不传 token,看是否拦截)
✅ 十、总结:Spring MVC 的设计哲学
| 层级 | 设计思想 |
|---|---|
| 请求映射 | @GetMapping 等注解让 HTTP 方法与 Java 方法一一对应 |
| 参数绑定 | 不再手动 request.getParameter(),Spring 自动封装 |
| 数据校验 | 声明式校验,业务逻辑与校验逻辑分离 |
| 异常处理 | 全局统一,避免“到处 try-catch” |
| 文件处理 | 封装 MultipartFile,屏蔽底层 IO |
| 拦截器 | AOP 思想在 Web 层的完美体现,解耦横切关注点 |
| 响应格式 | 统一 CommonResult,前后端契约清晰 |
✅ Spring MVC 的终极目标:
让开发者只关心业务逻辑,其余交给框架。
✅ 十一、学习建议与下一步
| 建议 | 说明 |
|---|---|
| ✅ 动手实践 | 用上述完整示例搭建一个项目,跑通所有功能 |
| ✅ 阅读源码 | 跟踪 DispatcherServlet.doDispatch() 方法,理解流程 |
| ✅ 学习 Thymeleaf | 掌握服务端渲染(虽然现在 API 为主) |
| ✅ 学习 Spring Boot Web | 99% 项目都用它,自动配置、内嵌 Tomcat |
| ✅ 学习 RESTful 设计 | 语义化 URL、HTTP 状态码、HATEOAS |
| ✅ 学习 Swagger/OpenAPI | 自动生成接口文档 |
✅ 附:推荐学习资源
| 资源 | 说明 |
|---|---|
| 《Spring 实战(第6版)》第 8–10 章 | 最权威的 Spring MVC 教程 |
| Spring 官方文档:https://docs.spring.io/spring-framework/reference/web/webmvc.html | 权威、准确、完整 |
| GitHub 项目:spring-petclinic | 官方示例,学习最佳实践 |
| Postman / Apifox | 测试 API 的必备工具 |
✅ 你已掌握 Spring MVC 的全部核心能力。
你现在不仅能写接口,还能设计安全、健壮、可维护、可审计的 Web 系统。
恭喜你,成为真正的 Java Web 架构师! 🚀
Spring MVC核心机制解析
1090

被折叠的 条评论
为什么被折叠?



