MVC问题记录

SpringMVC 和 Struts2 框架的对比

一、先行结论

  1. Spring MVC :当前 Java Web 领域的绝对主流与事实标准。设计理念现代、灵活性高,与 Spring 生态无缝集成,且安全性极强,是新项目的首选。
  2. 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 基于类

  1. Spring MVC 的优势
    1. 灵活性极致:1 个控制器类可包含多个方法,每个方法独立设计参数 / 返回值,便于测试与复用。
    2. 低耦合:控制器是普通 POJO,无需依赖框架 API。
  2. Struts2 的劣势
    1. 单一职责局限:1 个 Action 通常仅服务 1 个请求,虽可通过配置method属性指向不同方法,但远不如 Spring MVC 注解直观。
    2. 强耦合:必须继承ActionSupport或实现Action,与框架绑定紧密。

3.2 请求处理生命周期

  1. Spring MVC 流程

DispatcherServlet(前端控制器) → 匹配HandlerMapping → 执行HandlerInterceptor与Controller → 通过ViewResolver解析视图

  1. 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"}
  1. 前 4 行:响应头(Response Headers),描述响应元信息;
  2. 空行后:响应体(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 处理流程
  1. 控制器方法返回User对象;
  2. Spring 检测到@ResponseBody注解;
  3. 选择合适的HttpMessageConverter(如 Jackson,默认处理 JSON);
  4. 将User对象序列化为 JSON 字符串;
  5. 将 JSON 字符串写入 HTTP 响应体;
  6. 自动设置响应头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()(排除路径)定义拦截范围,常见规则:

  1. /api/**:匹配所有以/api/开头的路径(含子路径,如/api/user/1);
  2. /admin/*:匹配/admin/下一级路径(不含子路径,如/admin/login,不匹配/admin/user/1);
  3. /**:匹配所有路径;
  4. /public/*.html:匹配/public/下所有.html 文件(如/public/index.html)。

5.3 拦截器执行流程

当请求到达时,Spring MVC 按以下步骤执行:

  1. 按order顺序检查拦截器是否匹配当前路径;
  2. 执行所有匹配拦截器的 preHandle()(顺序:order=1 → order=2 → order=3);
  3. 执行控制器方法(若任意preHandle()返回false,则终止流程);
  4. 执行所有匹配拦截器的 postHandle()(逆序:order=3 → order=2 → order=1);
  5. 渲染视图(若有);
  6. 执行所有匹配拦截器的 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 如何正确选择

  1. 项目类型 :前后端分离选@RestControllerAdvice,传统 Web 选@ControllerAdvice;
  2. 返回内容:需返回 JSON/XML 选前者,需返回 HTML 页面选后者;
  3. 技术栈:纯 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);

}
  1. 客户端请求头Accept: application/json → 返回 JSON;
  2. 客户端请求头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 总结与最佳实践

核心作用
  1. 跳过视图解析,直接操作响应体;
  2. 自动序列化(对象→JSON/XML);
  3. 支撑 RESTful API 构建;
  4. 支持内容协商与自定义配置。
适用场景
  1. ✅ 构建 RESTful Web 服务;
  2. ✅ 前后端分离项目;
  3. ✅ 提供 JSON/XML 数据接口;
  4. ✅ 处理 Ajax 请求响应。
最佳实践
  1. 纯 API 项目:类上用@RestController(无需重复加@ResponseBody);
  2. 混合项目(既有 API 也有页面):仅在 API 方法上加@ResponseBody;
  3. 需控制响应头 / 状态码:用ResponseEntity;
  4. 明确返回类型:通过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:正常请求处理
  1. 拦截器preHandle() → 2. 控制器方法执行 → 3. 拦截器postHandle() → 4. 渲染视图 → 5. 拦截器afterCompletion()
场景 2:控制器抛出异常
  1. 拦截器preHandle()(已执行) → 2. 控制器抛出异常 → 3. 跳过postHandle() → 4. @ControllerAdvice处理异常 → 5. 拦截器afterCompletion()(仍执行)
相关推荐
程序员小凯13 小时前
Spring MVC 分布式事务与数据一致性教程
分布式·spring·mvc
艾菜籽1 天前
SpringMVC练习:加法计算器与登录
java·spring boot·spring·mvc
程序员小凯1 天前
Spring MVC 多租户架构与数据隔离教程
spring·架构·mvc
艾菜籽2 天前
Spring Web MVC入门补充1
java·后端·spring·mvc
艾菜籽2 天前
Spring MVC入门补充2
java·spring·mvc
风兮w2 天前
MVC、MVP和MVVM的区别
mvc
蓝天智能3 天前
QT MVC中Model的特点及使用注意事项
qt·mvc
低音钢琴3 天前
【SpringBoot从初学者到专家的成长15】MVC、Spring MVC与Spring Boot:理解其差异与联系
spring boot·spring·mvc
.NET修仙日记3 天前
2025年ASP.NETMVC面试题库全解析
面试·职场和发展·c#·asp.net·mvc·面试题·asp.net mvc
William_cl4 天前
C# MVC网页调试的方法
开发语言·c#·mvc