Spring AOP核心概念与实战指南

本节目标

  1. 了解AOP的概念

  2. 学习Spring AOP的实现方式以及实现原理, 对代理模式有一定了解

1. AOP概述

学习完Spring的统一功能之后, 我们进入到AOP的学习. AOP是Spring框架的第二大核心(第一大核心是 IoC)

什么是AOP?

• Aspect Oriented Programming(面向切面编程)

什么是面向切面编程呢? 切面就是指某一类特定问题, 所以AOP也可以理解为面向特定方法编程

什么是面向特定方法编程呢? 比如上个章节学习的"登录校验", 就是一类特定问题. 登录校验拦截器, 就 是对"登录校验"这类问题的统一处理. 所以, 拦截器也是AOP的一种应用. AOP是一种思想, 拦截器是AOP 思想的一种实现. Spring框架实现了这种思想, 提供了拦截器技术的相关接口.

同样的, 统一数据返回格式和统一异常处理, 也是AOP思想的一种实现

简单来说:AOP是一种思想, 是对某一类事情的集中处理

什么是Spring AOP?

AOP是一种思想, 它的实现方法有很多, 有Spring AOP,也有AspectJ、CGLIB等.

Spring AOP是其中的一种实现方式

学会了统一功能之后, 是不是就学会了Spring AOP呢, 当然不是.

拦截器作用的维度是URL(一次请求和响应), @ControllerAdvice 应用场景主要是全局异常处理 (配合自定义异常效果更佳), 数据绑定, 数据预处理. AOP作用的维度更加细致(可以根据包、类、方法 名、参数等进行拦截), 能够实现更加复杂的业务逻辑

举个例子:

我们现在有一个项目,项目中开发了很多的业务功能

现在有一些业务的执行效率比较低, 耗时较长, 我们需要对接口进行优化

第一步就需要定位出执行耗时比较长的业务方法, 再针对该业务方法来进行优化

如何定位呢? 我们就需要统计当前项目中每一个业务方法的执行耗时

如何统计呢? 可以在业务方法运行前和运行后, 记录下方法的开始时间和结束时间, 两者之差就是这个方法的耗时

这种方法是可以解决问题的, 但一个项目中会包含很多业务模块, 每个业务模块又有很多接口, 一个接口 又包含很多方法, 如果我们要在每个业务方法中都记录方法的耗时, 对于程序员而言, 会增加很多的工作量.

2. Spring AOP快速入门

学习什么是AOP后, 我们先通过下面的程序体验下AOP的开发, 并掌握Spring中AOP的开发步骤

需求: 统计图书系统各个接口方法的执行时间.

2.1 引入AOP依赖

在pom.xml文件中添加配置

复制代码
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2 编写AOP程序

记录Controller中每个方法的执行时间

java 复制代码
@Slf4j
@Aspect
@Component
public class TimeAspect {
    @Around("execution(* com.bit.book.springbookdemo..*.*(..))")
    public Object timeRecord(ProceedingJoinPoint pjp) throws Throwable {
        //1.记录开始时间
        long start=System.currentTimeMillis();
        //2.执行目标方法
        Object proceed=pjp.proceed();
        long end=System.currentTimeMillis();
        log.info(pjp.getSignature().toString()+"耗时"+(end-start)+"ms");
        return proceed;
    }
}

对程序进行简单的讲解:

  1. @Aspect: 标识这是一个切面类

  2. @Around: 环绕通知, 在目标方法的前后都会被执行. 后面的表达式表示对哪些方法进行增强.

  3. ProceedingJoinPoint.proceed() 让原始方法执行

我们通过AOP入门程序完成了业务接口执行耗时的统计

通过上面的程序, 我们也可以感受到AOP面向切面编程的一些优势:

• 代码无侵入: 不修改原始的业务方法, 就可以对原始的业务方法进行了功能的增强或者是功能的改变

• 减少了重复代码

• 提高开发效率

• 维护方便

3. Spring AOP 详解

下面我们再来详细学习AOP, 主要是以下几部分

• Spring AOP中涉及的核心概念

• Spring AOP通知类型

• 多个AOP程序的执行顺序

3.1 Spring AOP核心概念

3.1.1 切点(Pointcut)

切点(Pointcut), 也称之为"切入点"

Pointcut 的作用就是提供一组规则 (使用 AspectJ pointcut expression language 来描述), 告诉程序对哪些方法来进行功能增强

上面的表达式 execution(* com.example.demo.controller.*.*(..)) 就是切点表达式.

3.1.2 连接点(Join Point)

满足切点表达式规则的方法, 就是连接点. 也就是可以被AOP控制的方法

以入门程序举例, 所有 com.example.demo.controller 路径下的方法, 都是连接点

3.1.3 通知(Advice)

通知就是具体要做的工作, 指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

比如上述程序中记录业务方法的耗时时间, 就是通知

3.1.4 切面(Aspect)

切面(Aspect) = 切点(Pointcut) + 通知(Advice)

切面所在的类, 我们一般称为切面类(被@Aspect注解标识的类)

3.2 通知类型

上面我们讲了什么是通知, 接下来学习通知的类型. @Around 就是其中一种通知类型, 表示环绕通知.

Spring中AOP的通知类型有以下几种:

• @Around: 环绕通知, 此注解标注的通知方法在目标方法前, 后都被执行

• @Before: 前置通知, 此注解标注的通知方法在目标方法前被执行

• @After: 后置通知, 此注解标注的通知方法在目标方法后被执行, 无论是否有异常都会执行

• @AfterReturning: 返回后通知, 此注解标注的通知方法在目标方法后被执行, 有异常不会执行

• @AfterThrowing: 异常后通知, 此注解标注的通知方法发生异常后执行

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo {
 //前置通知
 @Before("execution(* com.example.demo.controller.*.*(..))")
 public void doBefore() {
 log.info("执行 Before 方法");
 }
 //后置通知
 @After("execution(* com.example.demo.controller.*.*(..))")
 public void doAfter() {
 log.info("执行 After 方法");
 }
 //返回后通知
 @AfterReturning("execution(* com.example.demo.controller.*.*(..))")
 public void doAfterReturning() {
 log.info("执行 AfterReturning 方法");
 }
 //抛出异常后通知
 @AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
 public void doAfterThrowing() {
 log.info("执行 doAfterThrowing 方法");
 }
 //添加环绕通知
 @Around("execution(* com.example.demo.controller.*.*(..))")
 public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
 log.info("Around 方法开始执行");
 Object result = joinPoint.proceed();
 log.info("Around 方法结束执行");
 return result;
 }
}

写一些测试程序

java 复制代码
@RequestMapping("/test")
@RestController
public class TestController {
 @RequestMapping("/t1")
 public String t1() {
 return "t1";
 }
 @RequestMapping("/t2")
 public boolean t2() {
 int a = 10 / 0;
 return true;
 }
}

程序正常运行的情况下, @AfterThrowing 标识的通知方法不会执行

从上图也可以看出来, @Around 标识的通知方法包含两部分, 一个"前置逻辑", 一个"后置逻辑".其 中"前置逻辑" 会先于 @Before 标识的通知方法执行, "后置逻辑" 会晚于 @After 标识的通知方法执行

  1. 异常时的情况

程序发生异常的情况下

• @AfterReturning 标识的通知方法不会执行, @AfterThrowing 标识的通知方法执行了

• @Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为 原始方法调用出异常了)

注意事项:

• @Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行, 其他 通知不需要考虑目标方法执行.

• @Around 环绕通知方法的返回值, 必须指定为Object, 来接收原始方法的返回值, 否则原始方法执 行完毕, 是获取不到返回值的

3.3 @PointCut

上面代码存在一个问题, 就是存在大量重复的切点表达式 execution(* com.example.demo.controller.*.*(..)) , Spring提供了 @PointCut 注解, 把公共的切点 表达式提取出来, 需要用到时引用该切入点表达式即可

java 复制代码
@Slf4j
    @Aspect
    @Component
    public class AspectDemo {
        //定义切点(公共的切点表达式)
        @Pointcut("execution(* com.example.demo.controller.*.*(..))")
        private void pt(){};
        //添加环绕通知
        @Around("pt()")
        public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("Around 方法开始执行");
            Object result = joinPoint.proceed();
            log.info("Around 方法结束执行");
            return result;
        }

        //前置通知
        @Before("pt()")
        public void doBefore() {
            log.info("执行 Before 方法");
        }
        //后置通知
        @After("pt()")
        public void doAfter() {
            log.info("执行 After 方法");
        }
        //返回后通知
        @AfterReturning("pt()")
        public void doAfterReturning() {
            log.info("执行 AfterReturning 方法");
        }
        //抛出异常后通知
        @AfterThrowing("pt()")
        public void doAfterThrowing() {
            log.info("执行 doAfterThrowing 方法");
        }
        
}

3.4 切面优先级 @Order

当我们在一个项目中, 定义了多个切面类时, 并且这些切面类的多个切入点都匹配到了同一个目标方法.

当目标方法运行的时候, 这些切面类中的通知方法都会执行, 那么这几个通知方法的执行顺序是什么样的呢?

我们还是通过程序来求证

定义多个切面类:

java 复制代码
public class AspectDemo2 {
 @Pointcut("execution(* com.example.demo.controller.*.*(..))")
 private void pt(){}
 //前置通知
 @Before("pt()")
 public void doBefore() {
 log.info("执行 AspectDemo2 -> Before 方法");
 }
 //后置通知
 @After("pt()")
 public void doAfter() {
 log.info("执行 AspectDemo2 -> After 方法");
 }
}
java 复制代码
public class AspectDemo3 {
 @Pointcut("execution(* com.example.demo.controller.*.*(..))")
 private void pt(){}
 //前置通知
 @Before("pt()")
 public void doBefore() {
 log.info("执行 AspectDemo3 -> Before 方法");
 }
 //后置通知
 @After("pt()")
 public void doAfter() {
 log.info("执行 AspectDemo3 -> After 方法");
 }
}
java 复制代码
public class AspectDemo4 {
 @Pointcut("execution(* com.example.demo.controller.*.*(..))")
 private void pt(){}
 //前置通知
 @Before("pt()")
 public void doBefore() {
 log.info("执行 AspectDemo4 -> Before 方法");
 }
 //后置通知
 @After("pt()")
 public void doAfter() {
 log.info("执行 AspectDemo4 -> After 方法");
 }
}

通过上述程序的运行结果, 可以看出:

存在多个切面类时, 默认按照切面类的类名字母排序

• @Before 通知:字母排名靠前的先执行

• @After 通知:字母排名靠前的后执行

但这种方式不方便管理, 我们的类名更多还是具备一定含义的

Spring 给我们提供了一个新的注解, 来控制这些切面通知的执行顺序: @Order

@Order 注解标识的切面类, 执行顺序如下:

• @Before 通知:数字越小先执行

• @After 通知:数字越大先执行

@Order 控制切面的优先级, 先执行优先级较高的切面, 再执行优先级较低的切面, 最终执行目标方法

3.5 切点表达式

上面的代码中, 我们一直在使用切点表达式来描述切点. 下面我们来介绍一下切点表达式的语法.

切点表达式常见有两种表达方式

  1. execution(......):根据方法的签名来匹配

  2. @annotation(......) :根据注解匹配

3.5.1 @annotation

execution表达式更适用有规则的, 如果我们要匹配多个无规则的方法呢, 比如:TestController中的t1() 和UserController中的u1()这两个方法

这个时候我们使用execution这种切点表达式来描述就不是很方便了

我们可以借助自定义注解的方式以及另一种切点表达式 @annotation 来描述这一类的切点

  1. 编写自定义注解

  2. 使用 @annotation 表达式来描述切点

  3. 在连接点的方法上添加自定义注解

准备测试代码:

java 复制代码
@RequestMapping("/test")
@RestController
public class TestController {
 @RequestMapping("/t1")
 public String t1() {
 return "t1";
 }
@RequestMapping("/t2")
 public boolean t2() {
 return true;
 }
}
java 复制代码
@RequestMapping("/user")
@RestController
public class UserController {
 @RequestMapping("/u1")
 public String u1(){
 return "u1";
 }
 @RequestMapping("/u2")
 public String u2(){
 return "u2";
 }
}
3.5.1.1 自定义注解 @MyAspect

创建一个注解类(和创建Class文件一样的流程, 选择Annotation就可以了)

java 复制代码
package com.bit.book.springbookdemo.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

}

代码简单说明, 了解即可. 不做过多解释

  1. @Target 标识了 Annotation 所修饰的对象范围, 即该注解可以用在什么地方

  2. @Retention 指Annotation被保留的时间长短, 标明注解的生命周期

3.5.1.2 切面类

使用 @annotation 切点表达式定义切点, 只对 @MyAspect 生效

java 复制代码
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
    @Before("@annotation(com.bit.book.springbookdemo.aspect.MyAspect)")
    public void before() {
        log.info("MyAspect -> before ...");
    }

    @After("@annotation(com.bit.book.springbookdemo.aspect.MyAspect)")
    public void after() {
        log.info("MyAspect -> after ...");
    }
}
3.5.1.3 添加自定义注解

在TestController中的t1()和UserController中的u1()这两个方法上添加自定义注解 @MyAspect , 其 他方法不添加

java 复制代码
@MyAspect
@RequestMapping("/t1")
public String t1() {
 return "t1";
}
@MyAspect
@RequestMapping("/u1")
public String u1(){
 return "u1";
}

4. Spring AOP 原理

上面我们主要学习了Spring AOP的应用, 接下来我们来学习Spring AOP的原理, 也就是Spring 是如何实现AOP的.

Spring AOP 是基于动态代理来实现AOP的, 咱们学习内容主要分以下两部分

  1. 代理模式

  2. Spring AOP源码剖析

4.1 代理模式

代理模式, 也叫委托模式

**定义:**为其他对象提供一种代理以控制对这个对象的访问. 它的作用就是通过提供一个代理类, 让我们 在调用目标方法的时候, 不再是直接对目标方法进行调用, 而是通过代理类间接调用.

在某些情况下, 一个对象不适合或者不能直接引用另一个对象, 而代理对象可以在客户端和目标对象之间起到中介的作用

代理模式的主要角色

  1. Subject: 业务接口类. 可以是抽象类或者接口(不一定有)

  2. RealSubject: 业务实现类. 具体的业务执行, 也就是被代理对象.

  3. Proxy: 代理类. RealSubject的代理

代理模式可以在不修改被代理对象的基础上, 通过扩展代理类, 进行一些功能的附加与增强.

根据代理的创建时期, 代理模式分为静态代理动态代理

• 静态代理: 由程序员创建代理类或特定工具自动生成源代码再对其编译, 在程序运行前代理类的 .class 文件就已经存在了.

• 动态代理: 在程序运行时, 运用反射机制动态创建而成

4.1.1 静态代理

静态代理: 在程序运行前, 代理类的 .class文件就已经存在了. (在出租房子之前, 中介已经做好了相关的工作, 就等租户来租房子了)

1. 定义接口

java 复制代码
public interface HouseSubject {
 void rentHouse();
}

2. 实现接口

java 复制代码
public class RealHouseSubject implements HouseSubject{
@Override
 public void rentHouse() {
 System.out.println("我是房东, 我出租房子");
 }
}

3. 代理

java 复制代码
public class HouseProxy implements HouseSubject{
 //将被代理对象声明为成员变量
 private HouseSubject houseSubject;
 public HouseProxy(HouseSubject houseSubject) {
 this.houseSubject = houseSubject;
 }
 @Override
 public void rentHouse() {
 //开始代理
 System.out.println("我是中介, 开始代理");
 //代理房东出租房子
 houseSubject.rentHouse();
 //代理结束
 System.out.println("我是中介, 代理结束");
 }
}

4. 使用

java 复制代码
public class StaticMain {
 public static void main(String[] args) {
 HouseSubject subject = new RealHouseSubject();
 //创建代理类
 HouseProxy proxy = new HouseProxy(subject);
 //通过代理类访问目标方法
 proxy.rentHouse();
 }
}

上面这个代理实现方式就是静态代理(仿佛啥也没干).

接下来新增需求: 中介又新增了其他业务: 代理房屋出售

1. 接口定义修改

java 复制代码
public interface HouseSubject {
 void rentHouse();
 void saleHouse();
}

2. 接口实现修改

java 复制代码
public class RealHouseSubject implements HouseSubject{
 @Override
 public void rentHouse() {
 System.out.println("我是房东, 我出租房子");
 }
 @Override
 public void saleHouse() {
 System.out.println("我是房东, 我出售房子");
 }
}

问题:

1.为啥代理类中实现的是接口,而不是实现类呢?

这是代理模式的精髓所在:面向接口编程,而不是面向实现类编程

好处:

  • 可以代理别的类
  • 降低代码耦合,符合开闭原则(对扩展开放,对修改关闭)

3. 代理类修改

java 复制代码
public class HouseProxy implements HouseSubject{
 //将被代理对象声明为成员变量
 private HouseSubject houseSubject;
 public HouseProxy(HouseSubject houseSubject) {
 this.houseSubject = houseSubject;
 }
 @Override
 public void rentHouse() {
 //开始代理
 System.out.println("我是中介, 开始代理");
 //代理房东出租房子
 houseSubject.rentHouse();
 //代理结束
 System.out.println("我是中介, 代理结束");
 }
 @Override
 public void saleHouse() {
 //开始代理
 System.out.println("我是中介, 开始代理");
 //代理房东出租房子
 houseSubject.saleHouse();
 //代理结束
 System.out.println("我是中介, 代理结束");
 }
}

从上述代码可以看出, 我们修改接口(Subject)和业务实现类(RealSubject)时, 还需要修改代理类 (Proxy)

同样的, 如果有新增接口(Subject)和业务实现类(RealSubject), 也需要对每一个业务实现类新增代理类 (Proxy)

既然代理的流程是一样的, 有没有一种办法, 让他们通过一个代理类来实现呢?

这就需要用到动态代理技术了

4.1.2 动态代理

相比于静态代理来说,动态代理更加灵活

我们不需要针对每个目标对象都单独创建一个代理对象, 而是把这个创建代理对象的工作推迟到程序运 行时由JVM来实现. 也就是说动态代理在程序运行时, 根据需要动态创建生成.

  1. JDK动态代理

  2. CGLIB动态代理

JDK动态代理

JDK 动态代理类实现步骤

  1. 定义一个接口及其实现类(静态代理中的 HouseSubject 和 RealHouseSubject )

  2. 自定义 InvocationHandler 并重写 invoke 方法,在 invoke 方法中我们会调用目标方 法(被代理类的方法)并自定义一些处理逻辑

  3. 通过 Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) 方法创建代理对象

定义JDK动态代理类

实现 InvocationHandler 接口

java 复制代码
public class JDKInvocationHandler implements InvocationHandler {
 //目标对象即就是被代理对象
 private Object target;
 public JDKInvocationHandler(Object target) {
 this.target = target;
 }

 @Override
 public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
 // 代理增强内容
 System.out.println("我是中介, 开始代理");
 //通过反射调用被代理类的方法
 Object retVal = method.invoke(target, args);
 //代理增强内容
 System.out.println("我是中介, 代理结束");
 return retVal;
 }
}

创建一个代理对象并使用

java 复制代码
public class DynamicMain {
 public static void main(String[] args) {
 HouseSubject target= new RealHouseSubject();
 //创建一个代理类:通过被代理类、被代理实现的接口、方法调用处理器来创建
 HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
 target.getClass().getClassLoader(),
 new Class[]{HouseSubject.class},
 new JDKInvocationHandler(target)
 );
 proxy.rentHouse();
 }
}

CGLIB 动态代理类实现步骤

  1. 定义一个类(被代理类)

  2. 自定义 MethodInterceptor 并重写 intercept 方法, intercept 用于增强目标方 法,和 JDK 动态代理中的 invoke 方法类似

  3. 通过 Enhancer 类的 create()创建代理类

接下来看下实现:

添加依赖

java 复制代码
<dependency>
 <groupId>cglib</groupId>
 <artifactId>cglib</artifactId>
 <version>3.3.0</version>
</dependency>

实现MethodInterceptor接口

java 复制代码
public class CGLIBInterceptor implements MethodInterceptor {
 //目标对象, 即被代理对象
 private Object target;
 public CGLIBInterceptor(Object target){
 this.target = target;
 }
 @Override
 public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
 // 代理增强内容
 System.out.println("我是中介, 开始代理");
 //通过反射调用被代理类的方法
 Object retVal = methodProxy.invoke(target, objects);
 //代理增强内容
 System.out.println("我是中介, 代理结束");
 return retVal;
 }
}

创建代理类, 并使用

java 复制代码
public class DynamicMain {
 public static void main(String[] args) {
 HouseSubject target= new RealHouseSubject();
 HouseSubject proxy= (HouseSubject)
Enhancer.create(target.getClass(),new CGLIBInterceptor(target));
 proxy.rentHouse();
 }
}

总结

  1. AOP是一种思想, 是对某一类事情的集中处理. Spring框架实现了AOP, 称之为SpringAOP

  2. Spring AOP常见实现方式有两种: 1. 基于注解@Aspect来实现 2. 基于自定义注解来实现, 还有一些 更原始的方式,比如基于代理, 基于xml配置的方式, 但目标比较少见

  3. Spring AOP 是基于动态代理实现的, 有两种方式: 1. 基本JDK动态代理实现 2. 基于CGLIB动态代理 实现. 运行时使用哪种方式与项目配置和代理的对象有关

相关推荐
亚历克斯神4 小时前
Java 安全最佳实践:构建安全的 Java 应用
java·spring·微服务
jieyucx5 小时前
# Go 语言指针零基础入门详解
开发语言·后端·golang
橙子圆1235 小时前
java之拦截器和适配器模式
java·开发语言
时空系5 小时前
第3篇:数据的运算——让数据动起来 Rust中文编程
开发语言·后端·rust
Shadow(⊙o⊙)5 小时前
智能指针、循环引用、锁、删除器
开发语言·c++·后端·visual studio
星浩AI5 小时前
OpenAI 大神 Karpathy 开源:用 Obsidian 实现 LLM Wiki 知识库管理方法
后端·openai·agent
lifewange5 小时前
Claude Code可以安装在IDEA和Pycharm中么
java·pycharm·intellij-idea
lifewange5 小时前
OpenCode可以安装在IDEA和Pycharm中么
java·pycharm·intellij-idea
untE EADO5 小时前
Java进阶之路,Java程序员职业发展规划
java·开发语言