SpringMVC是Spring生态中最早成熟起来的Web层框架,直到今天大量传统Java项目依然运行在它之上。要真正理解它,不能只停留在“接收请求返回JSON”的层面,而要搞清楚它的请求处理流程、核心组件分工,以及实际使用中那些容易踩的坑。本文就从这三个维度把SpringMVC完整讲一遍。

一、SpringMVC到底是什么
SpringMVC的全称是Spring Web MVC,它是Spring框架中基于MVC设计模式的Web层框架。MVC即Model(模型)、View(视图)、Controller(控制器)的分层思想:Controller负责接收和分发请求,Model封装业务数据,View负责最终呈现。SpringMVC对这套思想做了实现,并且天然与Spring的IoC容器融合,控制器可以像普通Bean一样被注入依赖。
它的核心是一个前端控制器——DispatcherServlet。所有请求先打到这个Servlet上,再由它统一调度后续组件完成处理。这种集中式分发的设计好处很明显:开发者只需要关注业务控制器怎么写,请求解析、适配、视图渲染等通用逻辑全部由框架接管。相比早期直接写Servlet的时代,代码量大幅下降,参数绑定、数据校验、统一异常处理等能力都开箱即用。
与Struts2相比,SpringMVC是方法级的拦截,一个Controller类里可以放多个处理方法,方法入参直接接收请求数据,返回值灵活支持视图、JSON、文件流等。而Struts2是类级拦截,每个请求要创建一个Action实例。加上SpringMVC与Spring容器无缝集成,这也是它逐渐成为Java Web开发主流选择的原因。
二、核心组件与请求处理流程详解
一次完整的请求会经过多个组件协作。首先DispatcherServlet接收请求,然后查询HandlerMapping找到对应的处理器(Handler)以及一组拦截器,返回一个HandlerExecutionChain执行链。接着HandlerAdapter负责真正调用处理器方法——之所以需要适配器,是因为处理器的形式多种多样,有基于注解的、有实现Controller接口的,适配器模式屏蔽了这些差异。
控制器方法执行完毕后返回一个ModelAndView对象(使用@ResponseBody时则直接由消息转换器写出响应)。如果是返回视图名,ViewResolver会把逻辑视图名解析为具体的物理视图,最后渲染数据输出到前端。整个流程中还有HandlerExceptionResolver处理异常、拦截器在前后插入通用逻辑。理解了这条链路,排查问题时的思路会清晰很多:404查HandlerMapping映射、500查异常解析器、返回内容不对查消息转换器。
下面用一段传统的XML加注解混合方式展示最小可运行的配置,帮助理解各组件在配置中如何体现:
<!-- web.xml 中注册前端控制器 -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
// 返回JSON数据
@RequestMapping(value = "/info", method = RequestMethod.GET)
@ResponseBody
public User getUser(@RequestParam("id") Long id) {
return userService.findById(id);
}
// 返回逻辑视图名,交给ViewResolver解析
@RequestMapping("/list")
public String list(Model model) {
model.addAttribute("users", userService.findAll());
return "user/list"; // 对应 /WEB-INF/views/user/list.jsp
}
}三、怎么做:搭建与进阶用法
搭建SpringMVC项目有三条路线。第一种是传统的XML配置,需要配置DispatcherServlet、组件扫描、注解驱动和视图解析器,适合维护老项目时阅读理解。第二种是完全Java Config的方式,继承AbstractAnnotationConfigDispatcherServletInitializer,摆脱web.xml,配置更类型安全。第三种是直接用Spring Boot,它内嵌Tomcat并自动配置了SpringMVC的绝大部分组件,加一个spring-boot-starter-web依赖即可写控制器,是目前新项目的首选。
日常开发中最常用的注解要掌握这几个:@Controller声明控制器,@RequestMapping映射请求路径(可细化为@GetMapping、@PostMapping),@RequestParam接收URL参数,@PathVariable接收REST风格的路径变量,@RequestBody把请求体反序列化为Java对象,@ResponseBody把返回对象序列化为JSON。配合@RestController这个组合注解,可以省去每个方法上的@ResponseBody。
进阶一点的功能包括统一异常处理和数据校验。全局异常处理用@ControllerAdvice加@ExceptionHandler,把业务异常统一转成友好的错误响应;参数校验配合@Validated和JSR-303注解(如@NotNull、@Size),在进入业务逻辑前就把非法参数拦下来,避免散落各处的if判断。
@RestControllerAdvice
public class GlobalExceptionHandler {
// 捕获业务异常并返回统一格式
@ExceptionHandler(BizException.class)
public Result<Void> handleBiz(BizException e) {
return Result.fail(e.getCode(), e.getMessage());
}
// 捕获参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.fail(400, msg);
}
}四、怎么选:SpringMVC的适用场景判断
如果团队维护的是传统WAR部署的单一应用,需要精细控制视图渲染流程,或者项目本身就是基于SpringMVC构建的历史系统,继续使用它完全没有问题,它的稳定性和生态成熟度经过了大量生产环境验证。对于前后端分离的项目,SpringMVC只承担REST接口层也非常合适,配合Jackson做JSON序列化即可。
但如果是全新项目,更推荐基于Spring Boot使用SpringMVC,因为底层依然是同一套东西,只是省去了繁琐配置。而在微服务架构下,网关转发、接口文档、链路追踪等需求增多,Spring Boot加Spring Cloud的组合显然更高效。简单说:SpringMVC是引擎,Spring Boot是整装好的汽车,选哪个取决于你是想自己组装还是直接上路。
五、注意事项与避坑建议
坑一:静态资源被前端控制器拦截。当DispatcherServlet的url-pattern配置成/时,js、css、图片等静态资源请求也会进入SpringMVC,由于没有对应的Handler导致404。解决办法是在配置中加上<mvc:resources>或标注<mvc:default-servlet-handler>,Spring Boot项目则默认已经处理好。
坑二:POST请求中文乱码。Tomcat默认按ISO-8859-1解析请求体,提交中文就乱码。要在web.xml中配置CharacterEncodingFilter并放在所有Filter之前,GET请求的乱码则需修改Tomcat连接器的URIEncoding为UTF-8。
坑三:返回JSON报406错误。方法上有@ResponseBody却返回406,通常是缺少Jackson依赖,或者对应的<mvc:annotation-driven>/ @EnableWebMvc没有开启,导致找不到能处理application/json的消息转换器。
坑四:控制器中的事务失效。SpringMVC的容器是子容器,Spring核心容器是父容器。如果把Service的事务配置只加在父容器而扫描规则写错,或者在Controller方法里用try-catch吞掉异常后再手动抛受检异常,都可能导致事务不回滚。建议事务统一通过@Transactional声明在Service层,并注意默认只对运行时异常回滚。
坑五:拦截器路径匹配写错。Interceptor的excludePathPatterns少写或通配符写错,会造成登录校验意外放行或误拦截。上线前务必用真实路径回归一遍,特别是带路径参数的REST接口。
把这些坑点记牢,再配合对DispatcherServlet流程的理解,无论是阅读老项目还是排查线上问题,都能做到心里有数。建议把本文收藏起来,遇到具体报错时对照流程逐段排查,效率会高很多。