目录

Spring Boot统一功能处理详解(新手完整版)

1. 拦截器详解

1.1 什么是拦截器

1.2 完整代码实现(逐行注释)

1.2.1 定义登录拦截器

1.2.2 注册拦截器到Spring MVC

1.3 拦截器执行流程图解

2. 统一数据返回格式

2.1 为什么需要统一格式?

2.2 完整实现代码

2.2.1 统一结果类Result

2.2.2 全局响应处理器ResponseAdvice

2.3 String类型问题的代码体现

3. 统一异常处理

3.1 为什么需要统一处理?

3.2 完整实现代码

3.2.1 全局异常处理器

3.2.2 自定义业务异常

3.3 异常处理调用链演示

4. 完整项目结构示例

5. 知识点与代码的完整对应关系表

6. 新手最容易踩的坑

6.1 拦截器不生效

6.2 String类型异常

6.3 异常没捕获

7. 总结与最佳实践

7.1 三者的协作流程图

7.2 代码层面的最佳实践

8. 最后的话


这里是为您生成的Markdown文档,内容完全按照您的要求整理,保留了所有代码注释和详细讲解。

Spring Boot Unified Function Handling

Spring Boot统一功能处理详解(新手完整版)

我会整合拦截器、统一返回格式和异常处理三部分内容,提供带逐行注释的完整代码,并详细说明每个知识点如何体现在代码中。

1. 拦截器详解

1.1 什么是拦截器

拦截器是Spring MVC提供的"安检门"机制,能在请求到达Controller之前、之后以及请求完成时插入自定义逻辑。它就像你去商场时要经过的安检:安检前检查包裹(preHandle),安检后刷卡(postHandle),离开时记录时间(afterCompletion)。

核心应用场景:

  • 登录认证:检查用户是否登录(如未登录不能访问订单页面)

  • 日志记录:记录每个请求的处理时间

  • 权限控制:判断用户是否有权限访问某个接口

  • 性能监控:统计接口响应时间

1.2 完整代码实现(逐行注释)

1.2.1 定义登录拦截器
// 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
// 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 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 拦截器执行流程图解

用户请求 → Tomcat → DispatcherServlet → applyPreHandle()
    ↓ (返回false则中断)             ↓ (返回true则继续)
拦截器preHandle()                Controller方法执行
    ↓                                 ↓
拦截器postHandle() ←─────────── 方法返回
    ↓
视图渲染
    ↓
拦截器afterCompletion() ←─────── 请求完成

代码体现:在DispatcherServlet.doDispatch()方法中(Spring源码),三个方法被按顺序调用,这就是结论"拦截器有固定执行顺序"的实现依据。

2. 统一数据返回格式

2.1 为什么需要统一格式?

想象你是前端开发,后端同事张三返回{"name":"张三"},李四返回"李四",王五返回true。你要写三种不同的解析逻辑,维护成本极高!

统一格式后,所有接口都返回:

{
  "status": 200,
  "data": "实际数据",
  "errorMessage": "",
  "timestamp": 1234567890
}

前端只需写一套解析逻辑:取data字段即可。

2.2 完整实现代码

2.2.1 统一结果类Result
// Lombok的@Data注解,自动生成getter/setter/toString/equals/hashCode方法
// 相当于让Lombok帮你写样板代码,你只需关注业务字段
import lombok.Data;
/**
 * 统一返回结果类(模板)
 * @param  泛型,表示data字段可以是任何类型(String、User、List等)
 * 就像快递盒,可以装任何东西
 */
@Data
public class Result {
    // 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的类型
     * * @param data 要返回的业务数据
     * @return 包装后的统一结果
     */
    public static  Result success(T data) {
        Result result = new Result<>();
        result.setStatus(200); // HTTP状态码200表示成功
        result.setData(data);  // 将业务数据放入data字段
        // errorMessage保持null,表示没有错误
        return result;
    }
    /**
     * 静态工厂方法:创建失败响应
     */
    public static  Result fail(String errorMessage) {
        Result result = new Result<>();
        result.setStatus(500); // HTTP状态码500表示服务器错误
        result.setErrorMessage(errorMessage); // 设置错误详情
        // data保持null
        return result;
    }
    /**
     * 自定义状态码和数据的响应
     * 用于特殊场景,如参数验证失败400,未授权401
     */
    public static  Result custom(int status, String errorMessage, T data) {
        Result 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
// 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 {
    /**
     * 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注册了默认的消息转换器:

// Spring源码片段(注释说明)
protected final void addDefaultHttpMessageConverters(List 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对象,类型不匹配导致异常。

我们的解决方案代码中:

if (body instanceof String) {
    return mapper.writeValueAsString(Result.success(body)); // 主动转换为String
}

这就是在代码层面主动适配StringHttpMessageConverter的行为,提前把Result转为JSON字符串。

3. 统一异常处理

3.1 为什么需要统一处理?

想象你的Controller里有100个接口,每个都写:

try {
    // 业务逻辑
} catch (Exception e) {
    return Result.fail("错误");
}

重复代码太多,维护困难。统一异常处理就像一个"中央错误处理中心",所有未捕获的异常都汇集到这里处理。

3.2 完整实现代码

3.2.1 全局异常处理器
// 导入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 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 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 handleArithmeticException(ArithmeticException e) {
        log.error("发生算术异常: {}", e.getMessage(), e);
        return Result.fail("计算错误: " + e.getMessage());
    }
    /**
     * 处理非法参数异常,通常由参数校验失败抛出
     * 如@NotNull校验不通过
     */
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result 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 自定义业务异常
// 继承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 异常处理调用链演示

@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("/user/{id}")
    public Result 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 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匹配时,会优先匹配最具体的异常类型。由于ResourceNotFoundExceptionBusinessException的子类,如果两个处理方法都存在,Spring会选择更匹配的那个。但通常我们只保留父类处理器,通过instanceofgetErrorCode()来区分具体类型。

4. 完整项目结构示例

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 三者的协作流程图

用户请求
   ↓
LoginInterceptor.preHandle() (登录检查)
   ↓ (放行)
Controller执行业务
   ↓ (抛异常)
ErrorAdvice捕获异常 → 返回Result.fail()
   ↓ (正常返回)
ResponseAdvice.beforeBodyWrite() → 包装成Result.success()
   ↓
返回给前端统一格式

7.2 代码层面的最佳实践

// 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(...)),代码语义清晰。

  • 全局层ResponseAdviceErrorAdvice做统一处理,代码复用性高。

8. 最后的话

适配器模式本质回顾:

在你的Spring MVC图片案例中,代码层面的区别是:

不用适配器(If-Else地狱):

// DispatcherServlet必须知道所有Controller类型
if (handler instanceof Controller) {
    // 强转+调用
} else if (handler instanceof HttpRequestHandler) {
    // 强转+调用
}
// 新增类型必须修改这段代码!

用适配器(开闭原则):

// 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时,代码修改范围被限制在新增类,而不是修改核心调度逻辑,这就是"开闭原则"的代码级证明。

希望这份详细的代码注释和知识点解析能帮你彻底理解!