目录
[Spring Boot统一功能处理详解(新手完整版)](#Spring Boot统一功能处理详解(新手完整版))
[1. 拦截器详解](#1. 拦截器详解)
[1.1 什么是拦截器](#1.1 什么是拦截器)
[1.2 完整代码实现(逐行注释)](#1.2 完整代码实现(逐行注释))
[1.2.1 定义登录拦截器](#1.2.1 定义登录拦截器)
[1.2.2 注册拦截器到Spring MVC](#1.2.2 注册拦截器到Spring MVC)
[1.3 拦截器执行流程图解](#1.3 拦截器执行流程图解)
[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)
[2.3 String类型问题的代码体现](#2.3 String类型问题的代码体现)
[3. 统一异常处理](#3. 统一异常处理)
[3.1 为什么需要统一处理?](#3.1 为什么需要统一处理?)
[3.2 完整实现代码](#3.2 完整实现代码)
[3.2.1 全局异常处理器](#3.2.1 全局异常处理器)
[3.2.2 自定义业务异常](#3.2.2 自定义业务异常)
[3.3 异常处理调用链演示](#3.3 异常处理调用链演示)
[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. 最后的话](#8. 最后的话)
这里是为您生成的Markdown文档,内容完全按照您的要求整理,保留了所有代码注释和详细讲解。
Spring Boot Unified Function Handling
Spring Boot统一功能处理详解(新手完整版)
我会整合拦截器、统一返回格式和异常处理三部分内容,提供带逐行注释的完整代码,并详细说明每个知识点如何体现在代码中。
1. 拦截器详解
1.1 什么是拦截器
拦截器是Spring MVC提供的"安检门"机制,能在请求到达Controller之前、之后以及请求完成时插入自定义逻辑。它就像你去商场时要经过的安检:安检前检查包裹(preHandle),安检后刷卡(postHandle),离开时记录时间(afterCompletion)。
核心应用场景:
-
登录认证:检查用户是否登录(如未登录不能访问订单页面)
-
日志记录:记录每个请求的处理时间
-
权限控制:判断用户是否有权限访问某个接口
-
性能监控:统计接口响应时间
1.2 完整代码实现(逐行注释)
1.2.1 定义登录拦截器
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;
/**
* 登录拦截器
* @Slf4j:Lombok注解,自动生成日志记录器log,不用写LoggerFactory.getLogger()
* @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表示"没有就别新建"
// 就像找储物柜钥匙,false表示"找不到就别给我新钥匙"
HttpSession session = request.getSession(false);
// 检查Session是否存在且包含用户信息
// &&是短路与,左边为false右边不执行(避免空指针)
if (session != null && session.getAttribute("user") != null) {
log.info("用户已登录,放行请求");
return true; // 放行,继续执行Controller里的方法
}
// 没登录,设置401状态码(Unauthorized,未授权)
// 就像商场保安说"请出示会员卡"
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // SC_UNAUTHORIZED就是401的常量
log.warn("用户未登录,请求被拦截");
return false; // 拦截,不执行后续操作
}
/**
* postHandle:在Controller方法执行后、视图渲染前调用(第二道关卡)
* 可以修改ModelAndView里的数据或视图名称
* 就像买完东西后,可以在包装袋上加点装饰
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
org.springframework.web.servlet.ModelAndView modelAndView) throws Exception {
log.info("LoginInterceptor.postHandle() - Controller执行完毕,准备渲染视图");
// 示例:可以给所有页面统一添加当前用户信息
if (modelAndView != null) {
modelAndView.addObject("currentTime", System.currentTimeMillis());
}
}
/**
* afterCompletion:在整个请求完成后调用(最后关卡)
* 视图已经渲染完毕,客户端已经收到响应
* 通常用于资源清理,比如关闭流、记录最终日志
* 就像顾客离开商场后,保安做收尾工作
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
log.info("LoginInterceptor.afterCompletion() - 请求处理完成,状态码: {}", response.getStatus());
// 如果有异常,可以在这里记录
if (ex != null) {
log.error("请求处理过程中发生异常", ex);
}
}
}
代码如何体现拦截器特性?
-
implements HandlerInterceptor:直接实现接口,这是Java的"契约编程",相当于签了合同就必须实现三个方法。 -
preHandle返回true/false:核心机制,代码中通过if(session...)判断来决定是否放行,这就是"拦截"的本质。 -
三个方法的执行顺序:通过日志可以观察到,Spring MVC框架保证了先执行preHandle,再执行Controller,然后postHandle,最后afterCompletion。
1.2.2 注册拦截器到Spring MVC
java
// Spring的依赖注入注解,自动从容器中找LoginInterceptor实例并注入
// 就像你点外卖,@Autowired表示"平台自动分配骑手",你不用自己找
import org.springframework.beans.factory.annotation.Autowired;
// 标记类为配置类,替代传统的XML配置文件
// @Configuration = "这是一个配置文件,Spring启动时要读取"
import org.springframework.context.annotation.Configuration;
// 拦截器注册表,用于添加和管理拦截器
// 就像商场的"安检门管理中心",可以指定哪些门需要安检
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
// Spring MVC配置接口,实现它可以自定义MVC行为(如添加拦截器、资源处理器等)
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// 工具类,用于创建不可变列表
import java.util.Arrays;
import java.util.List;
/**
* Web配置类
* @Configuration:告诉Spring"我是个配置类,启动时加载我"
* implements WebMvcConfigurer:表示"我要自定义Spring MVC的行为"
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
// @Autowired:自动注入Spring容器中已经存在的LoginInterceptor对象
// 这里不需要new,Spring会帮我们创建好并注入
@Autowired
private LoginInterceptor loginInterceptor;
// 定义不需要拦截的路径列表
// Arrays.asList():快速创建固定大小的列表
// 就像列个"免检名单",名单上的人不用过安检
private List<String> excludePaths = Arrays.asList(
"/user/login", // 登录接口本身不能拦截,否则无法登录
"/user/register", // 注册接口
"/static/**", // 静态资源(CSS/JS/图片)不用拦截
"/error/**" // 错误页面
);
/**
* addInterceptors:重写父类方法,用于注册拦截器
* Spring MVC启动时会自动调用这个方法
* * @param registry 拦截器注册表(安检门管理中心)
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor():添加拦截器
// addPathPatterns("/**"):拦截所有路径(**表示任意层级子路径)
// excludePathPatterns():排除指定路径(白名单)
// 链式调用:像搭积木一样连续配置
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // 先全部拦截
.excludePathPatterns(excludePaths); // 再开放部分路径
log.info("拦截器注册完成,已应用登录验证");
}
}
代码如何体现配置灵活性?
-
/**的匹配规则 :代码中通过字符串模式匹配实现,/表示路径分隔符,*通配符表示匹配任意字符。 -
excludePathPatterns():代码中通过列表排除,实现了"黑名单"机制,这是实际项目中最常用的方式。 -
@Configuration:Spring的约定,启动时扫描所有标记了此注解的类,自动执行配置方法。
1.3 拦截器执行流程图解
java
用户请求 → Tomcat → DispatcherServlet → applyPreHandle()
↓ (返回false则中断) ↓ (返回true则继续)
拦截器preHandle() Controller方法执行
↓ ↓
拦截器postHandle() ←─────────── 方法返回
↓
视图渲染
↓
拦截器afterCompletion() ←─────── 请求完成
代码体现 :在DispatcherServlet.doDispatch()方法中(Spring源码),三个方法被按顺序调用,这就是结论"拦截器有固定执行顺序"的实现依据。
2. 统一数据返回格式
2.1 为什么需要统一格式?
想象你是前端开发,后端同事张三返回{"name":"张三"},李四返回"李四",王五返回true。你要写三种不同的解析逻辑,维护成本极高!
统一格式后,所有接口都返回:
java
{
"status": 200,
"data": "实际数据",
"errorMessage": "",
"timestamp": 1234567890
}
前端只需写一套解析逻辑:取data字段即可。
2.2 完整实现代码
2.2.1 统一结果类Result
java
// Lombok的@Data注解,自动生成getter/setter/toString/equals/hashCode方法
// 相当于让Lombok帮你写样板代码,你只需关注业务字段
import lombok.Data;
/**
* 统一返回结果类(模板)
* @param <T> 泛型,表示data字段可以是任何类型(String、User、List等)
* 就像快递盒,可以装任何东西
*/
@Data
public class Result<T> {
// int:状态码,用数字表示结果(200成功,500失败等)
private int status;
// String:错误信息,失败时告诉用户/前端具体原因
private String errorMessage;
// T:泛型,实际业务数据,成功时存放返回内容
private T data;
// long:时间戳,记录响应的毫秒时间,用于调试和监控
private long timestamp;
/**
* 私有构造方法:防止外部直接new Result()
* 强制使用工厂方法创建,保证统一性
*/
private Result() {
// System.currentTimeMillis():获取当前系统时间的毫秒值
// 从1970年1月1日00:00:00到现在的总毫秒数
this.timestamp = System.currentTimeMillis();
}
/**
* 静态工厂方法:创建成功响应
* static:类方法,无需创建对象直接调用 Result.success()
* <T>:泛型方法,让编译器自动推断T的类型
* * @param data 要返回的业务数据
* @return 包装后的统一结果
*/
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setStatus(200); // HTTP状态码200表示成功
result.setData(data); // 将业务数据放入data字段
// errorMessage保持null,表示没有错误
return result;
}
/**
* 静态工厂方法:创建失败响应
*/
public static <T> Result<T> fail(String errorMessage) {
Result<T> result = new Result<>();
result.setStatus(500); // HTTP状态码500表示服务器错误
result.setErrorMessage(errorMessage); // 设置错误详情
// data保持null
return result;
}
/**
* 自定义状态码和数据的响应
* 用于特殊场景,如参数验证失败400,未授权401
*/
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;
}
}
代码如何体现统一性?
-
私有构造函数 :代码中的
private Result()强制所有创建必须通过success()/fail()方法,这就是"统一"的制度保障。 -
泛型
<T>:代码中的泛型设计让Result能包装任何类型,这是"通用"的实现手段。 -
工厂方法 :
static方法让创建点集中,便于后续添加统一逻辑(如自动填充traceId)。
2.2.2 全局响应处理器ResponseAdvice
java
// Spring核心类:方法参数描述,包含返回值的类型、注解等信息
// 就像"产品说明书",告诉你这个方法的返回值是什么
import org.springframework.core.MethodParameter;
// HTTP媒体类型,如application/json, text/html
// 决定返回什么格式的数据
import org.springframework.http.MediaType;
// 服务器端HTTP请求抽象,Spring封装后的请求对象
import org.springframework.http.server.ServerHttpRequest;
// 服务器端HTTP响应抽象,Spring封装后的响应对象
import org.springframework.http.server.ServerHttpResponse;
// @ControllerAdvice:控制器通知,对所有的Controller生效
// 类似"广播站",向所有Controller发送统一指令
import org.springframework.web.bind.annotation.ControllerAdvice;
// ResponseBodyAdvice:响应体增强接口,在返回数据写入响应前进行修改
// 就像"包装工",在商品出厂前统一装箱
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
// Jackson库的核心类,用于Java对象和JSON字符串互转
// 就像"翻译官",把Java对象翻译成JSON语言
import com.fasterxml.jackson.databind.ObjectMapper;
// Lombok的@SneakyThrows注解:自动处理受检异常,不用写try-catch
// 简化代码,但不建议在复杂场景使用
import lombok.SneakyThrows;
// Lombok的日志注解
import lombok.extern.slf4j.Slf4j;
/**
* 全局响应处理器
* @ControllerAdvice:这个类会影响所有的Controller返回值
* @Slf4j:记录日志
*/
@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
/**
* ObjectMapper是Jackson的核心类,线程安全,可以共享一个实例
* 用于将Result对象转换为JSON字符串
*/
private static ObjectMapper mapper = new ObjectMapper();
/**
* supports方法:决定是否要执行beforeBodyWrite
* return true表示"所有返回值我都要处理"
* return false表示"这个返回值我不处理,原样返回"
* * @param returnType 方法返回类型(产品说明书)
* @param converterType 消息转换器类型(翻译官类型)
* @return 是否处理
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 这里我们统一处理所有返回类型,所以直接返回true
// 实际项目中可以根据注解、包名等条件过滤
return true;
}
/**
* beforeBodyWrite:在响应体写入前执行(核心方法)
* 这是"装箱"的地方,把原始数据包装成Result
* * @param body 原始的返回值(可能要包装的商品)
* @param returnType 方法返回类型
* @param selectedContentType 选中的内容类型(如application/json)
* @param selectedConverterType 选中的转换器
* @param request 请求对象
* @param response 响应对象
* @return 包装后的对象
*/
@SneakyThrows // 自动抛出异常,简化代码
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
log.info("ResponseAdvice: 包装返回值, 原始类型={}", body != null ? body.getClass().getName() : "null");
// 1. 如果已经是Result类型,说明已经被包装过了(可能是手动返回的)
// instanceof:Java的实例判断关键字,"你是Result吗?"
if (body instanceof Result) {
log.info("已经是Result类型,无需包装");
return body; // 直接返回,避免重复包装
}
// 2. 如果是String类型,需要特殊处理(坑!重点!)
// 因为String类型的返回值会被StringHttpMessageConverter处理
// 它只接受String,不接受Result对象
if (body instanceof String) {
log.info("String类型,使用Jackson转为JSON字符串");
// 先包装成Result,再用ObjectMapper转为JSON字符串
// writeValueAsString:将Java对象转为JSON字符串
return mapper.writeValueAsString(Result.success(body));
}
// 3. 其他类型(Object、List、自定义类等)
// 直接调用Result.success()包装
log.info("普通对象类型,直接包装");
return Result.success(body);
}
}
代码如何体现统一包装逻辑?
-
implements ResponseBodyAdvice<Object>:代码层面的契约,Spring MVC保证所有Controller返回前都会调用此类的beforeBodyWrite。 -
if (body instanceof Result):代码中的短路逻辑,避免重复包装,这是"统一"的保护机制。 -
if (body instanceof String):代码中的特殊处理,这是解决String类型转换异常的关键,体现了对Spring内部机制的深入理解。
2.3 String类型问题的代码体现
问题根源代码层面分析:
在org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport中,Spring注册了默认的消息转换器:
java
// Spring源码片段(注释说明)
protected final void addDefaultHttpMessageConverters(List<Converter> converters) {
// 1. ByteArray转换器,处理byte[]
converters.add(new ByteArrayHttpMessageConverter());
// 2. String转换器,处理String(优先级高!)
converters.add(new StringHttpMessageConverter());
// 3. 如果Classpath中有Jackson,才添加JSON转换器(优先级低)
if (jackson2Present) {
converters.add(new MappingJackson2HttpMessageConverter());
}
}
结论与代码的联系 :当你的Controller返回String时,Spring会遍历转换器列表,第一个匹配的StringHttpMessageConverter被选中。它期望收到String,但ResponseAdvice返回了Result对象,类型不匹配导致异常。
我们的解决方案代码中:
java
if (body instanceof String) {
return mapper.writeValueAsString(Result.success(body)); // 主动转换为String
}
这就是在代码层面主动适配StringHttpMessageConverter的行为,提前把Result转为JSON字符串。
3. 统一异常处理
3.1 为什么需要统一处理?
想象你的Controller里有100个接口,每个都写:
java
try {
// 业务逻辑
} catch (Exception e) {
return Result.fail("错误");
}
重复代码太多,维护困难。统一异常处理就像一个"中央错误处理中心",所有未捕获的异常都汇集到这里处理。
3.2 完整实现代码
3.2.1 全局异常处理器
java
// 导入Result统一结果类
import com.example.demo.model.Result;
// 异常处理核心注解:标记此方法处理哪种异常
import org.springframework.web.bind.annotation.ExceptionHandler;
// 控制器通知注解,对全局生效
import org.springframework.web.bind.annotation.ControllerAdvice;
// 标识返回JSON数据,不是视图
import org.springframework.web.bind.annotation.ResponseBody;
// HTTP状态码枚举(404, 500等)
import org.springframework.http.HttpStatus;
// 指定响应状态码的注解
import org.springframework.web.bind.annotation.ResponseStatus;
// 日志注解
import lombok.extern.slf4j.Slf4j;
/**
* 全局异常处理器
* @ControllerAdvice:捕获所有Controller抛出的异常
* @ResponseBody:返回JSON格式的错误信息
*/
@Slf4j
@ResponseBody
@ControllerAdvice
public class ErrorAdvice {
/**
* @ExceptionHandler(Exception.class):捕获所有Exception及其子类
* 这是"兜底"处理器,处理未被特定方法捕获的异常
* * @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR):设置HTTP状态码为500
* 告诉浏览器"服务器内部错误"
* * @param e 捕获到的异常对象,包含错误堆栈
* @return 统一错误响应
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Object> handleGeneralException(Exception e) {
// log.error:记录错误日志,{}是占位符
// e.getMessage():获取异常简要信息
// e:第三个参数传入异常对象,会打印完整堆栈
log.error("系统发生未处理异常: {}", e.getMessage(), e);
// 返回统一错误格式,隐藏内部细节,给友好提示
return Result.fail("系统繁忙,请稍后再试");
}
/**
* 专门处理空指针异常
* 当代码中出现null.xx()时触发
* * @ResponseStatus:返回500状态码
*/
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Object> handleNullPointerException(NullPointerException e) {
log.error("发生空指针异常: {}", e.getMessage(), e);
// 提供比"系统繁忙"更具体的提示,但不过度暴露细节
return Result.fail("系统错误: 未初始化的对象被引用");
}
/**
* 处理算术异常,如除零错误
* * @ResponseStatus(HttpStatus.BAD_REQUEST):返回400状态码
* 400表示客户端请求有误,这里是数学逻辑错误
*/
@ExceptionHandler(ArithmeticException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Object> handleArithmeticException(ArithmeticException e) {
log.error("发生算术异常: {}", e.getMessage(), e);
return Result.fail("计算错误: " + e.getMessage());
}
/**
* 处理非法参数异常,通常由参数校验失败抛出
* 如@NotNull校验不通过
*/
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Object> handleIllegalArgumentException(IllegalArgumentException e) {
log.warn("参数校验失败: {}", e.getMessage());
// warn级别表示警告,不是严重错误
return Result.fail("参数错误: " + e.getMessage());
}
}
代码如何体现"统一"?
-
@ControllerAdvice:代码层面的作用域控制,Spring会创建代理,拦截所有Controller抛出的异常,这是"统一"的实现基础。 -
@ExceptionHandler(Exception.class):代码中的异常类型匹配,Spring通过 instanceof 判断异常类型,选择合适的处理方法。 -
return Result.fail():所有处理器都返回Result类型,这是"统一格式"的 代码约束。
3.2.2 自定义业务异常
java
// 继承RuntimeException:运行时异常,不需要强制try-catch
// 区别于受检异常(如IOException),业务异常通常是可预期的
public class BusinessException extends RuntimeException {
// 业务错误码,比HTTP状态码更精细化
// 如1001=用户不存在,1002=余额不足
private int errorCode;
/**
* 构造方法:只传错误信息
* 默认错误码为500
*/
public BusinessException(String message) {
super(message); // 调用父类构造方法
this.errorCode = 500; // 默认服务器错误
}
/**
* 构造方法:传错误码和错误信息
*/
public BusinessException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
// Getter方法,让外部可以获取错误码
public int getErrorCode() {
return errorCode;
}
}
/**
* 用户相关业务异常
* 继承BusinessException,语义更清晰
*/
public class UserException extends BusinessException {
public UserException(String message) {
super(10001, message); // 固定用户模块错误码为10001
}
}
/**
* 资源未找到异常
*/
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String resourceName, Object id) {
super(404, String.format("%s[id=%s]不存在", resourceName, id));
// String.format:格式化字符串,%s是占位符
// 例如:new ResourceNotFoundException("图书", 1)
// 消息为:"图书[id=1]不存在"
}
}
代码如何体现业务语义?
-
extends RuntimeException:代码中的继承关系,表明这是非受检异常,调用者可以选择处理或不处理。 -
String.format():代码中的字符串模板,动态生成错误信息,这是"友好提示"的实现方式。 -
不同异常类:代码中通过类名区分业务场景(UserException、ResourceNotFoundException),这是"精细化处理"的代码基础。
3.3 异常处理调用链演示
java
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
// 1. 参数校验
if (id == null || id <= 0) {
// 抛出参数异常,会被handleIllegalArgumentException捕获
throw new IllegalArgumentException("用户ID必须为正整数");
}
// 2. 查询用户
User user = userService.findById(id);
if (user == null) {
// 抛出业务异常,需要额外处理逻辑(见下)
throw new ResourceNotFoundException("用户", id);
}
// 3. 返回结果,会被ResponseAdvice包装
return Result.success(user); // 实际返回的是Result里的User对象
}
}
// 在ErrorAdvice中添加业务异常处理器
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Object> handleBusinessException(BusinessException e) {
// 代码体现: instanceof BusinessException判断
// 由于ResourceNotFoundException extends BusinessException,所以会被此方法捕获
log.warn("业务异常[错误码:{}]: {}", e.getErrorCode(), e.getMessage());
// 使用错误码和错误信息创建Result
// 代码体现:Result.custom()支持自定义状态码
return Result.custom(e.getErrorCode(), e.getMessage(), null);
}
结论与代码的联系:
-
结论:异常处理有优先级,子类异常优先于父类。
-
代码体现 :Spring在
@ExceptionHandler匹配时,会优先匹配最具体的异常类型。由于ResourceNotFoundException是BusinessException的子类,如果两个处理方法都存在,Spring会选择更匹配的那个。但通常我们只保留父类处理器,通过instanceof和getErrorCode()来区分具体类型。
4. 完整项目结构示例
java
src/main/java/com/example/demo/
├── DemoApplication.java // 启动类
├── config/
│ ├── WebConfig.java // 拦截器注册(上面已详解)
│ └── ResponseAdvice.java // 统一返回(上面已详解)
├── controller/
│ ├── UserController.java // 用户接口
│ └── BookController.java // 图书接口
├── interceptor/
│ └── LoginInterceptor.java // 登录拦截(上面已详解)
├── exception/
│ ├── ErrorAdvice.java // 统一异常(上面已详解)
│ ├── BusinessException.java // 业务异常基类
│ ├── UserException.java // 用户异常
│ └── ResourceNotFoundException.java // 资源未找到
├── model/
│ ├── Result.java // 统一结果(上面已详解)
│ └── User.java // 用户实体
└── constant/
└── Constants.java // 常量类
5. 知识点与代码的完整对应关系表
|----------------|-------------------------------------------------|----------------------------------------|---------------------------------------------------------------------------------|
| 知识点 | 结论 | 代码体现位置 | 代码如何体现 |
| 拦截器执行顺序 | preHandle→Controller→postHandle→afterCompletion | DispatcherServlet.doDispatch()源码 | 方法按顺序调用,applyPreHandle()在ha.handle()之前,applyPostHandle()在之后 |
| 拦截路径匹配 | /**匹配任意层级 | addPathPatterns("/**") | 字符串**被Spring的AntPathMatcher类解析,递归匹配所有子路径 |
| 统一返回包装 | 所有返回值变成Result | ResponseAdvice.beforeBodyWrite() | 通过instanceof判断类型,调用Result.success()包装 |
| String类型问题 | String需特殊处理防止转换异常 | if (body instanceof String)分支 | 主动调用ObjectMapper.writeValueAsString()转为JSON字符串,适配StringHttpMessageConverter |
| 异常处理优先级 | 子类异常优先匹配 | @ExceptionHandler(Exception.class)位置 | Spring通过ExceptionDepthComparator比较异常继承深度,深度小的优先 |
| 适配器模式作用 | 解耦DispatcherServlet和Controller | HandlerAdapter接口及其实现类 | supports()方法做类型检查,handle()方法做统一调用,DispatcherServlet无需关心具体类型 |
| 开闭原则 | 对扩展开放,对修改关闭 | 新增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() (登录检查)
↓ (放行)
Controller执行业务
↓ (抛异常)
ErrorAdvice捕获异常 → 返回Result.fail()
↓ (正常返回)
ResponseAdvice.beforeBodyWrite() → 包装成Result.success()
↓
返回给前端统一格式
7.2 代码层面的最佳实践
java
// Controller层示例:保持简洁
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) { // 直接返回User,不包Result
// 1. 参数校验(失败抛IllegalArgumentException)
if(id <= 0) throw new IllegalArgumentException("ID无效");
// 2. 业务逻辑(可能抛BusinessException)
User user = userService.getById(id);
// 3. 直接返回数据,由ResponseAdvice统一包装
// 代码体现:这里不包Result,保持简洁
return user;
}
}
代码体现最佳实践:
-
Controller层 :不手动包装
Result,代码职责单一,只关注业务。 -
Service层 :抛出业务异常(
throw new BusinessException(...)),代码语义清晰。 -
全局层 :
ResponseAdvice和ErrorAdvice做统一处理,代码复用性高。
8. 最后的话
适配器模式本质回顾:
在你的Spring MVC图片案例中,代码层面的区别是:
不用适配器(If-Else地狱):
java
// DispatcherServlet必须知道所有Controller类型
if (handler instanceof Controller) {
// 强转+调用
} else if (handler instanceof HttpRequestHandler) {
// 强转+调用
}
// 新增类型必须修改这段代码!
用适配器(开闭原则):
java
// DispatcherServlet代码永远不变
HandlerAdapter adapter = getHandlerAdapter(handler); // 自动找到适配器
adapter.handle(request, response, handler); // 统一调用
// 新增Controller类型:只需添加新适配器类
public class NewControllerAdapter implements HandlerAdapter {
public boolean supports(Object handler) { return handler instanceof NewType; }
public ModelAndView handle(...) { /* 新逻辑 */ }
}
// DispatcherServlet源码无需修改!
结论与代码联系的终极答案 :设计模式的结论是通过代码结构体现的。适配器模式的"解耦"结论,体现在DispatcherServlet不依赖具体Controller类型,而是依赖HandlerAdapter接口。新增Controller时,代码修改范围被限制在新增类,而不是修改核心调度逻辑,这就是"开闭原则"的代码级证明。
希望这份详细的代码注释和知识点解析能帮你彻底理解!