MVC问题记录
SpringMVC 和 Struts2 框架的对比
一、先行结论
- Spring MVC:当前 Java Web 领域的绝对主流与事实标准。设计理念现代、灵活性高,与 Spring 生态无缝集成,且安全性极强,是新项目的首选。
- Struts2:传统经典框架,曾广泛应用,但因严重安全漏洞(如 S2-045、S2-057 等远程代码执行漏洞) 、设计理念落后及维护更新停滞(Apache 已将其归入 attic 状态,即项目退休),已完全不推荐用于新项目。
二、核心特性对比表
特性 | SPRING MVC | STRUTS2 |
核心架构与设计 | 基于方法(Method) :1 个 URL 映射到 1 个控制器类的 1 个方法 | 基于类(Class) :1 个 URL 映射到 1 个 Action 类,执行其 execute 方法 |
拦截机制 | 处理器拦截器(HandlerInterceptor):细粒度控制(preHandle/postHandle/afterCompletion) | 拦截器栈:功能强但设计复杂,基于责任链模式,需通过配置管理 |
控制器实现 | 用@Controller注解,普通 POJO 类,方法参数 / 返回值灵活 | 需继承ActionSupport类或实现Action接口,与框架 API 强耦合 |
数据绑定 | 支持@RequestParam/@PathVariable/@RequestBody,与 Spring 转换器无缝集成 | 基于 OGNL 表达式,曾是安全漏洞主要来源 |
视图集成 | 高度解耦,支持 JSP/Thymeleaf/Freemarker 等,通过 ViewResolver 配置 | 与 JSP+OGNL 深度绑定,支持其他视图但灵活性差 |
性能 | 更高:控制器默认单例,无需频繁创建对象,减少 GC 压力 | 较低:Action 默认多例(每个请求创建新实例),GC 压力大 |
配置方式 | 推崇注解驱动 + Java Config,配置简洁易维护,XML 配置已淘汰 | 重度依赖 XML+“约定优于配置”,配置繁琐 |
与 Spring 生态集成 | 无缝集成(Spring 核心组件),可直接使用 IoC/AOP/ 事务 / Spring Security | 集成困难,需额外插件 + 复杂配置才能对接 Spring IoC |
安全性 | 极高:设计简洁 + 社区支持强,历史严重漏洞极少,与 Spring Security 是黄金组合 | 极差:历史大量高危 RCE 漏洞,是其衰落的核心原因 |
RESTful 支持 | 原生支持:通过@RestController/@GetMapping等注解轻松构建 REST API | 支持差:需插件或手动配置,非 RESTful 优先设计 |
学习曲线 | 中等:有 Spring 基础则极易上手,设计直观 | 中等偏上:需理解拦截器栈、OGNL 等独特概念 |
当前状态与社区 | 极其活跃:Spring 生态核心,持续更新,社区庞大,行业标准 | 基本停滞:Apache 归入 attic 状态,不再维护 |
三、核心差异详解
3.1 架构设计:基于方法 vs 基于类
- Spring MVC 的优势:
- 灵活性极致:1 个控制器类可包含多个方法,每个方法独立设计参数 / 返回值,便于测试与复用。
- 低耦合:控制器是普通 POJO,无需依赖框架 API。
- Struts2 的劣势:
- 单一职责局限:1 个 Action 通常仅服务 1 个请求,虽可通过配置method属性指向不同方法,但远不如 Spring MVC 注解直观。
- 强耦合:必须继承ActionSupport或实现Action,与框架绑定紧密。
3.2 请求处理生命周期
- Spring MVC 流程:
DispatcherServlet(前端控制器) → 匹配HandlerMapping → 执行HandlerInterceptor与Controller → 通过ViewResolver解析视图
- Struts2 流程:
Filter(核心控制器) → 执行配置的拦截器栈 → 调用Action → 返回结果字符串 → 匹配视图
注:Struts2 拦截器栈功能(如验证、文件上传)虽强,但导致框架 “重量级” 与复杂度飙升。
四、HTTP 响应体解析
4.1 直观理解响应体
以下是一个完整的 HTTP 响应示例,空行后的内容即为响应体:
HTTP/1.1 200 OKContent-Type: application/json; charset=UTF-8Content-Length: 56Date: Wed, 24 Jan 2024 10:30:00 GMT{"id": 1, "name": "张三", "email": "zhangsan@example.com"}
- 前 4 行:响应头(Response Headers),描述响应元信息;
- 空行后:响应体(Response Body),实际传输的数据内容。
4.2 响应体的核心属性
4.2.1 响应体的位置
HTTP 响应的固定结构:
[状态行](如HTTP/1.1 200 OK)[响应头1](如Content-Type: application/json)[响应头2](如Content-Length: 56)...[空行](分隔响应头与响应体)[响应体](实际数据,如JSON/HTML/二进制文件)
4.2.2 响应体的内容类型
不同内容类型对应不同的Content-Type头,常见类型如下:
内容类型 | 示例 | 对应 CONTENT-TYPE |
JSON 数据 | {"name": "John", "age": 25} | application/json |
HTML 页面 | <html><body>Hello</body></html> | text/html |
纯文本 | Hello World | text/plain |
XML 数据 | <user><name>John</name></user> | application/xml |
图片文件 | 二进制图像数据 | image/jpeg/image/png |
文件下载 | 二进制文件数据(如.zip/.pdf) | application/octet-stream |
4.3 @ResponseBody的作用机制
@ResponseBody用于告诉 Spring:方法返回值直接写入响应体,不解析为视图名称。
4.3.1 基础示例
@GetMapping("/user/{id}")@ResponseBody // 返回的User对象自动放入响应体public User getUser(@PathVariable Long id) {return userService.findById(id); // 最终转为JSON写入响应体}
4.3.2 处理流程
- 控制器方法返回User对象;
- Spring 检测到@ResponseBody注解;
- 选择合适的HttpMessageConverter(如 Jackson,默认处理 JSON);
- 将User对象序列化为 JSON 字符串;
- 将 JSON 字符串写入 HTTP 响应体;
- 自动设置响应头Content-Type: application/json。
五、Spring MVC 拦截器工作机制
5.1 拦截器配置顺序(核心)
拦截器按配置顺序执行,形成责任链,可通过order()显式指定优先级(数值越小,优先级越高)。
@Configurationpublic class WebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 1. 日志拦截器:优先级最高(order=1),拦截/api/**路径registry.addInterceptor(loggingInterceptor()).addPathPatterns("/api/**").order(1);// 2. 权限拦截器:优先级次之(order=2),拦截/api/**与/admin/**registry.addInterceptor(authInterceptor()).addPathPatterns("/api/**", "/admin/**").order(2);// 3. 性能监控拦截器:优先级最低(order=3),拦截所有路径registry.addInterceptor(performanceInterceptor()).addPathPatterns("/**").order(3);}}
5.2 拦截路径匹配规则
通过addPathPatterns()(包含路径)和excludePathPatterns()(排除路径)定义拦截范围,常见规则:
- /api/**:匹配所有以/api/开头的路径(含子路径,如/api/user/1);
- /admin/*:匹配/admin/下一级路径(不含子路径,如/admin/login,不匹配/admin/user/1);
- /**:匹配所有路径;
- /public/*.html:匹配/public/下所有.html 文件(如/public/index.html)。
5.3 拦截器执行流程
当请求到达时,Spring MVC 按以下步骤执行:
- 按order顺序检查拦截器是否匹配当前路径;
- 执行所有匹配拦截器的preHandle()(顺序:order=1 → order=2 → order=3);
- 执行控制器方法(若任意preHandle()返回false,则终止流程);
- 执行所有匹配拦截器的postHandle()(逆序:order=3 → order=2 → order=1);
- 渲染视图(若有);
- 执行所有匹配拦截器的afterCompletion()(逆序,且无论是否异常都会执行)。
六、@RestControllerAdvice与@ControllerAdvice的区别
6.1 核心区别:注解组合关系
@RestControllerAdvice = @ControllerAdvice + @ResponseBody,即@RestControllerAdvice会自动为所有方法添加@ResponseBody效果。
6.1.1 @RestControllerAdvice示例(适合 REST API)
// 自动为所有方法添加@ResponseBody,返回值直接序列化为JSON@RestControllerAdvicepublic class GlobalExceptionHandler {@ExceptionHandler(RuntimeException.class) // 捕获RuntimeExceptionpublic ErrorResponse handleException(RuntimeException ex) {return new ErrorResponse("500", ex.getMessage()); // 返回JSON格式错误信息}}
6.1.2 @ControllerAdvice示例(适合传统 Web)
// 需手动控制返回类型,默认返回视图名称@ControllerAdvicepublic class GlobalExceptionHandler {@ExceptionHandler(RuntimeException.class)public String handleException(RuntimeException ex, Model model) {model.addAttribute("errorMsg", ex.getMessage()); // 向视图传递数据return "error-page"; // 返回视图名称(如error-page.jsp/error-page.html)}}
6.2 返回类型的本质差异
注解 | 返回值处理方式 | 适合场景 |
@RestControllerAdvice | 直接序列化为 JSON/XML(无视图解析) | 前后端分离、REST API 项目 |
@ControllerAdvice | 作为视图名称解析(需视图引擎) | 传统 Web 项目(JSP/Thymeleaf 渲染) |
6.3 如何正确选择
- 项目类型:前后端分离选@RestControllerAdvice,传统 Web 选@ControllerAdvice;
- 返回内容:需返回 JSON/XML 选前者,需返回 HTML 页面选后者;
- 技术栈:纯 API 项目(如微服务接口)选前者,使用 JSP/Thymeleaf 选后者。
七、@ResponseBody核心概念与实践
7.1 核心作用
告诉 Spring:方法返回值直接写入 HTTP 响应体,跳过视图解析流程,是构建 REST API 的基础。
7.2 与@RestController的关系
@RestController是组合注解,等价于@Controller + @ResponseBody,即类上添加@RestController后,所有方法默认具有@ResponseBody效果。
// 以下两种写法完全等价// 写法1:@Controller + @ResponseBody@Controller@ResponseBodypublic class UserController {@GetMapping("/user/1")public User getUser() { return new User("张三", 20); }}// 写法2:@RestController(推荐,更简洁)@RestControllerpublic class UserController {@GetMapping("/user/1")public User getUser() { return new User("张三", 20); }}
7.3 消息转换器(HttpMessageConverter)
@ResponseBody的底层依赖HttpMessageConverter,Spring 根据返回值类型和请求头Accept自动选择转换器,常见转换器:
转换器 | 功能 | 默认生效条件 |
MappingJackson2HttpMessageConverter | 将对象转为 JSON | 项目依赖 Jackson(如jackson-databind) |
Jaxb2RootElementHttpMessageConverter | 将对象转为 XML | 项目依赖 JAXB |
StringHttpMessageConverter | 直接返回字符串(不序列化) | 方法返回值为String类型 |
7.4 支持的返回值类型
@RestControllerpublic class ExampleController {// 1. 返回对象:自动转为JSON@GetMapping("/object")public User returnObject() {return new User("John", 25);}// 2. 返回集合:自动转为JSON数组@GetMapping("/list")public List<User> returnList() {return Arrays.asList(new User("John"), new User("Jane"));}// 3. 返回Map:自动转为JSON对象@GetMapping("/map")public Map<String, Object> returnMap() {Map<String, Object> map = new HashMap<>();map.put("name", "John");map.put("age", 25);return map;}// 4. 返回字符串:直接写入响应体(Content-Type: text/plain)@GetMapping("/string")public String returnString() {return "Hello World";}// 5. 返回ResponseEntity:灵活控制响应头/状态码@GetMapping("/response-entity")public ResponseEntity<User> returnResponseEntity() {User user = new User("John");return ResponseEntity.ok() // 状态码200.header("Custom-Header", "spring-mvc") // 自定义响应头.body(user); // 响应体内容}}
7.5 内容协商(Content Negotiation)
Spring 支持根据请求头Accept返回不同格式的数据,通过produces指定支持的类型:
// 支持返回JSON或XML,根据请求头Accept选择@GetMapping(value = "/user/{id}",produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})public User getUser(@PathVariable Long id) {return userService.findById(id);}
- 客户端请求头Accept: application/json → 返回 JSON;
- 客户端请求头Accept: application/xml → 返回 XML。
7.6 常见问题与解决方案
问题 1:返回中文乱码
解决方案:通过produces指定字符集:
@GetMapping(value = "/data", produces = "application/json;charset=UTF-8")public String getData() {return "中文数据"; // 避免乱码}
问题 2:自定义 JSON 序列化(如忽略字段、格式化日期)
解决方案:使用 Jackson 注解:
@Data // Lombok注解,自动生成getter/setterpublic class User {@JsonIgnore // 序列化时忽略password字段private String password;@JsonProperty("user_name") // 序列化后字段名为user_name(而非username)private String username;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") // 日期格式化private Date createTime;}
问题 3:全局配置字符编码
解决方案:配置HttpMessageConverter:
@Configurationpublic class WebConfig implements WebMvcConfigurer {@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {// 配置String转换器,全局使用UTF-8StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(StandardCharsets.UTF_8);converters.add(0, stringConverter); // 优先使用自定义转换器}}
7.7 与@RequestBody的对比
注解 | 作用 | 使用位置 |
@ResponseBody | 输出:将方法返回值写入响应体 | 方法上或类上 |
@RequestBody | 输入:将请求体转为方法参数 | 方法参数上 |
示例:同时处理请求体输入与响应体输出
@PostMapping("/users")public User createUser(@RequestBody User user) {// @RequestBody:将请求体JSON转为User对象(输入)User savedUser = userService.save(user);return savedUser; // @ResponseBody(因类上有@RestController):将User转为JSON写入响应体(输出)}
7.8 总结与最佳实践
核心作用
- 跳过视图解析,直接操作响应体;
- 自动序列化(对象→JSON/XML);
- 支撑 RESTful API 构建;
- 支持内容协商与自定义配置。
适用场景
- ✅ 构建 RESTful Web 服务;
- ✅ 前后端分离项目;
- ✅ 提供 JSON/XML 数据接口;
- ✅ 处理 Ajax 请求响应。
最佳实践
- 纯 API 项目:类上用@RestController(无需重复加@ResponseBody);
- 混合项目(既有 API 也有页面):仅在 API 方法上加@ResponseBody;
- 需控制响应头 / 状态码:用ResponseEntity;
- 明确返回类型:通过produces指定Content-Type(如produces = "application/json;charset=UTF-8")。
八、为什么异常处理会触发拦截器?
8.1 拦截器在异常处理前执行
拦截器的preHandle()在控制器方法执行之前调用,即使控制器抛出异常,preHandle()已执行完成。
public class LoggingInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) {System.out.println("preHandle:在控制器执行前调用");return true; // 继续流程}}
8.2 异常处理后的完整流程
Spring MVC 会确保请求流程 “完整性”,即使发生异常,也会执行afterCompletion()。内部逻辑可简化为:
// 模拟Spring MVC内部流程try {// 1. 执行所有拦截器的preHandle()(顺序执行)for (Interceptor interceptor : interceptors) {if (!interceptor.preHandle(request, response, handler)) {return; // 若preHandle()返回false,终止流程}}// 2. 执行控制器方法(可能抛出异常)handler.handle(request, response);// 3. 执行所有拦截器的postHandle()(逆序执行)for (Interceptor interceptor : reverse(interceptors)) {interceptor.postHandle(request, response, handler, modelAndView);}// 4. 渲染视图renderView(modelAndView);} catch (Exception ex) {// 5. 异常处理(如@ControllerAdvice)handleException(ex, request, response);} finally {// 6. 执行所有拦截器的afterCompletion()(逆序执行,无论是否异常)for (Interceptor interceptor : reverse(interceptors)) {interceptor.afterCompletion(request, response, handler, ex);}}
8.3 afterCompletion():异常场景的 “兜底” 方法
afterCompletion()是唯一无论是否异常都会执行的拦截器方法,常用于资源清理(如关闭流、释放连接)。
public class LoggingInterceptor implements HandlerInterceptor {@Overridepublic void afterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex) {// 即使控制器抛出异常,此方法仍会执行System.out.println("afterCompletion:异常信息=" + (ex == null ? "无" : ex.getMessage()));}}
8.4 实际场景对比
场景 1:正常请求处理
- 拦截器preHandle() → 2. 控制器方法执行 → 3. 拦截器postHandle() → 4. 渲染视图 → 5. 拦截器afterCompletion()
场景 2:控制器抛出异常
- 拦截器preHandle()(已执行) → 2. 控制器抛出异常 → 3. 跳过postHandle() → 4. @ControllerAdvice处理异常 → 5. 拦截器afterCompletion()(仍执行)