SpringBoot 拦截器 (Interceptor) 与切面 (AOP):示例、作用、及适用场景

SpringBoot 拦截器 (Interceptor) 与切面 (AOP):示例、作用、及适用场景

一、拦截器 (Interceptor):SpringMVC 层的请求拦截器

拦截器是SpringMVC 框架的组件 ,基于Java 动态代理 实现,作用于Web 层(Controller 层),仅能拦截 HTTP 请求的处理过程,生命周期与 SpringMVC 的请求处理流程绑定。

核心作用

  1. 拦截 Controller 层的 HTTP 请求(如 GET/POST 请求);
  2. 实现请求前的预处理(如登录校验、token 验证、跨域配置);
  3. 实现请求后的后置处理(如响应数据统一封装);
  4. 实现请求完成后的最终处理(如资源释放、请求日志记录)。

1.自定义拦截器:实现HandlerInterceptor接口

java 复制代码
@Component
@Slf4j
public class CompanyContextInterceptor implements HandlerInterceptor {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    private static final String REDIS_TOKEN_KEY_PREFIX = "login:";

    // 请求开始前:解析 Header,设置公司编码到上下文
    @Override
    public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {

        // 1. 从请求头获取Token(Bearer Token格式)
        String authHeader = request.getHeader("Authorization");
        if (StringUtils.isBlank(authHeader) || !authHeader.startsWith("Bearer ")) {
            // 未携带Token,直接通过(或返回401,根据业务需求)
            return true;
        }
        String token = authHeader.substring(7); // 截取Bearer后的Token

        // 2. 从Redis查询登录用户信息
        String redisKey = REDIS_TOKEN_KEY_PREFIX + token;
        LoginController.LoginWxUser loginWxUser = (LoginController.LoginWxUser) redisTemplate.opsForValue().get(redisKey);
        if (loginWxUser == null || StringUtils.isBlank(loginWxUser.getPhone())) {
            // Token无效/过期,直接通过(或返回401)
            return true;
        }

        // 3. 核心:将loginWxUser存入request,key必须是「loginWxUser」(和接口一致!)
        request.setAttribute("loginWxUser", loginWxUser);
        // 可选:Token续期
        redisTemplate.expire(redisKey, 7, TimeUnit.DAYS);

        // 获取请求路径
        String requestURI = request.getRequestURI();

        // 检查是否为需要忽略的登录接口
        if (requestURI.equals("/workaffairs/api/user/loginByPhone") ||
                requestURI.startsWith("/workaffairs/api/user/loginByPhone")) {
            return true; // 直接放行,不设置公司编码
        }

        // 从 Header 获取公司编码
        String companyCode = request.getHeader("x-Company-Code");
        if (companyCode != null && !companyCode.isEmpty()) {
            CompanyContextHolder.setCompanyCode(companyCode);
            log.info("获取到公司编码----" + companyCode);
        }
        return true; // 放行请求
    }

    // 请求结束后:清理上下文(必须执行,避免内存泄漏)
    @Override
    public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler, Exception ex) throws Exception {
        CompanyContextHolder.clear();
    }
}

2.拦截器配置类:实现WebMvcConfigurer接口

java 复制代码
/**
 * 拦截器配置类:注册拦截器并配置拦截规则
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    // 注入自定义拦截器
    @Resource
    private CompanyContextInterceptor companyContextInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(companyContextInterceptor)
                // 拦截所有需要切换数据源的业务接口(根据你的实际路径调整)
                .addPathPatterns("/purchase/**", "/delivery/**","/api/user/getSupplierByCompany")
                // 排除不需要拦截的接口(比如登录接口)
                .excludePathPatterns("/api/user/loginByPhone");
    }
}

二、切面 (AOP):面向切面的通用横切逻辑实现

核心概念

切面是Spring AOP 框架的核心 ,基于JDK 动态代理(针对接口)+CGLIB 动态代理(针对类) 实现,是面向切面编程 的落地方式,可作用于Spring 容器管理的所有 Bean(Controller、Service、Dao、普通组件等),无层级限制。

核心作用

  1. 提取通用横切逻辑(与业务无关、可复用的逻辑),如日志记录、事务管理、耗时统计、异常统一捕获;
  2. 实现业务代码与横切逻辑的解耦,无需在业务方法中硬编码通用逻辑;
  3. 可对任意方法进行增强(前置增强、后置增强、环绕增强、异常增强等),支持灵活的切点表达式配置。

1.自定义切面

java 复制代码
@Aspect // 声明为切面类
@Component // 交给Spring容器管理
@Slf4j
public class DbOperationLogAspect {

    /**
     * 定义切点:指定切面作用的目标方法
     * 表达式规则:execution(访问修饰符 返回值 包名.类名.方法名(参数类型))
     * 示例:匹配com.tgerp.workaffairs.mapper包下所有类的所有方法
     */
    @Pointcut("execution(* com.tgerp.workaffairs.mapper..*(..))")
    public void dbOperationPointCut() {
    }

    @Around("dbOperationPointCut()")
    public Object aroundDbOperation(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 此时controller已经执行了setDbName,能拿到正确的数据库名
        String currentDbName = DynamicDataSourceContextHolder.peek(); // 从数据源上下文拿
//        }

        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();

        // 打印日志
        log.info("[workaffairs-数据库: {}] 执行数据库操作 - 类:{},方法:{}", currentDbName, className, methodName);
        try {
            return joinPoint.proceed();
        } catch (Exception e) {
            log.error("[workaffairs-数据库: {}] 数据库操作失败 - 类:{},方法:{},异常信息:{}",
                    currentDbName, className, methodName, e.getMessage(), e);
            throw e;
        }
    }
}

三、拦截器与切面的适用场景

选型核心原则:根据横切逻辑的「作用范围」和「是否与 Web 请求相关」判断

拦截器 (Interceptor):仅适用于Web 相关的横切逻辑

所有与 HTTP 请求强绑定的场景,优先使用拦截器,因为可直接获取 Web 上下文(Request/Response),无需额外处理:

  1. 登录 / 权限校验:基于请求头 / 参数验证 token、会话、用户身份;
  2. 跨域配置:统一设置响应头(Access-Control-Allow-Origin 等);
  3. 请求参数预处理:统一解析请求头、请求参数,如防 XSS 处理;
  4. 响应数据统一封装:对 Controller 返回的结果统一添加 code/msg/data 格式;
  5. Web 层日志记录:记录请求 URI、请求方式、IP、响应状态等 Web 相关信息;
  6. 接口限流:基于 IP / 请求频率限制 HTTP 请求(如每秒最大请求数)。

切面 (AOP):适用于通用、跨层的横切逻辑

所有与业务无关、需要在多层级复用的通用横切逻辑,优先使用切面,解耦效果最佳,无环境依赖:

  1. 全局日志记录:记录方法的入参、出参、执行耗时(适用于 Service/Dao 层);
  2. 事务管理 :通过@Transactional(底层基于 AOP)实现方法级事务控制;
  3. 方法级权限校验:基于自定义注解(如 @RequiresPermission)实现方法访问控制;
  4. 异常统一捕获:捕获指定层的异常,统一封装异常响应(如 Service 层业务异常);
  5. 缓存控制:实现方法级缓存(如查询方法结果缓存,避免重复查询);
  6. 性能监控:统计指定方法的执行耗时、调用次数,做系统性能分析;
  7. 非 Web 环境的横切逻辑:如定时任务、后台服务的方法增强(无 Web 上下文,拦截器无法使用)。

实际项目中,拦截器和切面并非互斥 ,而是协同工作,各自负责专属领域的横切逻辑,形成完整的横切体系:

例如:一个接口的完整处理流程中:

  1. 拦截器:在请求进入 Controller 前,完成登录校验、token 验证(Web 层专属);
  2. 切面:在 Service 层方法执行时,完成耗时统计、事务管理、异常捕获(通用跨层);
  3. 拦截器:在请求完成后,完成响应数据封装、Web 日志记录(Web 层专属)。

这种协同模式既保证了 Web 相关逻辑的高效处理,又实现了通用逻辑的跨层复用,是 SpringBoot 项目的标准实践。

相关推荐
葫芦和十三1 天前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp1 天前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑1 天前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯1 天前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan1 天前
多Agent之间的区别
后端
青石路1 天前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充1 天前
1.面向对象设计思想
后端
IT_陈寒1 天前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro1 天前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗1 天前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端