Appearance
第56课:Spring MVC
🎯 学习目标
- 理解 Spring MVC 的请求处理链路:
DispatcherServlet、HandlerMapping、HandlerAdapter、Controller、ViewResolver。 - 掌握 Controller、参数绑定、模型传递、视图渲染和 REST 响应。
- 能区分传统 MVC 页面开发和 REST API 开发。
- 能使用拦截器、全局异常处理和数据绑定扩展点。
- 能识别参数绑定、异常处理、拦截器顺序和文件上传中的常见问题。
📖 一、Spring MVC 是什么
Spring MVC 是基于 Servlet 的 Web MVC 框架。它把 HTTP 请求处理拆成清晰的组件:
text
浏览器/客户端
↓
DispatcherServlet
↓
HandlerMapping 找到处理器
↓
HandlerAdapter 调用 Controller
↓
Controller 返回 ModelAndView 或 JSON
↓
ViewResolver 渲染页面,或 HttpMessageConverter 写 JSON
↓
响应客户端Spring Boot Web 的 REST API 开发也是基于 Spring MVC,只是默认不再返回 JSP/Thymeleaf 页面,而是通过 @ResponseBody 或 @RestController 返回 JSON。
📖 二、DispatcherServlet 流程
完整流程:
text
1. 请求进入 Servlet 容器。
2. DispatcherServlet 作为前端控制器接管请求。
3. HandlerMapping 根据 URL、HTTP 方法等查找 Handler。
4. HandlerAdapter 适配并调用具体 Controller 方法。
5. Controller 执行业务,返回视图名、ModelAndView 或响应体。
6. 如果是页面,ViewResolver 解析视图。
7. 如果是 JSON,HttpMessageConverter 序列化响应体。
8. 返回 HTTP 响应。核心思想是“一个入口统一分发”,避免每个 Servlet 自己处理完整流程。
📖 三、Controller 开发
传统页面:
java
@Controller
@RequestMapping("/users")
public class UserPageController {
@GetMapping("/{id}")
public String detail(@PathVariable Long id, Model model) {
model.addAttribute("user", userService.findById(id));
return "user/detail";
}
@PostMapping
public String create(@Valid UserForm form, BindingResult result) {
if (result.hasErrors()) {
return "user/form";
}
userService.create(form);
return "redirect:/users";
}
}REST API:
java
@RestController
@RequestMapping("/api/users")
public class UserApiController {
@GetMapping("/{id}")
public UserResponse getById(@PathVariable Long id) {
return userService.getById(id);
}
@PostMapping
public UserResponse create(@Valid @RequestBody UserCreateRequest request) {
return userService.create(request);
}
}@RestController 等价于:
text
@Controller + @ResponseBody📖 四、参数绑定
常见参数来源:
java
@GetMapping("/users/{id}")
public UserResponse getById(@PathVariable Long id) {
return userService.getById(id);
}java
@GetMapping("/users")
public PageResult<UserResponse> list(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer size,
@RequestParam(required = false) String keyword) {
return userService.list(page, size, keyword);
}java
@PostMapping("/users")
public UserResponse create(@Valid @RequestBody UserCreateRequest request) {
return userService.create(request);
}java
@GetMapping("/profile")
public UserProfile profile(
@RequestHeader("Authorization") String authorization,
@CookieValue(value = "SESSION", required = false) String session) {
return userService.profile(authorization, session);
}对象绑定:
java
@GetMapping("/search")
public List<UserResponse> search(UserQueryRequest request) {
return userService.search(request);
}查询参数会自动绑定到对象属性。
📖 五、Validation 集成
java
public class UserCreateRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
}Controller:
java
@PostMapping
public UserResponse create(@Valid @RequestBody UserCreateRequest request) {
return userService.create(request);
}如果是查询参数:
java
@Validated
@RestController
public class UserController {
@GetMapping("/users")
public PageResult<UserResponse> list(
@Min(1) @RequestParam Integer page) {
return userService.list(page);
}
}请求体校验和方法参数校验抛出的异常类型不同,通常需要全局异常处理统一格式。
📖 六、HttpMessageConverter
REST API 返回 JSON 的关键是 HttpMessageConverter。
java
@GetMapping("/{id}")
public UserResponse getById(@PathVariable Long id) {
return userService.getById(id);
}Spring MVC 会选择 Jackson converter,把 UserResponse 序列化为 JSON。
请求体也是类似:
text
JSON 请求体 -> Jackson -> Java Request DTO
Java Response DTO -> Jackson -> JSON 响应体所以 JSON 配置、DTO 设计、Validation 和 Spring MVC 是连在一起的。
📖 七、拦截器
拦截器适合处理登录校验、请求日志、权限上下文等。
java
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || token.isBlank()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
// 请求完成后清理 ThreadLocal 等上下文
}
}注册:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/login", "/api/register");
}
}拦截器和 Servlet Filter 不同。Filter 更靠近 Servlet 容器,Interceptor 更靠近 Spring MVC Handler。
📖 八、统一异常处理
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException e) {
return ResponseEntity.badRequest()
.body(ApiError.validation(e.getBindingResult()));
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiError> handleBusiness(BusinessException e) {
return ResponseEntity.badRequest()
.body(new ApiError(e.getCode(), e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleUnknown(Exception e) {
log.error("unexpected error", e);
return ResponseEntity.internalServerError()
.body(new ApiError("INTERNAL_ERROR", "系统异常"));
}
}REST API 不应该把 Java 堆栈返回给前端。
📖 九、文件上传下载
上传:
java
@PostMapping("/files")
public FileResponse upload(@RequestParam("file") MultipartFile file) throws IOException {
if (file.isEmpty()) {
throw new BusinessException("FILE_EMPTY", "文件不能为空");
}
String filename = storageService.save(file);
return new FileResponse(filename);
}大小限制:
yaml
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB下载:
java
@GetMapping("/files/{filename}")
public ResponseEntity<Resource> download(@PathVariable String filename) {
Resource resource = storageService.load(filename);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.body(resource);
}文件名要防路径穿越,不能直接信任用户输入。
⚠️ 十、常见陷阱
1. 忘记 @RequestBody
POST JSON 请求如果没写 @RequestBody,Spring 不会从请求体解析 JSON。
2. BindingResult 位置错误
传统 MVC 中 BindingResult 必须紧跟被校验对象,否则不会正确接收错误。
3. 拦截器没有清理上下文
如果在拦截器中设置 ThreadLocal,必须在 afterCompletion 清理。
4. 直接返回 Entity
会导致敏感字段泄露、懒加载异常、循环引用和接口耦合数据库结构。
5. 文件上传路径穿越
用户传入的文件名可能包含 ../,保存前必须规范化和重命名。
🆚 十一、Java vs C 对比
| 维度 | C Web 服务 | Spring MVC |
|---|---|---|
| 路由 | 手写匹配或框架路由 | HandlerMapping |
| 参数解析 | 手动解析 query/body | 参数绑定和 Converter |
| JSON | 手动调用库解析 | HttpMessageConverter |
| 中间件 | 函数链或回调 | Filter/Interceptor |
| 错误处理 | 手动返回错误码 | @RestControllerAdvice |
Spring MVC 把 HTTP 处理的重复逻辑标准化,让开发者专注 Controller 和业务边界。
💡 十二、最佳实践
- REST API 使用
@RestController,页面开发使用@Controller。 - 请求和响应使用 DTO,不直接暴露 Entity。
- JSON 请求体必须显式使用
@RequestBody。 - 参数校验配合全局异常处理,返回统一错误格式。
- 登录认证等通用逻辑放在 Filter、Interceptor 或 Spring Security 中。
- 文件上传必须限制大小、重命名、校验类型和防路径穿越。
- Controller 保持薄,复杂业务交给 Service。
- 统一响应、统一异常、统一日志是 Web 项目的基础设施。
🧭 十三、项目落地清单
text
Controller 是否只处理 HTTP 边界?
所有请求体是否有 Request DTO?
所有响应是否有 Response DTO?
参数校验是否统一?
异常响应是否统一?
认证逻辑是否没有散落在每个 Controller?
文件上传是否限制大小和路径?
是否区分 Filter 与 Interceptor 的职责?🔍 十四、自测问题
text
DispatcherServlet 在 Spring MVC 中负责什么?
HandlerMapping 和 HandlerAdapter 分别做什么?
@Controller 和 @RestController 有什么区别?
@RequestParam、@PathVariable、@RequestBody 分别从哪里取值?
HttpMessageConverter 为什么能自动处理 JSON?
Filter 和 Interceptor 有什么区别?
为什么 REST API 要用 @RestControllerAdvice?
为什么不能直接返回 Entity?🎓 小结
Spring MVC 是 Spring Web 的核心。掌握它不是只会写 Controller,而是理解请求如何进入框架、参数如何绑定、JSON 如何转换、异常如何统一、拦截器如何参与处理链。后续 Spring Boot Web、Security、事务、接口文档都建立在这些基础上。