SpringMVC九大高频面试题深度拆解与实战剖析
最近几年,Java后端开发岗位的面试,SpringMVC几乎成了必考项。很多朋友在准备时,往往陷入两个极端:要么死记硬背官方文档里的概念,要么只停留在“会用”的层面,一旦面试官深入追问设计原理和对比细节,就容易卡壳。这篇文章,我想从一个面试官和一线开发者的双重角度,和你聊聊SpringMVC面试中最常被问到的九个核心问题。我不会照本宣科地复述流程,而是会结合我这些年面试别人和被别人面试的经验,拆解问题背后的设计思想、实战中的“坑”,以及与Struts2这类“前辈”框架的关键差异。希望这份深度解析,能帮你不仅“答对”,更能“答透”,在面试中展现出真正的技术深度。
1. 从请求到响应:SpringMVC的核心工作流程到底是怎么跑的?
面试官抛出这个问题,绝不仅仅是让你背出那“九步”或“十步”。他真正想考察的,是你对请求生命周期和组件协作的理解深度。很多候选人能说出DispatcherServlet、HandlerMapping这些名词,但一旦追问“为什么这么设计?”或者“某个环节出错了会怎样?”,就露怯了。
我们先抛开复杂的时序图,想象一个真实的场景:用户在你的电商网站点击了“加入购物车”按钮。这个点击动作,触发了一个HTTP请求。这个请求的“奇幻漂流”之旅,正是SpringMVC工作流程的缩影。
第一步,请求的“总入口”:DispatcherServlet。 这可不是一个普通的Servlet。在web.xml里,我们通常会把它映射到/,意味着所有请求(除了静态资源)都先经过它。你可以把它理解为一个公司的前台接待或者调度中心。它自己不处理具体业务,但手握所有部门的联系方式(组件列表)。它的核心职责是流程控制与协调。
注意:很多初级开发者会混淆
DispatcherServlet和Spring的ContextLoaderListener。后者负责加载根应用上下文(比如Service、DAO层的Bean),而DispatcherServlet会创建自己的子应用上下文(专管Controller、ViewResolver等Web层组件),并继承根上下文。这是父子容器设计,面试时能点明这一点会很加分。
第二步,寻找“对口部门”:HandlerMapping。 前台接到“加入购物车”的请求,得知道该派给哪个部门(哪个Controller的哪个方法)处理。HandlerMapping就是公司的通讯录或路由表。它维护着请求URL与具体处理器(HandlerMethod)之间的映射关系。现在主流是基于@RequestMapping注解的RequestMappingHandlerMapping,它会在应用启动时扫描所有@Controller注解的类,建立一张映射表。
// 一个典型的Controller方法,其映射信息会被HandlerMapping记录
@Controller
@RequestMapping("/cart")
public class CartController {
@PostMapping("/add")
@ResponseBody
public ResponseEntity<String> addItem(@RequestBody AddItemRequest request) {
// 业务逻辑
return ResponseEntity.ok("添加成功");
}
}
第三步,适配与执行:HandlerAdapter。 找到对口的“开发部”(Controller)后,发现他们用的工作语言(方法签名、参数类型)可能五花八门。有的方法参数用@RequestParam,有的用@RequestBody,还有的直接用HttpServletRequest。HandlerAdapter就是翻译官或适配器。它的核心作用是以统一的方式调用目标处理器方法,并处理复杂的参数绑定、数据转换、返回值处理等。最常用的是RequestMappingHandlerAdapter。
这里有个关键细节:参数解析器(HandlerMethodArgumentResolver)和返回值处理器(HandlerMethodReturnValueHandler)是HandlerAdapter工作的基石。SpringMVC的强大灵活性,很大程度上源于它那几十个内置的解析器和处理器。
第四步,业务逻辑执行与结果封装。 “翻译官”调用目标方法后,我们写的业务代码真正执行。方法执行后返回的结果,可能是String(视图名)、ModelAndView、ResponseEntity,或者一个普通的POJO对象。HandlerAdapter会将这些结果统一包装或处理,最终通常会形成一个包含**模型数据(Model)和视图信息(View)**的ModelAndView对象(即使返回的是JSON,其内部逻辑也类似)。
第五步与第六步,视图的解析与渲染:ViewResolver & View。 如果返回的结果指示需要渲染一个页面(比如JSP、Thymeleaf模板),流程就进入视图环节。ViewResolver根据返回的逻辑视图名(如"success"),结合配置的前缀后缀(如/WEB-INF/views/和.jsp),解析出具体的物理视图对象(View)。最后,View对象接手,将模型数据(第二步放入Model的数据)与模板结合,生成最终的HTML响应。
如果方法标注了@ResponseBody或者返回ResponseEntity,流程会在这里“短路”。RequestResponseBodyMethodProcessor这个返回值处理器会直接接管,利用HttpMessageConverter(如MappingJackson2HttpMessageConverter)将对象转换为JSON/XML,直接写入响应体,不再走视图解析和渲染。
整个流程中,还有两个重要的“幕后角色”:HandlerExceptionResolver负责处理整个过程中抛出的异常,将其转换为友好的错误响应;LocaleResolver、ThemeResolver等则处理国际化、主题等交叉关注点。
把这个流程理解成一个高度可插拔的管道,每个环节的组件都可以被定制或替换,你就能真正把握SpringMVC设计的精髓,而不仅仅是背诵步骤。
2. 为什么说SpringMVC是方法级别的拦截,而Struts2是类级别的?这带来了什么本质区别?
这个问题是SpringMVC vs Struts2对比的经典开场,它直接指向了两个框架在设计哲学和使用范式上的根本不同。理解这一点,就能理解它们后续的很多差异。
Struts2的“类拦截”世界 在Struts2中,核心的处理单元是Action类。一个Action类对应一个请求上下文。你在配置中(无论是XML还是注解)将请求URL映射到一个Action类。当请求到来时,Struts2会:
- 为这个请求创建一个新的
Action实例(默认是多例的)。 - 将请求参数通过
setter方法注入到这个Action实例的属性中。 - 调用
Action类中指定的execute方法(或通过method属性指定的其他方法)。
关键在于,Action类的所有属性(成员变量)在方法调用前就已经被注入值。这意味着,如果你在Action类中定义了多个处理不同请求的方法(通过@Action注解指定不同方法),这些方法共享同一个请求注入的成员变量。这种设计使得Action类更像一个“请求处理器”的容器,但方法间的隔离性较弱,容易引发线程安全问题(虽然Struts2通过每次请求创建新实例来规避,但编程模型上仍给人以“类级别”的感知)。
SpringMVC的“方法拦截”范式 SpringMVC则截然不同。它的核心处理单元是HandlerMethod,即Controller中的某一个具体方法。@RequestMapping注解直接映射到方法上。当请求到来时:
- SpringMVC容器中的Controller是单例的(默认情况下),由Spring IoC容器管理生命周期。
DispatcherServlet通过HandlerMapping找到对应的HandlerMethod。HandlerAdapter会动态解析该方法的参数(通过HandlerMethodArgumentResolver),从请求中提取数据并绑定,然后调用这个方法。- 方法的参数、局部变量都是线程安全的,因为它们存在于每个线程的调用栈中。
这种“方法级别”的拦截,带来了几个深远影响:
| 特性对比 | Struts2 (类级别) | SpringMVC (方法级别) |
|---|---|---|
| 映射粒度 | 较粗,以类为单位,方法通过额外配置指定 | 极细,直接映射到具体方法,更直观 |
| 默认作用域 | Action类多例(每次请求新实例) | Controller类单例(Spring Bean) |
| 线程安全关注点 | 类属性被所有方法共享,需注意(尽管实例不同) | 方法参数和局部变量天然线程安全 |
| RESTful支持 | 相对繁琐,需通过配置或约定区分HTTP方法 | 天然友好,@GetMapping, @PostMapping等注解直接支持 |
| 参数绑定 | 主要依赖OGNL表达式和类属性的setter注入 | 灵活多样,支持@RequestParam, @PathVariable, @RequestBody等注解,更符合Java习惯 |
| 与Spring集成 | 需要额外整合,是两个独立的框架 | 无缝集成,是Spring生态系统的一部分,可方便使用AOP、事务等 |
最直观的体验就是编写RESTful API。在SpringMVC里,你可以这样清晰地表达:
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) { ... } // GET 获取
@PostMapping
public User createUser(@RequestBody User user) { ... } // POST 创建
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) { ... } // PUT 更新
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) { ... } // DELETE 删除
}
同一个URL路径/api/users,通过不同的HTTP方法和路径变量,映射到同一个Controller类的不同方法上,结构清晰,意图明确。而在Struts2中实现同样的效果,配置或注解会显得更冗长,风格上也不够统一。
所以,“方法级别拦截”不仅仅是技术实现的不同,它推动了一种更现代、更清晰、更贴近HTTP协议本身的Web服务开发风格。这也是SpringMVC能成为主流的重要原因之一。
3. 核心控制器:为什么SpringMVC用Servlet,Struts2用Filter?这仅仅是技术选型问题吗?
这又是一个触及架构设计根本的问题。选择Servlet还是Filter作为核心控制器,背后是对请求处理链的定位和与Servlet容器集成方式的差异。
Struts2的Filter选择:前置拦截与包装 Struts2的核心控制器是StrutsPrepareAndExecuteFilter。Filter在Servlet规范中,是位于Servlet之前的组件,它可以对请求(ServletRequest)和响应(ServletResponse)进行包装和修改。
Struts2选择Filter,主要基于以下考虑:
- 更早的介入点:Filter可以在请求到达任何Servlet之前就进行处理,这使得Struts2能够完全掌控请求的入口,方便实现自己的请求处理流水线。
- 请求/响应包装能力:Struts2的很多功能,如通过拦截器(Interceptor)栈处理参数、验证、文件上传等,都需要对原生的
HttpServletRequest进行装饰(例如,将请求参数解析到ActionContext中)。Filter天生适合做这件事。 - 与Servlet解耦:理论上,Struts2的Filter可以处理所有请求,不依赖于后端具体的Servlet,框架的自主性更强。
但这种设计也带来了复杂性。开发者需要理解Struts2自己构建的一套上下文(ActionContext,


被折叠的 条评论
为什么被折叠?



