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 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 56
Date: 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 基础示例
java
@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()显式指定优先级(数值越小,优先级越高)。
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public 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)
java
// 自动为所有方法添加@ResponseBody,返回值直接序列化为JSON
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class) // 捕获RuntimeException
public ErrorResponse handleException(RuntimeException ex) {
return new ErrorResponse("500", ex.getMessage()); // 返回JSON格式错误信息
}
}
6.1.2 @ControllerAdvice示例(适合传统 Web)
java
// 需手动控制返回类型,默认返回视图名称
@ControllerAdvice
public 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效果。
java
// 以下两种写法完全等价
// 写法1:@Controller + @ResponseBody
@Controller
@ResponseBody
public class UserController {
@GetMapping("/user/1")
public User getUser() { return new User("张三", 20); }
}
// 写法2:@RestController(推荐,更简洁)
@RestController
public 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 支持的返回值类型
java
@RestController
public 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指定支持的类型:
java
// 支持返回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指定字符集:
java
@GetMapping(value = "/data", produces = "application/json;charset=UTF-8")
public String getData() {
return "中文数据"; // 避免乱码
}
问题 2:自定义 JSON 序列化(如忽略字段、格式化日期)
解决方案:使用 Jackson 注解:
java
@Data // Lombok注解,自动生成getter/setter
public 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:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 配置String转换器,全局使用UTF-8
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(StandardCharsets.UTF_8);
converters.add(0, stringConverter); // 优先使用自定义转换器
}
}
7.7 与@RequestBody的对比
|---------------|----------------|--------|
| 注解 | 作用 | 使用位置 |
| @ResponseBody | 输出:将方法返回值写入响应体 | 方法上或类上 |
| @RequestBody | 输入:将请求体转为方法参数 | 方法参数上 |
示例:同时处理请求体输入与响应体输出
java
@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()已执行完成。
java
public class LoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
System.out.println("preHandle:在控制器执行前调用");
return true; // 继续流程
}
}
8.2 异常处理后的完整流程
Spring MVC 会确保请求流程 "完整性",即使发生异常,也会执行afterCompletion()。内部逻辑可简化为:
java
// 模拟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()是唯一无论是否异常都会执行的拦截器方法,常用于资源清理(如关闭流、释放连接)。
java
public class LoggingInterceptor implements HandlerInterceptor {
@Override
public 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()(仍执行)