SpringMVC面试题
Spring MVC 请求处理流程:DispatcherServlet → HandlerMapping → HandlerAdapter → ViewResolver
面试官您好!Spring MVC 的核心请求处理流程可以用一句话概括:DispatcherServlet 作为总指挥,串联起 HandlerMapping、HandlerAdapter、ViewResolver 三大核心组件,完成从请求到响应的全链路处理。
🟢 先看一张总览图(心里得有这张图)
📌 一句话核心:前端控制器 DispatcherServlet 是总调度,拦截所有请求,按 “找谁干 → 怎么干 → 出结果 → 长啥样” 四步走。
核心流程总览 📊
各组件核心职责(面试必答点)✅
1. DispatcherServlet 🎯
// 所有请求入口(配置在web.xml或SpringBoot自动装配)
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>- 本质:一个特殊的 Servlet,是整个 Spring MVC 的入口和总指挥
- 核心作用:接收所有客户端请求,将请求分发到各个组件,统一处理异常
- 关键点:在 Spring Boot 中已经自动配置,无需手动在 web.xml 中注册
- 坑点:它就是个普通
Servlet,doService()调doDispatch(),所以线程不安全。⚠️ (你的 Controller 如果是单例也同理,别写共享状态)
2. HandlerMapping 🗺️
- 本质:请求与处理器的映射关系管理器
- 核心方法:
getHandler(HttpServletRequest request) - 核心作用:根据请求 URL 找到对应的 Handler(Controller 方法)和拦截器
- 关键点:
- 最常用实现:
RequestMappingHandlerMapping(处理@RequestMapping注解) - 返回的不是 Handler 本身,而是HandlerExecutionChain(执行链)
- 最常用实现:
- 返回的不是纯Handler,是
HandlerExecutionChain—— 包含目标处理器 + 拦截器链 🔗。 (这就解释了为什么拦截器能前后织入)
3. HandlerAdapter 🔌
- 本质:适配器模式的经典应用,解决不同类型 Handler 的统一调用问题
- 核心作用:按照统一的接口调用各种类型的 Handler
- 关键点:
- 最常用实现:
RequestMappingHandlerAdapter(处理 @RequestMapping 注解的方法) - 为什么需要适配器?因为 Handler 的类型千差万别(Controller、HttpRequestHandler、Servlet 等)
- 执行完返回
ModelAndView(哪怕是null,它也会包装)。
- 最常用实现:
- 为什么不直接调控制器? 因为 Spring 里 Handler 形态五花八门:
@Controller注解的类方法HttpRequestHandler接口Servlet等老式处理器
4. ViewResolver 🖼️
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
#用的是 InternalResourceViewResolver。
如果是前后分离,直接返回 @ResponseBody,走 HttpMessageConverter,根本不会走到这一步 🚫- 本质:逻辑视图名到物理视图的解析器,如 "
user/list")解析为真实View对象。 - 核心作用:根据 Controller 返回的逻辑视图名,找到对应的实际视图对象
- 关键点:
- 最常用实现:
InternalResourceViewResolver(解析 JSP) - 在前后端分离项目中,ViewResolver 通常不参与工作,Controller 直接返回 JSON 数据
- 最常用实现:
5. Controller 执行 —— 你写的业务 💼
- 方法参数绑定、数据校验、调用Service——全是这一步。
- 最后返回
ModelAndView(或直接@ResponseBody转JSON,此时后置处理会跳过视图解析)。
面试加分细节 💯
- 拦截器执行时机:在 Handler 执行前后、视图渲染前后都会执行拦截器的对应方法
- 异常处理流程:如果 Handler 执行过程中抛出异常,会由 HandlerExceptionResolver 进行处理
- 参数解析:HandlerAdapter 会调用 HandlerMethodArgumentResolver 来解析方法参数
- 返回值处理:HandlerAdapter 会调用 HandlerMethodReturnValueHandler 来处理方法返回值
❓ “异常了怎么办?”
👉 流程中有 HandlerExceptionResolver 链条,一旦抛异常会被它劫持,渲染错误视图或返回错误JSON。所以你的 @ControllerAdvice 就是在这里被调用的。
❓ “拦截器和过滤器的区别这里能体现吗?”
👉 过滤器在 DispatcherServlet 之前,是Tomcat级别;拦截器是框架级别,通过上面 HandlerExecutionChain 在 HandlerAdapter 调用前后织入。一图胜千言:
过滤器(前) → DispatcherServlet → 拦截器(pre) → HandlerAdapter → 控制器 → 拦截器(post) → 拦截器(after) → 过滤器(后)❓ “SpringBoot 怎么改这一套流程?”
👉 自动配置加载了 WebMvcAutoConfiguration,帮你配好了 DispatcherServlet、默认的 HandlerMapping、HandlerAdapter,你只需写 Controller,其他基本零配置 🧘
常用注解:@Controller、@RestController、@RequestMapping、@GetMapping 等
🎯面试官您好!接下来我会从核心作用、底层原理、使用场景、关键区别四个维度,为您系统讲解这几个 Spring MVC 最常用的注解。
📌 核心注解总览
这四个注解是 Spring MVC 处理 HTTP 请求的 "四大金刚",它们共同构成了 Web 层请求处理的核心骨架:
🔍 逐个注解深度解析
1️⃣ @Controller 🎮
核心作用:标识一个类为 Spring MVC 的控制器组件,负责接收用户请求并处理业务逻辑。
关键点:
- 本质是一个
@Component的派生注解,会被 Spring 容器自动扫描并实例化 - 方法默认返回视图名称(如 "index"),配合视图解析器渲染 HTML 页面
- 若要返回 JSON 数据,需要在方法上额外添加 @ResponseBody 注解
使用场景:传统的服务端渲染(SSR)项目,如 JSP、Thymeleaf 模板引擎开发。
@Controller
@RequestMapping("/user")
public class UserController {
@GetMapping("/profile")
public String userProfile(Model model) {
model.addAttribute("username", "张三");
return "user/profile"; // 返回视图名称
}
}2️⃣ @RestController 📡
核心作用:Spring 4.0 引入的组合注解,等价于 @Controller + @ResponseBody。
关键点:
- 类中所有方法的返回值都会被HttpMessageConverter自动转换为 JSON/XML 格式
- 不会走视图解析器流程,直接将数据写入 HTTP 响应体
- 是前后端分离项目中最常用的注解
使用场景:RESTful API 开发,前后端分离架构。
@RestController
@RequestMapping("/api/user")
public class UserApiController {
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.findById(id); // 直接返回User对象,自动转JSON
}
}3️⃣ @RequestMapping 🗺️
核心作用:映射 HTTP 请求 URL 到控制器的处理方法上,是最基础也是最灵活的请求映射注解。
关键点:
- 可标注在类和方法上,类上的路径会与方法上的路径拼接
- 支持所有 HTTP 方法:GET、POST、PUT、DELETE、PATCH 等
- 支持路径变量、请求参数、请求头、请求体等多种匹配方式
- 是所有 HTTP 方法特定注解的父注解
常用属性:
| 属性名 | 作用 | 示例 |
|---|---|---|
value/path | 请求 URL 路径 | @RequestMapping("/user") |
method | HTTP 方法 | method=RequestMethod.GET |
params | 请求参数匹配 | params="id=1" |
headers | 请求头匹配 | headers="Content-Type=application/json" |
produces | 响应内容类型 | produces="application/json;charset=UTF-8" |
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) { ... }
@PostMapping(consumes = "application/json")
public User createUser(@RequestBody User user) { ... }
}类上的 @RequestMapping("/users") 给所有方法加了前缀,避免了重复。
4️⃣ @GetMapping 🏃♂️
核心作用:Spring 4.3 引入的组合注解,专门用于处理HTTP GET请求。
关键点:
- 等价于
@RequestMapping(method=RequestMethod.GET) - 语义更清晰,代码更简洁
- 同系列还有:
@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping
使用场景:查询数据操作,符合 RESTful 规范。
// 等价于 @RequestMapping(value="/list", method=RequestMethod.GET)
@GetMapping("/list")
public List<User> getUserList() {
return userService.findAll();
}用一句话记住:
- @Controller 找模板,@RestController 返回数据。 🔑
- @RequestMapping 是老祖宗,其他都是语法糖。 🍬
流程图:请求如何被注解领走?🔄
这张图记在脑子里,面试时随手一画,技术深度立马上来了。📈
⚔️ 核心区别对比表
这是面试中最常被问到的对比问题,一定要记牢!
| 注解 | 组合关系 | 返回值处理 | 主要用途 | 适用架构 |
|---|---|---|---|---|
@Controller | @Component | 返回视图名称,走视图解析器 | 服务端渲染页面 | 传统 MVC |
@RestController | @Controller+@ResponseBody | 直接返回数据,自动转 JSON | 提供 RESTful API | 前后端分离 |
@RequestMapping | 基础注解 | 取决于类上的注解 | 通用请求映射 | 所有场景 |
@GetMapping | @RequestMapping(method=GET) | 取决于类上的注解 | 处理 GET 请求 | 查询操作 |
💡 面试加分点 & 常见陷阱 ✨
✅ 加分回答
- @RestController 的底层原理:它会为类中所有方法隐式添加
@ResponseBody注解,Spring 会在方法返回后,通过RequestMappingHandlerAdapter调用合适的HttpMessageConverter将返回值序列化为 JSON。 - @RequestMapping 的模糊匹配:支持 Ant 风格路径匹配(
?匹配单个字符,*匹配任意字符,**匹配任意多级路径)。 - RequestMappingHandlerMapping 在容器启动时扫描所有带
@Controller/@RequestMapping的 Bean,解析成RequestMappingInfo注册到映射表中。 - @ResponseBody 的消息转换流程,默认使用
MappingJackson2HttpMessageConverter(引入 Jackson 包时)把对象转成 JSON。 - HTTP 方法的语义规范:
- GET:查询资源,幂等
- POST:创建资源,非幂等
- PUT:更新资源(全量),幂等
- DELETE:删除资源,幂等
- PATCH:更新资源(部分),非幂等
❌ 常见陷阱
- 混淆 @Controller 和 @RestController:在 @Controller 类中忘记加 @ResponseBody 注解,导致返回的 JSON 字符串被当作视图名称处理,出现 404 错误。
- 路径重复映射:同一个 URL 被多个方法映射,启动时会抛出
IllegalStateException异常。 - GET 方法接收请求体:虽然技术上可以实现,但不符合 HTTP 规范,部分浏览器和代理服务器会拒绝 GET 请求携带请求体。
拦截器(Interceptor)与过滤器(Filter)区别
这是 Java 后端面试90% 会被问到的基础高频题,答得好直接加分,答得差直接减分!下面是我作为面试官最想听到的标准答案👇
🚀 一句话核心区别
过滤器 (Filter) 是 Servlet 规范的产物,拦截所有进入容器的请求;拦截器 (Interceptor) 是 Spring MVC 的产物,只拦截 Controller 层的请求。
📊 执行顺序流程图
🧩 一句话定位(先别懵,咱分层看)
- 过滤器(Filter):Servlet 级别的,请求压根没到 Spring 容器,它就能掐住。
- 拦截器(Interceptor):Spring 级别的,得进到
DispatcherServlet之后才能管事儿。
从流程就能看出本质:一个守大门,一个守二门 🚪
🍽️ 超接地气比喻
把整个 Web 请求流程比作去海底捞吃饭:
- 🛡️ Filter = 海底捞门口的保安:检查所有进入商场的人,不管你是来吃饭的、上厕所的还是路过的
- 🧑💼 Interceptor = 海底捞的服务员:只服务真正来吃饭的顾客,在你点菜前、上菜后、结账后提供服务
- 👨🍳 Controller = 厨师:负责给你做菜
- 🍳 Service = 后厨:负责具体烹饪
- 🛒 DAO = 采购员:负责采购食材
📋 详细对比表
| 对比维度 | 过滤器 (Filter) | 拦截器 (Interceptor) |
|---|---|---|
| 所属规范 | Servlet 规范 (JSR-315) | Spring MVC 框架 |
| 依赖容器 | 必须依赖 Tomcat/Jetty 等 Servlet 容器 | 依赖 Spring 容器,不依赖 Servlet 容器 |
| 拦截范围 | 几乎所有进入容器的请求 (静态资源、Servlet、Controller) | 仅拦截 Controller 层请求,不拦截静态资源 |
| 执行时机 | 请求进入 Servlet 之前,响应离开 Servlet 之后 | 请求进入 Controller 之前,视图渲染之前,请求完成之后 |
| 实现方式 | 实现javax.servlet.Filter接口 | 实现org.springframework.web.servlet.HandlerInterceptor接口 |
| 生命周期 | 由 Servlet 容器管理,应用启动时初始化一次 | 由 Spring 容器管理,支持单例 / 多例,可注入 Spring Bean |
| 功能侧重 | 通用型处理:编码转换、跨域配置、XSS 防护、日志记录 | 业务型处理:权限校验、登录拦截、参数校验、性能监控 |
| 能否获取 Spring 上下文 | 不能直接获取,需要手动注入 | 天然可以获取,直接 @Autowired 注入任何 Bean |
| 异常处理 | 只能处理 Filter 链和 Servlet 中的异常 | 可以处理 Controller 层的异常,但不能处理视图渲染异常 |
🎯 再深化一个点:细粒度 VS 粗粒度
Filter 能动手脚的是 ServletRequest 和 ServletResponse —— 你只能操作原始的请求/响应对象,想拿方法参数、返回值?做不到,太底层了。
Interceptor 则可以直接拿到 HandlerMethod 对象,反射看方法注解、参数注解、甚至修改返回值。做权限控制时,我经常在 preHandle 里读自定义注解判断权限,Filter 干这个非常别扭。🛠️
🧠 总结记忆口诀(面试官的小彩蛋)
“Filter 管生(Request进来)管死(Response出去),不管你是谁;Interceptor 认主,只保 Spring 的人(进过DispatcherServlet的请求)。” 😂
再送个表情包帮你记:
🚧 Filter: "统统停下,先过我这关!"
↓
🛂 Interceptor: "进了Spring的门,就得守我的规矩。"💡 面试官最爱追问的 4 个问题
1. 什么时候用 Filter?什么时候用 Interceptor?
✅ 用 Filter:需要对所有请求做统一处理,且与业务无关的场景
- 全局编码设置
CharacterEncodingFilter - 跨域资源共享
CorsFilter - 防止 XSS 攻击的过滤器
- 静态资源缓存控制
- 全局编码设置
✅ 用 Interceptor:需要与业务逻辑结合,只针对 Controller 的场景
- 用户登录状态校验
- 接口权限控制
- 接口访问日志记录
- 接口性能监控
2. 多个 Filter 和多个 Interceptor 的执行顺序是怎样的?
- Filter:按照
web.xml中配置的顺序从上到下执行,响应时从下到上执行 - Interceptor:按照注册的顺序执行
preHandle,逆序执行postHandle和afterCompletion
3. 为什么 Spring MVC 还要设计 Interceptor,直接用 Filter 不行吗?
Filter 是 Servlet 规范的,只能在 Servlet 前后执行,无法深入到 Spring MVC 的请求处理流程中。而 Interceptor 可以:
- 在 Controller 方法执行前后做更细粒度的控制
- 直接访问 Spring 容器中的 Bean
- 获取 Controller 方法的参数和返回值
- 与 Spring 的 AOP 机制无缝结合
4. 如果一个接口返回 404 了,Filter 和 Interceptor 都能捕获到吗?
- Filter 👉 能,因为请求进来了,还没到 Servlet 或找不到资源时,Filter 照样执行链里的 doFilter。404 作为一种响应状态,Filter 在返回路上照样能截获。
- Interceptor 👉 一般不行,
DispatcherServlet如果发现没有对应 Handler,直接抛异常或转发到错误页,你的preHandle都没机会跑。除非你专门配置了针对/**且处理了 noHandlerFound 的情况。
💥 这就能看出 拦截器依赖映射成功的 Handler。
⚠️ 常见踩坑点
- ❌ 不要在 Interceptor 中处理静态资源请求,会导致性能问题
- ❌ 不要在 Filter 中注入 Spring Bean,可能会导致注入失败
- ❌ 不要在
preHandle中抛出异常,应该返回false并设置响应 - ✅ 记得在
afterCompletion中释放资源,比如ThreadLocal
统一响应格式与全局异常处理
面试官您好!关于统一响应格式与全局异常处理,我从为什么做、怎么做、做了什么好处、踩过什么坑四个维度来给您汇报,这是我们后端开发的 "门面工程",也是项目规范化的第一步。
为什么必须做?🤔
如果没有统一处理,会出现什么问题?
- 前端同学要对接 N 种返回格式,心态爆炸 💥
- 异常信息直接暴露给用户,存在安全风险
- 错误码混乱,排查问题像大海捞针
- 代码中到处都是 try-catch,冗余且丑陋
统一响应格式设计 📦
1. 标准响应体结构
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
// 响应码:200成功,非200失败
private Integer code;
// 响应消息
private String message;
// 响应数据
private T data;
// 时间戳
private Long timestamp;
// 成功静态方法
public static <T> Result<T> success(T data) {
return new Result<>(200, "操作成功", data, System.currentTimeMillis());
}
// 失败静态方法
public static <T> Result<T> fail(Integer code, String message) {
return new Result<>(code, message, null, System.currentTimeMillis());
}
}核心要素就是这张表:
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
code | int | 业务状态码,我方定义,绝不是HTTP 200/500。 | 200 成功,4001 参数缺失,5001 系统繁忙 |
message | String | 给用户看的提示,或给开发的调试信息。 | “手机号不能为空” |
data | T(泛型) | 真正的业务数据,分页的话就装分页对象。 | { "id":1, "name":"张三" } |
✨ 这样前端只需判断 code == 200,然后安全地去 data 里拿东西,结构百年不变,彻底告别 Cannot read property of undefined。
2. 错误码设计规范
我一般采用三段式错误码设计,清晰明了:
| 错误码段 | 含义 | 示例 |
|---|---|---|
| 10000-19999 | 系统级错误 | 10001 = 系统内部错误,10002 = 参数校验失败 |
| 20000-29999 | 用户模块错误 | 20001 = 用户不存在,20002 = 密码错误 |
| 30000-39999 | 订单模块错误 | 30001 = 订单不存在,30002 = 订单已支付 |
| 40000-49999 | 商品模块错误 | 40001 = 商品不存在,40002 = 库存不足 |
@Getter
@AllArgsConstructor
public enum ResultCodeEnum {
SUCCESS(200, "操作成功"),
SYSTEM_ERROR(10001, "系统内部错误"),
PARAM_VALID_ERROR(10002, "参数校验失败"),
USER_NOT_EXIST(20001, "用户不存在"),
PASSWORD_ERROR(20002, "密码错误");
private final Integer code;
private final String message;
}全局异常处理实现 🛡️
SpringBoot 提供了@RestControllerAdvice+@ExceptionHandler的完美解决方案,这是目前最主流的实现方式。
1. 核心实现代码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 处理自定义业务异常
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.error("业务异常:{}", e.getMessage(), e);
return Result.fail(e.getCode(), e.getMessage());
}
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("参数校验异常:{}", e.getMessage(), e);
String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
return Result.fail(ResultCodeEnum.PARAM_VALID_ERROR.getCode(), message);
}
// 处理所有未捕获的异常(兜底)
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常:{}", e.getMessage(), e);
// 生产环境不要返回具体异常信息,防止泄露敏感信息
return Result.fail(ResultCodeEnum.SYSTEM_ERROR.getCode(), "系统繁忙,请稍后再试");
}
}2. 自定义业务异常
@Data
@EqualsAndHashCode(callSuper = true)
public class BusinessException extends RuntimeException {
private Integer code;
private String message;
public BusinessException(ResultCodeEnum resultCodeEnum) {
this.code = resultCodeEnum.getCode();
this.message = resultCodeEnum.getMessage();
}
public BusinessException(Integer code, String message) {
this.code = code;
this.message = message;
}
}🔑 关键点:
BusinessException是业务和开发的沟通契约,服务层发现问题直接throw new BusinessException(4001,"手机号已注册"),不脏Controller。- 参数校验直接用 Spring Validation 注解(
@NotBlank等),异常全交由全局处理器统一转换,不用手写任何校验代码。 - 兜底异常必须记录完整堆栈,这是未来系统健壮性的命根子。
3. 处理流程示意图
进阶优化点 ✨
- 统一响应体增强:使用
ResponseBodyAdvice对所有 Controller 返回值自动包装,不用每个方法都写Result.success() - 异常信息国际化:支持多语言错误信息
- 异常告警:关键异常发生时自动发送邮件 / 短信 / 钉钉告警
- 链路追踪:在响应体中加入 traceId,方便分布式系统排查问题
- 参数校验分组:同一个 DTO 在不同场景下有不同的校验规则
常见坑点 ⚠️
- ❌ 不要把所有异常都吞掉,该抛的一定要抛
- ❌ 生产环境不要返回完整的异常堆栈信息
- ❌ 错误码不要随意定义,一定要有统一规范
- ❌ 不要在异常处理器中写业务逻辑
- ❌ 不要忘记处理 404、401、403 等 HTTP 状态码异常
升华:不仅仅是少写几行代码
面试官: “那除了少写 try-catch,你觉得这套机制更大的价值在哪?”
我: “我认为有几点:
- 安全感 🔒:任何一条路径抛出的异常,都绝对会变成我们约定的 Result 格式返回,架构层面的确定性大大增加。
- 可观测性 👁️:可以在全局处理器里统一做异常报警、链路追踪ID注入、错误码标准化,是监控体系的优质数据源。
- 职责清晰 ✂️:Controller 层终于可以干它该干的事:接参数、调服务、返回结果,而不用背负处理各种奇葩异味的包袱。
- 扩展性强 🧩:将来加国际化(i18n),只需在
BusinessException里扔个 message key,全局处理器里去模板引擎拼消息即可,改动极小。”
