MVC问题记录

发布于:2025-09-02 ⋅ 阅读:(15) ⋅ 点赞:(0)

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 → 执行HandlerInterceptorController → 通过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 基础示例
@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()显式指定优先级(数值越小,优先级越高)。

@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=1order=2order=3);
  3. 执行控制器方法(若任意preHandle()返回false,则终止流程);
  4. 执行所有匹配拦截器的postHandle()(逆序:order=3order=2order=1);
  5. 渲染视图(若有);
  6. 执行所有匹配拦截器的afterCompletion()(逆序,且无论是否异常都会执行)。

六、@RestControllerAdvice@ControllerAdvice的区别

6.1 核心区别:注解组合关系

@RestControllerAdvice = @ControllerAdvice + @ResponseBody,即@RestControllerAdvice会自动为所有方法添加@ResponseBody效果。

6.1.1 @RestControllerAdvice示例(适合 REST API)
// 自动为所有方法添加@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)

// 需手动控制返回类型,默认返回视图名称

@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效果。

// 以下两种写法完全等价

// 写法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 支持的返回值类型

@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指定支持的类型:

// 支持返回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指定字符集:

@GetMapping(value = "/data", produces = "application/json;charset=UTF-8")

public String getData() {

    return "中文数据"; // 避免乱码

}
问题 2:自定义 JSON 序列化(如忽略字段、格式化日期)

解决方案:使用 Jackson 注解:

@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

@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

输入:将请求体转为方法参数

方法参数上

示例:同时处理请求体输入与响应体输出

@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()已执行完成。

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()。内部逻辑可简化为:

// 模拟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 {

    @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()(仍执行)