JavaEE 进阶第二十期:Spring Boot 中的横切逻辑统一治理方案

专栏:JavaEE 进阶跃迁营

个人主页:手握风云

目录

一、拦截器

[1.1. 拦截器入门](#1.1. 拦截器入门)

[1. 拦截器核心定义](#1. 拦截器核心定义)

[2. 定义拦截器](#2. 定义拦截器)

[3. 注册配置拦截器](#3. 注册配置拦截器)

[1.2. 拦截器详解](#1.2. 拦截器详解)

[1. 拦截路径](#1. 拦截路径)

[2. 拦截器执行流程](#2. 拦截器执行流程)

[1.3. 登录校验](#1.3. 登录校验)

二、统一数据返回格式

[2.1. 快速入门](#2.1. 快速入门)

三、统一异常处理

[3.1. 核心注解](#3.1. 核心注解)

[3.2. 基础实现方式](#3.2. 基础实现方式)

[3.3. 异常匹配顺序](#3.3. 异常匹配顺序)

[3.4. 核心效果](#3.4. 核心效果)


一、拦截器

拦截器是 Spring 框架提供的核心功能之一,核心用于统一拦截用户的请求 ,可在目标方法执行前后执行预先设定的通用代码,也能在请求执行前阻止其继续运行,常用来实现登录校验、日志记录等通用操作,是解决接口通用功能处理的高效方式,其入门核心围绕定义拦截器注册配置拦截器两步展开。

1.1. 拦截器入门

1. 拦截器核心定义

拦截器允许开发人员在应用程序中对请求做通用性处理,在用户请求响应的前后插入自定义逻辑,也可根据业务需求拦截请求(如判断 Session 中是否有登录信息,无则拦截请求,有则放行)。就如同学校的保安,他需要判断哪些人能进,哪些人不能进。

拦截器的实现需要完成自定义拦截器注册配置拦截器两个核心步骤,缺一不可,且需通过指定注解让 Spring 容器管理相关类。自定义拦截器相当于告知保安的工作,注册配置拦截器相当于将保安放到指定的岗位上。

2. 定义拦截器

java 复制代码
package org.springframework.web.servlet;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;

public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

自定义类实现 Spring 提供的 HandlerInterceptor 接口,并重写里面的方法。preHandle 方法在请求到达具体的控制器方法之前执行,它是拦截器中最先被调用的方法。该方法返回一个布尔值,如果返回 true,表示请求继续向下传递,进入后续的拦截器或控制器;如果返回 false,则表示请求被终止。postHandle 方法在控制器方法执行完毕之后,但在视图渲染之前执行。这意味着在这个阶段,开发者已经可以访问到控制器处理后的 ModelAndView 对象,因此常用于对模型数据进行统一的修改或补充,或者根据不同的视图需求进行相应的处理。afterCompletion 方法在整个请求处理完成之后执行,即在视图渲染结束后进行回调。无论请求是否成功,只要 preHandle 返回了 true,该方法最终都会被执行。

3. 注册配置拦截器

java 复制代码
package com.yang.test2_22_1.config;

import com.yang.test2_22_1.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**");
    }
}

1.2. 拦截器详解

1. 拦截路径

拦截路径用于指定自定义拦截器对哪些请求生效,在注册配置拦截器时通过两个核心方法配置:addPathPatterns(),指定需要拦截的请求路径;excludePathPatterns(),指定需要排除、不进行拦截的请求路径。

拦截路径支持通配符匹配,核心规则如下表,规则可拦截项目中所有 URL(包括图片、JS、CSS 等静态文件):

拦截路径 含义
/* 一级路径
/** 任意级路径
/api/* /api下的一级路径
/api/** /api下的任意级路径
java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**") // 拦截所有请求
                .excludePathPatterns("/api/login"); // 排除登录请求,不拦截
    }
}

2. 拦截器执行流程

正常的调用顺序:

有了拦截器之后的调用顺序:

拦截器核心作用是在Controller层方法前后插入自定义逻辑,其执行流程依赖HandlerInterceptor 接口三个核心方法,与SpringMVC请求流转紧密相关。无拦截器时,请求按"用户请求→Controller→Service→Mapper→数据库"流转,处理后原路返回。添加拦截器后,请求先被拦截:到达Controller前执行preHandle(),返回true放行继续,返回false中断;若preHandle()返回true,请求正常流转,Controller目标方法执行完、视图渲染前执行postHandle()(后端少用);视图渲染完毕执行afterCompletion()用于收尾。核心方法执行顺序为preHandle()(Controller前)→目标Controller方法→postHandle()(Controller后、视图渲染前)→afterCompletion()(视图渲染后),postHandle() 和 afterCompletion() 仅在 preHandle() 返回true时执行。

1.3. 登录校验

java 复制代码
package com.yang.test2_22_1.controller;

import com.yang.test2_22_1.model.User;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api")
public class LoginController {
    @PostMapping("/login")
    public Map<String, Object> processLogin(@RequestBody User user) {

        Map<String, Object> response = new HashMap<>();

        String validUsername = "admin";
        String validPassword = "password123";

        if (validUsername.equals(user.getUsername()) && validPassword.equals(user.getPassword())) {
            response.put("success", true);
            response.put("message", "登录成功!欢迎回来," + user.getUsername() + "。");
        } else {
            response.put("success", false);
            response.put("message", "用户名或密码错误,请重试。");
        }
        return response;
    }

    @GetMapping("/secret")
    public String getSecretInfo() {
        return "这是一条机密信息,只有登录后的用户才能看到!";
    }
}
java 复制代码
package com.yang.test2_22_1.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object loginUser = request.getSession().getAttribute("LOGIN_USER");
        if (loginUser == null) {
            response.setStatus(HttpServletResponse.SC_ACCEPTED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"error\": \"未登录,请先登录系统\"}");
            return false;
        }
        log.info("执行 preHandle 方法");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        log.info("执行 postHandle 方法");
    }
}
java 复制代码
package com.yang.test2_22_1.config;

import com.yang.test2_22_1.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/api/login", "/api/secret");
    }
}

二、统一数据返回格式

2.1. 快速入门

统一的数据返回格式通过 @ControllerAdvice + ResponseBodyAdvice 组合实现,核心是对所有接口的返回结果进行统一封装,简化前后端交互。

@ControllerAdvice:控制器通知类,用于全局拦截控制器的返回结果和异常,是实现统一处理的基础注解;ResponseBodyAdvice:响应体增强接口,提供对返回数据的拦截和修改能力,需重写其核心方法。@RestControllerAdvice 注解是包含了 @ControllerAdvice 和 ResponseBodyAdvice,方法返回值直接写入 HTTP 响应体 (Body),通常转换为 JSON/XML 数据。

java 复制代码
import com.example.demo.model.Result;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    // 判断是否执行beforeBodyWrite方法(true=执行,false=不执行)
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true; // 对所有接口的返回结果都进行统一封装
    }

    // 对返回数据进行具体封装处理
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, 
                                  MediaType selectedContentType, Class selectedConverterType, 
                                  ServerHttpRequest request, ServerHttpResponse response) {
        return Result.success(body); // 调用Result工具类封装数据
    }
}

supports 方法作用是控制哪些接口的返回结果需要被统一处理,并且可通过 returnType 获取当前执行的类名和方法名,实现精准匹配。

java 复制代码
// 获取当前执行的控制器类
Class<?> declaringClass = returnType.getMethod().getDeclaringClass();
// 获取当前执行的方法
Method method = returnType.getMethod();

beforeBodyWrite 方法作用是对接口返回的原始数据(body)进行封装,返回统一格式的 Result 对象。依赖 Result 工具类:需提前定义 Result 泛型类,包含 status(状态)、errorMessage(错误信息)、data(业务数据)三个核心字段。

三、统一异常处理

SpringBoot 中统一异常处理核心通过 @ControllerAdvice + @ExceptionHandler 注解组合实现,实现了项目中异常的统一捕获、处理和返回格式标准化,避免零散的异常处理逻辑,降低前后端沟通和开发成本。

3.1. 核心注解

  • @ControllerAdvice:标识为控制器通知类,作用于全局,可捕获所有控制器层抛出的异常;
  • @ExceptionHandler:异常处理器注解,标注在方法上,指定该方法处理的异常类型;
  • @ResponseBody:若需要将异常处理结果以 JSON 等数据格式返回给前端,需在类或方法上添加该注解。

3.2. 基础实现方式

  • 创建全局异常处理类,添加@ControllerAdvice和@ResponseBody注解;
  • 定义异常处理方法,添加@ExceptionHandler注解,方法参数为需要处理的异常类型(如Exception),返回项目统一的结果对象(如Result);
  • 方法名、类名无强制要求,核心是注解的正确使用,方法内可通过异常对象获取信息并封装统一的失败返回结果。

3.3. 异常匹配顺序

当存在多个异常处理方法时,优先匹配最具体的异常类型 ,匹配规则为:当前异常类及其子类向上依次匹配

  • 底层通过ExceptionDepthComparator按异常深度排序,抛出的异常与处理方法声明的异常类型深度越小,优先级越高;
  • 例如:空指针异常(NullPointerException)会优先匹配标注@ExceptionHandler(NullPointerException.class)的方法,而非处理通用Exception的方法。
java 复制代码
package com.yang.test2_25_1.exception;

import com.yang.test2_25_1.common.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理用户未找到异常
     */
    @ExceptionHandler(UserNotFoundException.class)
    public ApiResponse<Void> handleUserNotFoundException(UserNotFoundException e) {
        log.warn("参数异常:{}", e.getMessage());
        return ApiResponse.error(404, e.getMessage());
    }

    /**
     * 处理参数异常
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public ApiResponse<Void> handleIllegalArgumentException(IllegalArgumentException e) {
        log.warn("参数异常:{}", e.getMessage());
        return ApiResponse.error(400, e.getMessage());
    }

    /**
     * 处理所有异常
     */
    @ExceptionHandler(Exception.class)
    public ApiResponse<Void> handleException(Exception e) {
        log.error("系统内部发生未知异常", e.getMessage());
        return ApiResponse.error(500, "服务器内部开小差了,请稍后再试");
    }
}

3.4. 核心效果

  • 所有异常统一捕获,返回格式与项目统一数据返回格式保持一致(如Result对象),方便前端统一解析;
  • 避免出现前端接收到杂乱的原生异常信息,减少前后端对接问题;
  • 项目异常处理逻辑集中管理,便于维护和修改,统一后端技术规范。
相关推荐
Potato_土豆2 小时前
深入JDK 17的语法新特性
java·后端·面试
栈外2 小时前
别吹了,AI写Java代码到底能省多少时间?一个后端仔的真实记录
java
苡~2 小时前
【claude热点资讯】炸裂!炸裂!Claude Code 更新:手机遥控电脑开发,Remote Control 功能上线
java·人工智能·智能手机·ai编程·claude api
咖啡色格调2 小时前
以码为念,以证为章——2048小游戏的软著登记心得
java·程序人生
我命由我123452 小时前
Android 多进程开发 - AIDL 回调、RemoteCallbackList、AIDL 安全校验
android·java·安全·android studio·安卓·android-studio·android runtime
一个有梦有戏的人2 小时前
Java 网络编程核心:BIO、NIO、AIO IO 模型深度解析与实战
java·网络·后端·netty·nio
这波不该贪内存的2 小时前
Linux文件编程:流与操作全解析
java·服务器·前端
重生之后端学习2 小时前
35. 搜索插入位置
java·数据结构·算法·leetcode·职场和发展·深度优先
昱宸星光2 小时前
Xnio源码分析
java·jvm·spring