JavaEE进阶——SpringBoot统一功能处理实战指南

目录

[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,代码语义清晰

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

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)中 ,让核心逻辑保持稳定,符合开闭原则(对扩展开放,对修改关闭)。

相关推荐
昵称为空C5 小时前
Spring Boot 项目docker分层镜像构建案例
spring boot·ci/cd·docker
阿拉斯攀登5 小时前
SkyWalking使用:Spring Boot场景
spring boot·后端·skywalking
小单于PRO5 小时前
Spring Boot 实现构建一个轻量级地图瓦片服务
java·spring boot·后端
Selegant5 小时前
Spring Boot 3 + Java 21 全新特性实战:虚拟线程、结构化并发与 Record 类型
java·spring boot·后端
huahailing10245 小时前
springboot 整合 rustfs
spring boot·rustfs
Jinkxs5 小时前
Java 架构 02:DDD 领域模型设计实战(限界上下文划分)
java·开发语言·架构
+VX:Fegn08955 小时前
计算机毕业设计|基于springboot + vue旅游信息推荐系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计·旅游
百锦再6 小时前
国产数据库的平替亮点——关系型数据库架构适配
android·java·前端·数据库·sql·算法·数据库架构
码界奇点6 小时前
基于SpringBoot和Vue的Fuint门店会员营销系统设计与实现
vue.js·spring boot·后端·毕业设计·springboot·源代码管理