目录
[Spring Boot统一功能处理详解(新手完整版)](#Spring Boot统一功能处理详解(新手完整版))
[1. 拦截器详解](#1. 拦截器详解)
[1.1 什么是拦截器](#1.1 什么是拦截器)
[1.2 完整代码实现(逐行注释)](#1.2 完整代码实现(逐行注释))
[1.2.1 定义登录拦截器(传统Session方式)](#1.2.1 定义登录拦截器(传统Session方式))
[1.2.3 定义登录拦截器(现代Token方式,推荐)](#1.2.3 定义登录拦截器(现代Token方式,推荐))
[1.3 注册拦截器到Spring MVC](#1.3 注册拦截器到Spring MVC)
[2. 统一数据返回格式](#2. 统一数据返回格式)
[2.1 为什么需要统一格式?](#2.1 为什么需要统一格式?)
[2.2 完整实现代码](#2.2 完整实现代码)
[2.2.1 统一结果类Result](#2.2.1 统一结果类Result)
[2.2.2 全局响应处理器ResponseAdvice](#2.2.2 全局响应处理器ResponseAdvice)
[3. 统一异常处理](#3. 统一异常处理)
[3.1 为什么需要统一处理?](#3.1 为什么需要统一处理?)
[3.2 完整实现代码](#3.2 完整实现代码)
[3.2.1 全局异常处理器](#3.2.1 全局异常处理器)
[3.2.2 自定义业务异常](#3.2.2 自定义业务异常)
[4. 完整项目结构示例](#4. 完整项目结构示例)
[5. 知识点与代码的完整对应关系表](#5. 知识点与代码的完整对应关系表)
[6. 新手最容易踩的坑](#6. 新手最容易踩的坑)
[6.1 拦截器不生效](#6.1 拦截器不生效)
[6.2 String类型异常](#6.2 String类型异常)
[6.3 异常没捕获](#6.3 异常没捕获)
[7. 总结与最佳实践](#7. 总结与最佳实践)
[7.1 三者的协作流程图](#7.1 三者的协作流程图)
[7.2 代码层面的最佳实践](#7.2 代码层面的最佳实践)
[8. 进阶:为什么 Spring MVC 需要适配器模式?(选读)](#8. 进阶:为什么 Spring MVC 需要适配器模式?(选读))
Spring Boot统一功能处理详解(新手完整版)
前置准备 :本教程使用 Lombok 简化代码(如
@Data,@Slf4j),请务必在pom.xml中添加依赖,并在 IDE 中安装 Lombok 插件,否则代码会报错。
java<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
1. 拦截器详解
1.1 什么是拦截器
拦截器是Spring MVC提供的"安检门"机制,能在请求到达Controller之前、之后以及请求完成时插入自定义逻辑。它就像你去商场时要经过的安检:安检前检查包裹(preHandle),安检后刷卡(postHandle),离开时记录时间(afterCompletion)。
核心应用场景:
-
登录认证:检查用户是否登录(如未登录不能访问订单页面)
-
日志记录:记录每个请求的处理时间
-
权限控制:判断用户是否有权限访问某个接口
-
性能监控:统计接口响应时间
1.2 完整代码实现(逐行注释)
1.2.1 定义登录拦截器(传统Session方式)
⚠️ 注意 :Session方式适合传统Web页面项目。对于现代前后端分离 项目(Vue/React + Spring Boot),推荐使用 JWT Token 方案(见1.2.3节)。
java
// import关键字:导入其他包中的类,就像你要用别人的工具得先拿来
// slf4j:Simple Logging Facade for Java,日志门面框架,类似一个日志的"翻译官"
// 它能让你在不改代码的情况下切换log4j、logback等具体实现
import lombok.extern.slf4j.Slf4j;
// Spring框架的组件注解,标记这个类为Spring管理的Bean(就像商品贴上条形码入库)
// Spring容器会自动创建它的实例,其他地方可以直接"借用"
import org.springframework.stereotype.Component;
// Spring MVC的核心接口,实现它就拥有了拦截请求的能力
// 类似"安检员资格证",只有拿到这个证才能在指定位置检查
import org.springframework.web.servlet.HandlerInterceptor;
// Servlet规范提供的HTTP请求对象,封装了客户端发送的所有信息
// 包括请求头、参数、Cookie等,相当于"快递包裹单"
import jakarta.servlet.http.HttpServletRequest;
// Servlet规范提供的HTTP响应对象,用于向客户端返回数据
// 相当于"快递回执单",你可以填写返回内容和状态
import jakarta.servlet.http.HttpServletResponse;
// Session是会话对象,用于在多次请求间保存用户状态
// 就像商场的储物柜,存一次东西,多次取(前提是有钥匙)
import jakarta.servlet.http.HttpSession;
/**
* 登录拦截器(Session版)
* @Slf4j:Lombok注解,自动生成日志记录器
* @Component:让Spring管理这个拦截器
*/
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
/**
* preHandle:在Controller方法执行前调用(安检门第一道关卡)
* 返回true = 放行(绿灯),返回false = 拦截(红灯)
* * @param request HTTP请求对象(包裹单)
* @param response HTTP响应对象(回执单)
* @param handler 要执行的Controller方法(目标商店)
* @return boolean 是否允许通过
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor.preHandle() - 开始检查用户登录状态, URI: {}", request.getRequestURI());
// 获取Session,参数false表示"没有就别新建"
HttpSession session = request.getSession(false);
// 检查Session是否存在且包含用户信息
// &&是短路与,左边为false右边不执行(避免空指针)
if (session != null && session.getAttribute("user") != null) {
log.info("用户已登录,放行请求");
return true; // 放行
}
// 没登录,设置401状态码(Unauthorized,未授权)
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // SC_UNAUTHORIZED就是401的常量
log.warn("用户未登录,请求被拦截");
return false; // 拦截,不执行后续操作
}
// ... postHandle和afterCompletion方法同上,省略 ...
}
1.2.3 定义登录拦截器(现代Token方式,推荐)
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* 登录拦截器(JWT Token版 - 前后端分离项目推荐)
* 从请求头中获取Token进行验证
*/
@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("TokenInterceptor.preHandle() - 开始验证Token, URI: {}", request.getRequestURI());
// 从请求头中获取Token(前端在Header中传递:Authorization: Bearer xxxx)
String token = request.getHeader("Authorization");
// 简单的Token验证逻辑(实际应调用JWT工具类解析)
if (token != null && token.startsWith("Bearer ") && validateToken(token)) {
log.info("Token验证通过,放行请求");
return true;
}
// Token无效或过期
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
log.warn("Token验证失败,请求被拦截");
return false;
}
/**
* 模拟Token验证方法(实际应使用JWT库解析)
* @param token 请求头中的Token字符串
* @return 是否有效
*/
private boolean validateToken(String token) {
// 实际业务:解析JWT,验证签名是否有效、是否过期
// 例如:return JwtUtil.verify(token.replace("Bearer ", ""));
return "Bearer valid-token".equals(token); // 模拟实现
}
}
1.3 注册拦截器到Spring MVC
java
// ... import 语句(略) ...
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor; // 或 TokenInterceptor
// 定义不需要拦截的路径列表
private List<String> excludePaths = Arrays.asList(
"/user/login", // 登录接口
"/user/register", // 注册接口
"/api/auth/refresh", // Token刷新接口(Token方式需要)
"/static/**", // 静态资源
"/error/**" // 错误页面
);
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor) // 或 tokenInterceptor
.addPathPatterns("/**") // 先全部拦截
.excludePathPatterns(excludePaths); // 再开放白名单
log.info("拦截器注册完成,已应用登录验证");
}
}
2. 统一数据返回格式
2.1 为什么需要统一格式?
想象你是前端开发,后端同事张三返回{"name":"张三"},李四返回"李四",王五返回true。你要写三种不同的解析逻辑,维护成本极高!
统一格式后,所有接口都返回:
java
{
"status": 200,
"data": "实际数据",
"errorMessage": "",
"timestamp": 1234567890
}
前端只需写一套解析逻辑:取data字段即可。
2.2 完整实现代码
2.2.1 统一结果类Result
java
import lombok.Data;
/**
* 统一返回结果类
* @param <T> 泛型,表示data字段可以是任何类型
*/
@Data
public class Result<T> {
private int status;
private String errorMessage;
private T data;
private long timestamp;
private Result() {
this.timestamp = System.currentTimeMillis();
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setStatus(200);
result.setData(data);
return result;
}
public static <T> Result<T> fail(String errorMessage) {
Result<T> result = new Result<>();
result.setStatus(500);
result.setErrorMessage(errorMessage);
return result;
}
public static <T> Result<T> custom(int status, String errorMessage, T data) {
Result<T> result = new Result<>();
result.setStatus(status);
result.setErrorMessage(errorMessage);
result.setData(data);
return result;
}
}
2.2.2 全局响应处理器ResponseAdvice
java
// ... import 语句(略) ...
@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
// Jackson转换器(处理String类型专用)
private static ObjectMapper mapper = new ObjectMapper();
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true; // 处理所有返回值
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 1. 如果已经是Result类型,直接返回(避免重复包装)
if (body instanceof Result) {
return body;
}
// 2. ❗❗❗ String类型特殊处理(重要!易错点!)
// Spring的StringHttpMessageConverter只接受String,不接受对象
// 必须先将Result转为JSON字符串,否则会报类型转换异常
if (body instanceof String) {
return mapper.writeValueAsString(Result.success(body));
}
// 3. 其他类型正常包装
return Result.success(body);
}
}
3. 统一异常处理
3.1 为什么需要统一处理?
⚠️ 反模式警告 :以下代码仅供对比理解 ,实际开发中严禁在每个接口里写 try-catch!
java
// ❌ 错误示范:重复代码,维护噩梦,不要这样做!
@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
try {
// 业务逻辑
User user = userService.findById(id);
return Result.success(user);
} catch (Exception e) {
log.error("错误", e);
return Result.fail("系统错误"); // 每个接口都要重复!
}
}
统一异常处理就像一个"中央错误处理中心",所有未捕获的异常都汇集到这里处理,Controller层只需专注业务逻辑。
3.2 完整实现代码
3.2.1 全局异常处理器
java
@Slf4j
@ResponseBody
@ControllerAdvice
public class ErrorAdvice {
/**
* 兜底处理器:处理所有未被捕获的异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Object> handleGeneralException(Exception e) {
log.error("系统发生未处理异常: {}", e.getMessage(), e);
return Result.fail("系统繁忙,请稍后再试");
}
/**
* 处理业务异常(自定义异常)
*/
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Object> handleBusinessException(BusinessException e) {
log.warn("业务异常[错误码:{}]: {}", e.getErrorCode(), e.getMessage());
return Result.custom(e.getErrorCode(), e.getMessage(), null);
}
/**
* 处理参数校验异常
*/
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Object> handleIllegalArgumentException(IllegalArgumentException e) {
log.warn("参数错误: {}", e.getMessage());
return Result.fail("参数错误: " + e.getMessage());
}
}
3.2.2 自定义业务异常
java
/**
* 业务异常基类(自定义异常)
*/
public class BusinessException extends RuntimeException {
private int errorCode;
public BusinessException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public int getErrorCode() {
return errorCode;
}
}
/**
* 资源未找到异常
*/
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String resourceName, Object id) {
super(404, String.format("%s[id=%s]不存在", resourceName, id));
}
}
4. 完整项目结构示例
java
src/main/java/com/example/demo/
├── DemoApplication.java // 启动类
├── config/
│ ├── WebConfig.java // 拦截器注册
│ └── ResponseAdvice.java // 统一返回
├── controller/
│ ├── UserController.java // 用户接口
│ └── BookController.java // 图书接口
├── interceptor/
│ └── LoginInterceptor.java // 登录拦截(或TokenInterceptor)
├── exception/
│ ├── ErrorAdvice.java // 统一异常
│ └── BusinessException.java // 业务异常
├── model/
│ ├── Result.java // 统一结果
│ └── User.java // 用户实体
└── constant/
└── Constants.java // 常量类
5. 知识点与代码的完整对应关系表
|------------|-------------------------------------------------|------------------------------------|---------------------------------------------------------------------------------|
| 知识点 | 结论 | 代码体现位置 | 代码如何体现 |
| 拦截器执行顺序 | preHandle→Controller→postHandle→afterCompletion | DispatcherServlet.doDispatch()源码 | 方法按顺序调用,applyPreHandle()在ha.handle()之前,applyPostHandle()在之后 |
| 统一返回包装 | 所有返回值变成Result | ResponseAdvice.beforeBodyWrite() | 通过instanceof判断类型,调用Result.success()包装 |
| String类型问题 | String需特殊处理防止转换异常 | if (body instanceof String)分支 | 主动调用ObjectMapper.writeValueAsString()转为JSON字符串,适配StringHttpMessageConverter |
| 异常处理优先级 | 子类异常优先匹配 | @ExceptionHandler的顺序 | Spring通过ExceptionDepthComparator比较异常继承深度,深度小的优先 |
| 开闭原则 | 对扩展开放,对修改关闭 | 新增XxxHandlerAdapter类 | 添加新Controller类型时,只需新增适配器类,无需修改DispatcherServlet源码 |
6. 新手最容易踩的坑
6.1 拦截器不生效
-
问题 :
LoginInterceptor写了但没效果。 -
原因 :没实现
WebMvcConfigurer或没加@Configuration。 -
代码检查点 :确认
WebConfig类上有@Configuration且实现了WebMvcConfigurer。
6.2 String类型异常
-
问题 :返回String时报错
ClassCastException。 -
原因 :没做
instanceof String判断。 -
代码检查点 :确认
ResponseAdvice中有if (body instanceof String)分支。
6.3 异常没捕获
-
问题:抛了异常但返回500错误。
-
原因 :
@ControllerAdvice没扫描到或异常类型不匹配。 -
代码检查点 :确认
ErrorAdvice在Spring Boot主启动类的同级或子包下。
7. 总结与最佳实践
7.1 三者的协作流程图
java
用户请求
↓
LoginInterceptor.preHandle() (登录检查/Token验证)
↓ (放行)
Controller执行业务(专注业务,不try-catch)
↓ (抛BusinessException)
ErrorAdvice捕获异常 → 返回Result.fail()
↓ (正常返回对象)
ResponseAdvice.beforeBodyWrite() → 包装成Result.success()
↓
返回给前端统一格式 {status, data, errorMessage, timestamp}
7.2 代码层面的最佳实践
java
// ✅ Controller层:保持简洁,只关注业务
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) { // 直接返回User,不包Result
// 参数校验不通过就抛IllegalArgumentException
if(id <= 0) throw new IllegalArgumentException("ID无效");
// Service层可能抛BusinessException
User user = userService.getById(id);
// 直接返回数据,由ResponseAdvice统一包装
return user;
}
}
最佳实践总结:
-
Controller层 :不手动包装
Result,专注业务逻辑,异常直接抛 -
Service层 :抛出自定义
BusinessException,代码语义清晰 -
全局层 :
ResponseAdvice和ErrorAdvice做统一处理,代码复用性高
8. 进阶:为什么 Spring MVC 需要适配器模式?(选读)
本小节为设计原理探讨,不影响实战开发,可跳过
在Spring MVC底层,DispatcherServlet需要处理多种类型的Controller(如老式Controller接口、注解@Controller、HttpRequestHandler等)。如果没有适配器模式,DispatcherServlet会充满if-else判断:
java
// ❌ 假设没有适配器模式的噩梦代码
public void doDispatch(HttpServletRequest request, HttpServletResponse response) {
Object handler = getHandler(request);
if (handler instanceof Controller) {
((Controller) handler).handleRequest(request, response);
} else if (handler instanceof HttpRequestHandler) {
((HttpRequestHandler) handler).handleRequest(request, response);
} else if (handler.getClass().isAnnotationPresent(Controller.class)) {
// 复杂的反射调用逻辑...
}
// 每新增一种Controller类型,都要修改这段代码!
}
适配器模式解决方案:
-
HandlerAdapter接口统一了调用方式(supports()+handle()) -
DispatcherServlet只需调用adapter.handle(),无需关心具体类型 -
新增Controller类型时,只需新增一个适配器类 ,无需修改
DispatcherServlet源码
结论 :适配器模式的本质是将调用的复杂性从调用者(DispatcherServlet)转移到适配器(HandlerAdapter)中 ,让核心逻辑保持稳定,符合开闭原则(对扩展开放,对修改关闭)。