Spring AOP 和 Spring 拦截器

一、SpringBoot 拦截器

在日常的 SpringBoot 开发中,我们经常需要对请求做一些“统一功能处理”,比如:

  • 登录校验
  • 权限验证
  • 日志打印
  • 统一返回数据

如果每个接口里都写一遍判断逻辑,会非常麻烦且冗余。
这时就可以用 拦截器(Interceptor) 来统一处理。

本文带你从零开始学习 Spring 拦截器,逐步过渡到源码分析,并结合 适配器模式 理解 SpringMVC 的设计思想。


1. 拦截器的定义与使用

拦截器(Interceptor)是 Spring MVC 提供的核心功能之一,主要作用是在 请求到达 Controller 前后 进行拦截和处理。

1.1 自定义拦截器

实现 HandlerInterceptor 接口,重写其方法:

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("preHandle - 目标方法执行前");
        return true; // 返回 true 表示放行,false 表示拦截
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle - 目标方法执行后");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion - 视图渲染完成后");
    }
}

1.2 注册拦截器

实现 WebMvcConfigurer 接口,重写 addInterceptors 方法:

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

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")        // 拦截所有请求
                .excludePathPatterns("/user/login", "/**/*.js", "/**/*.css", "/**/*.png", "/**/*.html"); // 排除路径
    }
}

2. 拦截路径配置

常见拦截路径规则如下:

拦截路径含义示例
/*一级路径/user /book
/**任意级路径/user/login /book/add
/book/*/book 下的一层路径/book/addBook
/book/**/book 下任意级路径/book/addBook/1 /book/list

3. 拦截器执行流程

拦截器的执行顺序如下图:

请求进入 → preHandle()
       ↓ (放行=true)
Controller 方法执行
       ↓
postHandle()
       ↓
视图渲染
       ↓
afterCompletion()
  • preHandle:Controller 方法执行前
  • postHandle:Controller 方法执行后
  • afterCompletion:整个请求完成后(包括视图渲染)

如果 preHandle 返回 false,请求将被拦截,后续方法不会执行。


4. 登录校验功能实现

在实际开发中,常见的需求是 拦截未登录用户

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("USER") != null) {
            return true; // 已登录,放行
        }
        response.setStatus(401); // 未认证
        return false;
    }
}
  • 401 Unauthorized:表示请求未通过身份认证。

配置拦截器时,排除 /user/login 接口,避免死循环。


5. Controller 示例:RequestMapping 用法

拦截器配置好以后,我们来写一个简单的 Controller,测试是否生效:

@RestController
@RequestMapping("/user")
public class UserController {

    // 模拟登录接口,不拦截
    @PostMapping("/login")
    public String login(HttpSession session, @RequestParam String username) {
        session.setAttribute("USER", username);
        return "登录成功,欢迎 " + username;
    }

    // 被拦截的接口
    @GetMapping("/profile")
    public String profile(HttpSession session) {
        String user = (String) session.getAttribute("USER");
        return "当前登录用户: " + user;
    }

    // 普通测试接口
    @RequestMapping("/hello")
    public String hello(@RequestParam String name) {
        return "hello " + name + " , 时间戳: " + System.currentTimeMillis();
    }
}

👉 访问 /user/hello 时会被拦截器处理;
👉 访问 /user/login 时不会被拦截;
👉 登录成功后再访问 /user/profile,才能获取到用户信息。


6. Spring MVC 源码解析

所有请求都会先经过 DispatcherServlet,再分发到 Controller。
拦截器就是在 DispatcherServlet#doDispatch() 里生效的。

源码关键流程(简化):

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 1. 获取 Handler(执行链)
    HandlerExecutionChain mappedHandler = getHandler(request);

    // 2. 获取 HandlerAdapter(适配器)
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

    // 3. 执行拦截器 preHandle
    if (!mappedHandler.applyPreHandle(request, response)) {
        return; // 拦截返回
    }

    // 4. 执行目标 Controller 方法
    ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());

    // 5. 执行拦截器 postHandle
    mappedHandler.applyPostHandle(request, response, mv);

    // 6. 渲染视图 + 执行 afterCompletion
    processDispatchResult(request, response, mappedHandler, mv, null);
}

可以看到:

  • 拦截器preHandle 在 Controller 之前执行
  • 拦截器postHandle 在 Controller 之后执行
  • 拦截器afterCompletion 在请求完成后执行

7. 设计模式中的适配器模式

DispatcherServlet 中,为什么要有 HandlerAdapter
因为不同的 Handler 类型(Controller、HttpRequestHandler、Servlet 等)需要一个 统一的适配入口

👉 这就是 适配器模式(Adapter Pattern)

7.1 生活中的例子

  • 出国旅行需要 电源转换插头
  • 旧手机耳机(3.5mm)需要 Type-C 转接头

7.2 slf4j / log4j 桥接示例

// slf4j API
interface Slf4jApi {
    void log(String message);
}

// log4j 原始类(不兼容)
class Log4j {
    void log4jLog(String message) {
        System.out.println("Log4j打印: " + message);
    }
}

// 适配器
class Slf4jLog4jAdapter implements Slf4jApi {
    private Log4j log4j;
    public Slf4jLog4jAdapter(Log4j log4j) {
        this.log4j = log4j;
    }
    @Override
    public void log(String message) {
        log4j.log4jLog(message);
    }
}

// 客户端
public class Slf4jDemo {
    public static void main(String[] args) {
        Slf4jApi logger = new Slf4jLog4jAdapter(new Log4j());
        logger.log("使用slf4j打印日志");
    }
}

通过适配器,不需要改动 log4j 的代码,就能让它兼容 slf4j 的接口。

Spring MVC 的 HandlerAdapter 就是这种思想的应用:

  • 目标类:各种 Handler(Controller)
  • 适配器:HandlerAdapter
  • 调用方:DispatcherServlet

二、Spring AOP

在日常开发中,我们经常遇到一些“横切关注点”的需求,例如:

  • 日志打印(记录请求参数、返回值、耗时)
  • 权限验证
  • 异常拦截与统一处理
  • 事务控制
  • 性能监控

如果这些代码散落在每一个业务方法里,既冗余又难维护。AOP(面向切面编程) 就是为了解决这类问题而生的。


1. AOP 基本概念

AOP(Aspect Oriented Programming):面向切面编程。
核心思想是将与业务逻辑无关的“共性问题”抽取出来,形成一个“切面(Aspect)”,并在需要的地方织入(Weaving)到业务代码中。

AOP 可以看作是 OOP(面向对象编程)的补充。OOP 关注“对象纵向结构”,AOP 关注“横向切面逻辑”。

1.1 AOP 的应用场景

  • 日志记录
  • 权限校验
  • 缓存处理
  • 异常监控
  • 性能统计
  • 事务管理

2. Spring AOP 快速入门

2.1 引入依赖

在 Spring Boot 项目 pom.xml 中添加:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Spring Boot 默认开启 AOP(底层基于 AspectJ 动态代理实现)。


2.2 示例:记录方法耗时

@Slf4j
@Aspect
@Component
public class TimeAspect {

    @Around("execution(* com.example.demo.service.*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed(); // 执行目标方法,可能抛异常
        long end = System.currentTimeMillis();
        log.info("方法 {} 执行耗时: {} ms", pjp.getSignature(), (end - start));
        return result; // 必须返回结果
    }
}

参数与注解详解

注解/参数作用
@Slf4jLombok 注解,自动生成日志对象 log
@Aspect声明当前类是切面类
@Component注入 Spring 容器,使切面生效
@Around("execution(...)")环绕通知,包裹目标方法,可在方法执行前后插入逻辑,可控制目标方法是否执行
ProceedingJoinPoint pjp环绕通知专用参数,可获取方法签名、参数,并执行目标方法
pjp.proceed()执行目标方法,必须调用,否则方法不会被执行,可能抛出异常
pjp.getSignature()获取方法签名(类名.方法名)
pjp.getArgs()获取方法参数

3. AOP 核心概念

名称解释示例
切点 (Pointcut)定义拦截规则execution(* com.example..service.*(..))
连接点 (JoinPoint)程序运行中的某个方法执行点UserService.getUser()
通知 (Advice)增强逻辑,在连接点执行的动作@Before@After@Around
切面 (Aspect)切点 + 通知的组合日志切面、权限切面
织入 (Weaving)把切面应用到目标对象的过程动态代理(JDK Proxy、CGLIB)

代理类型简述

  • JDK 动态代理:只能拦截接口方法,目标类必须实现接口。
  • CGLIB 代理:通过生成子类拦截类方法,也能拦截接口方法,但 final 类/方法无法拦截

Spring AOP 默认优先 JDK 代理,无接口时自动使用 CGLIB。


4. 通知类型(Advice)详解

4.1 前置通知 - @Before

在方法执行之前执行,无法阻止目标方法执行。

@Before("execution(* com.example.demo.service.*.*(..))")
public void beforeAdvice(JoinPoint jp) {
    log.info("Before - 方法开始: {}", jp.getSignature());
}

4.2 后置通知 - @After

在方法执行之后执行(无论正常返回或抛异常都会执行),可用于资源清理。

@After("execution(* com.example.demo.service.*.*(..))")
public void afterAdvice(JoinPoint jp) {
    log.info("After - 方法执行完成: {}", jp.getSignature());
}

4.3 返回后通知 - @AfterReturning

在方法正常返回后执行,可获取返回值。

@AfterReturning(value = "execution(* com.example.demo.service.*.*(..))", returning = "result")
public void afterReturningAdvice(Object result) {
    log.info("AfterReturning - 返回值: {}", result);
}

🔹 注意:returning 属性名必须与方法参数名一致,否则 Spring 找不到对应参数。

4.4 异常通知 - @AfterThrowing

在方法抛出异常后执行,可获取异常对象。

@AfterThrowing(value = "execution(* com.example.demo.service.*.*(..))", throwing = "ex")
public void afterThrowingAdvice(Exception ex) {
    log.error("AfterThrowing - 捕获异常: {}", ex.getMessage());
}

4.5 环绕通知 - @Around

包裹目标方法,可在执行前后添加逻辑,可控制目标方法是否执行。

@Around("execution(* com.example.demo.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
    log.info("Around - 方法执行前");
    Object result = pjp.proceed(); // 执行目标方法,可能抛异常
    log.info("Around - 方法执行后");
    return result;
}

🔹 Tip:环绕通知执行顺序前于 @Before,其“后逻辑”晚于 @AfterReturning,实际日志顺序可能因切面顺序或代理类型不同略有差异。


5. 切点表达式详解

execution 用于匹配方法执行,其基本格式为:

execution([访问修饰符] 返回类型 包名.类名.方法名(参数列表) [异常])

拆解说明:

部分含义
访问修饰符(可选)publicprivate
返回类型* 表示任意返回类型
包名.类名.方法名(方法参数)指定要拦截的类和方法,可用 *.. 通配
异常(可选)匹配方法抛出的异常类型

示例:

@Pointcut("execution(* com.example.demo.controller.*.*(..))")
  • * → 任意返回类型
  • com.example.demo.controller.*.*(..) → 拦截 controller 包下所有类的所有方法,参数不限

🔹 Tip:

  • .. 可匹配任意层级包或任意数量参数
  • * 可匹配任意返回类型或任意方法名

6. 自定义注解 + @annotation

6.1 定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}

6.2 定义切面

@Slf4j
@Aspect
@Component
public class AnnotationAspect {

    @Around("@annotation(com.example.demo.annotation.LogExecutionTime)")
    public Object logTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        long end = System.currentTimeMillis();
        log.info("方法 {} 执行耗时 {} ms", pjp.getSignature(), (end - start));
        return result;
    }
}

6.3 使用注解

@Service
public class UserService {
    @LogExecutionTime
    public void getUserList() {
        try { Thread.sleep(200); } catch (InterruptedException ignored) {}
    }
}

调用 getUserList() 时会自动统计方法耗时。


7. 多切面优先级

使用 @Order 控制执行顺序,数值越小优先级越高:

@Order(1)
@Aspect
@Component
public class FirstAspect { ... }

@Order(2)
@Aspect
@Component
public class SecondAspect { ... }

执行顺序:

  1. FirstAspect @Before
  2. SecondAspect @Before
  3. 方法执行
  4. SecondAspect @After
  5. FirstAspect @After

8. 实战案例

8.1 方法耗时统计

@Around("execution(* com.example.demo.service.*.*(..))")
public Object timeCost(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.nanoTime();
    Object result = pjp.proceed();
    long end = System.nanoTime();
    log.info("[耗时监控] {} 执行耗时: {} ms", pjp.getSignature(), (end - start)/1_000_000);
    return result;
}

8.2 异常拦截

@AfterThrowing(pointcut = "execution(* com.example.demo.service.*.*(..))", throwing = "ex")
public void handleException(Exception ex) {
    log.error("[异常拦截] 捕获到异常: {}", ex.getMessage());
}

8.3 通用日志打印

@Before("execution(* com.example.demo.controller.*.*(..))")
public void logRequest(JoinPoint jp) {
    log.info("[请求日志] 方法: {}, 参数: {}", jp.getSignature(), Arrays.toString(jp.getArgs()));
}

9. 总结

本文系统介绍了 Spring AOP

  1. AOP 概念与应用场景
  2. Spring AOP 快速入门(耗时统计示例)
  3. 核心概念:切点、连接点、通知、切面
  4. 通知类型详解(@Before / @After / @AfterReturning / @AfterThrowing / @Around)
  5. 切点表达式(execution、@annotation)
  6. 自定义注解的使用方式
  7. 多个切面的优先级控制(@Order)
  8. 实战案例:耗时监控、异常拦截、日志打印
  9. 日志运行效果示例

三、Spring AOP 和 Spring 拦截器有什么关系?

在实际开发中,我们经常会同时接触 Spring AOPSpring MVC 拦截器,它们都能实现“横切逻辑”,比如日志、权限校验、异常处理。但两者之间经常被混淆。本文简单梳理一下它们的关系和区别。


1、Spring AOP

Spring AOP 属于 Spring 核心功能,本质是通过 动态代理(JDK Proxy / CGLIB)方法调用的前后 织入额外逻辑。

  • 作用范围:任意 Spring 管理的 Bean 方法(Controller、Service、DAO)。
  • 常见场景:日志打印、事务控制、性能监控、统一异常处理。
  • 特点:与 Web 无关,任何 Spring 项目都能用。

2、Spring 拦截器

Spring 拦截器(HandlerInterceptor)属于 Spring MVC 模块,主要作用在 Web 请求处理链 上。

  • 拦截时机

    1. 请求进入 Controller 之前(preHandle
    2. Controller 方法执行之后(postHandle
    3. 请求完成后(afterCompletion
  • 常见场景:登录校验、权限验证、接口限流、请求日志、跨域处理。

  • 特点:依赖 SpringMVC,仅 Web 项目可用。


3、执行链路对比

来看一条典型的请求处理链路:

HTTP 请求
   ↓
Filter (Servlet 过滤器)      ← 拦截最早
   ↓
Interceptor (Spring 拦截器) ← 拦截 Controller 前后
   ↓
Controller 方法执行
   ↓       ↑
   └── AOP 可以在方法前后织入逻辑
   ↓
Service / DAO 方法
   ↓
返回响应

👉 可以看到:

  • 拦截器 更偏向 请求级别 的控制;
  • AOP 更偏向 方法级别 的增强;
  • 两者位置不同,但都能实现“横切逻辑”。

4、关系总结

  • 共同点

    • 都能实现非业务逻辑的统一处理。
    • 都是“拦截 + 增强”的思想。
  • 不同点

    特性Spring AOPSpring 拦截器
    所属模块Spring AOPSpring MVC
    作用范围Bean 方法(任意层)Web 请求(Controller 前后)
    实现原理动态代理(JDK Proxy/CGLIB)责任链模式(HandlerInterceptor)
    常用场景日志、事务、异常、性能监控登录校验、权限验证、接口限流

5、结语

Spring AOP 和 Spring 拦截器没有直接继承或实现关系,它们是 两个平行但互补的机制

  • 想拦截 请求入口 → 用拦截器;
  • 想拦截 方法调用 → 用 AOP;
  • 想拦截 所有请求(包括静态资源) → 用 Filter。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值