JavaEE进阶——SpringBoot统一功能处理全解析

目录

[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匹配时,会优先匹配最具体的异常类型。由于ResourceNotFoundExceptionBusinessException的子类,如果两个处理方法都存在,Spring会选择更匹配的那个。但通常我们只保留父类处理器,通过instanceofgetErrorCode()来区分具体类型。

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

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

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

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

相关推荐
汤姆yu4 小时前
基于springboot的二手物品交易系统的设计与实现
java·spring boot·后端
Java水解4 小时前
基于Rust实现爬取 GitHub Trending 热门仓库
数据结构·后端
小橙编码日志4 小时前
MongoDB深入与实战:基于SQL的对照解析
后端·面试
Java编程爱好者4 小时前
Spring AI 2.x 发布:全面拥抱 Java 21,Redis 史诗级增强!
后端
中国胖子风清扬4 小时前
Spring AI Alibaba + Ollama 实战:基于本地 Qwen3 的 Spring Boot 大模型应用
java·人工智能·spring boot·后端·spring·spring cloud·ai
2501_944875515 小时前
Go后端工程师
开发语言·后端·golang
foundbug9995 小时前
Modbus协议C语言实现(易于移植版本)
java·c语言·前端
该用户已不存在5 小时前
没有这7款工具,难怪你的Python这么慢
后端·python
听风吟丶5 小时前
Java 反射机制深度解析:从原理到实战应用与性能优化
java·开发语言·性能优化