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()(仍执行)
相关推荐
寒士obj9 小时前
SpringMVC的执行流程
spring·mvc
zhong liu bin3 天前
【SpringMVC】SSM框架【二】——SpringMVC超详细
spring·mvc·intellij-idea
切糕师学AI4 天前
.Net Core Web 架构(管道机制)的底层实现
中间件·系统架构·kestrel·mvc·.netcore·路由·请求管道
CodeLongBear6 天前
Spring Boot 与 Spring MVC 的区别与联系:从本质到实践
spring boot·spring·mvc
fatfishccc6 天前
Spring MVC 全解析:从核心原理到 SSM 整合实战 (附完整源码)
java·spring·ajax·mvc·ssm·过滤器·拦截器interceptor
PythonicCC11 天前
Django中的MVC和MVT模式
数据库·django·mvc
神仙别闹14 天前
基于 JSP+Mysql实现MVC房屋租赁系统
java·mysql·mvc
optimistic_chen18 天前
【Java EE进阶 --- SpringBoot】初识Spring(创建SpringBoot项目)
spring boot·后端·spring·java-ee·tomcat·mvc·idea
不知名搬运工19 天前
9 ABP Framework 中的 MVC 和 Razor Pages
mvc