Spring AOP 解析

写在前面

  • 本文中的内容非原创,主要来源见文末的参考链接,本人仅作整理工作,用以记录自己的学习过程,由于个人水平有限,故部分内容可能会出现错误,还请包涵

思维导图

问题来源

  • 做系统设计时,一个很重要的工作就是将系统分解,按业务功能分解成一个个低耦合、高内聚的模块
  • 分解之后发现有些功能是通用的,如
    • 日志 :对特定的操作输出日志来记录
    • 安全 :在执行操作之前进行操作检查
    • 性能 :要统计每个方法的执行时间
    • 事务 :方法开始之前要开始事务, 结束后要提交或者回滚事务
    • 等等。。。

解决方案

最简单的方法

  • 将通用模块的接口写好,在实现业务模块时调用相应的接口方法
  • 存在的问题
    • 日志、性能、事务相关的代码几乎将真正的业务代码给淹没
    • 所有希望有日志、性能等功能的类都要如此,重复代码很多

设计模式:模板方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public abstract class BaseCommand {
public void execute() {
Logger logger = Logger.getLog(...);
//记录日志
logger.debug("...");
//性能分析
PerformanceUtil.startTimer(...);
//权限检查
if (!user.hasPreviledge(...)) {
//抛出异常
}
//开始业务
beginTransaction();
//这是一个需要子类实现的抽象方法
doBusiness();

commitTransaction();

PerformanceUtil.endTimer();
logger.debug("...");
}

public abstract void doBusiness();
}

class PlaceOrderCommand extends BaseCommand {
public void doBusiness() {
//执行下订单操作
}
}

class PaymentCommand extends BaseCommand {
public void doBusiness() {
//执行支付操作
}
}
  • 在父类中将通用的非业务代码都写好,子类只关注业务逻辑
  • 存在的问题
    • 要执行哪些非功能代码,以什么顺序执行等等,子类只能无条件接受
    • 如果有个子类不需要事务,但其本身没有办法将事务代码去除

设计模式:装饰者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public interface Command {
public void execute();
}

//一个用于记录日志的装饰器
public class LoggerDecorator implements Command {
Command cmd;
public LoggerDecorator(Command command) {
this.cmd = command;
}
public void execute() {
Logger logger = Logger.getLog(...);
//记录日志
logger.debug("...");
this.cmd.execute();
logger.debug("...");
}
}

//一个用于性能统计的装饰器
public class PerformanceDecorator implements Command {
Command cmd;
public PerformanceDecorator(Command command) {
this.cmd = command;
}
public void execute() {
PerformanceUtil.startTimer(...);
this.cmd.execute();
PerformanceUtil.endTimer(...);
}
}

class PlaceOrderCommand implements Command {
public void execute() {
//执行下订单操作
}
}

class PaymentCommand implements Command {
public void execute() {
//执行支付操作
}
}
  • 现在让 PlaceOrderCommand 能够打印日志、进行性能统计分析

    1
    2
    3
    4
    Command cmd = new LoggerDecorator(
    new PerformanceDecorator(
    new PlaceOrderCommand()));
    cmd.execute();
  • 让 PaymentCommand 只打印日志,装饰一次就可以了

    1
    2
    3
    Command cmd = new LoggerDecorator(
    new PaymentCommand());
    cmd.execute();
  • 可以使用任意数量装饰器,还可以以任意次序执行(严格意义上来说是不行的)

  • 存在的问题:
    • 一个处理日志、性能、业务的类为什么要实现业务接口(Command)
    • 如果别的业务模块,没有实现Command接口,但是也想利用日志/性能/事务等功能,该怎么办

AOP

  • 鉴于装饰者模式的缺点,最好把日志、安全、事务这样的代码和业务代码完全隔离开来,因为它们的关注点和业务代码的关注点完全不同

AOP 实现

  • 首先有一个切面类(Aspect),是一个 POJO,以一个事务类为例

    1
    2
    3
    4
    5
    6
    7
    8
    public class Transaction {
    public void beginTx() {
    //开始业务
    }
    public void commitTx() {
    //提交事务
    }
    }
  • 想要实现的功能:对于某个 package 中所有类的 execute 方法,在方法调用之前需要调用 Transaction.beginTx() 方法,在调用之后需要调用 Transaction.commitTx() 方法

  • 对于某个 package 中所有类的 execute 方法:即切入点(PointCut),可以是一个方法或一组方法(可以通过通配符来支持)
  • 在方法调用之前/之后需要调用:即通知(Advice)
  • 想要描述这些规则,可以使用:
    • 使用注解创建切面,@Aspect
    • 使用 JavaConfig 类创建切面
    • 使用 XML 配置文件

Spring AOP 术语

横切关注点

  • 分布在整个应用多处的功能,如日志、安全、事务等
  • 可以被模块化为特殊的类,这些类被称为切面(aspect)

    通知(Advice)

  • 切面的工作称为通知
  • 通知定义了切面是什么以及何时使用
  • 通知有 5 种类型:
    • 前置通知 (Before):在目标方法被调用之前调用通知功能
    • 后置通知 (After):在目标方法完成之后调用通知, 此时不会关心方法的输出是什么
    • 返回通知 (After-returning):在目标方法成功执行之后调用通知
    • 异常通知 (After-throwing):在目标方法抛出异常后调用通知
    • 环绕通知 (Around):通知包裹了被通知的方法, 在被通知的方法调用之前和调用之后执行自定义的行为

连接点(Join Point)

  • 在应用执行过程种能够插入切面的一个点
  • 切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为

切点(Pointcut)

  • 一个切面并不需要通知应用的所有连接点
  • 切点有助于缩小切面所通知的连接点的范围
  • 如果说通知定义了切面是什么何时的话,那么切点就定义了何处

切面(Aspect)

  • 切面是通知和切点的结合,通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能

引入(Introduction)

  • 引入允许我们向现有的类添加新方法或属性

织入(Weaving)

  • 织入是把切面应用到目标对象并创建新的代理对象的过程
  • 切面是在指定的连接点被织入到目标对象中
  • 在目标对象的声明周期里有多个点可以进行织入:
    • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的
    • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5 的加载时织入(load-time weaving, LTW)就支持以这种方式织入切面
    • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面的

Spring AOP 实现机制

  • AOP 实现的关键在于 AOP 框架自动创建的 AOP 代理
  • AOP 代理主要分为静态代理动态代理
  • 静态代理:

    • 在编译阶段生成 AOP 代理类,即生成的字节码织入了增强后的 AOP 对象
    • AspectJ 为代表
  • 动态代理:

    • 不会修改字节码,而是在内存中临时生成一个 AOP 对象,该对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法
    • Spring AOP 为代表

JDK 动态代理

  • 通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口
  • 核心是 InvocationHandler 接口和 Proxy
  • 具体解析可阅读《Java 动态代理》

具体使用

  • 定义一个 Performance 接口和一个具体实现类 Movie

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public interface Performance {
    public void perform();
    }

    //@Component(任选一个注解均可以实现)
    @Service
    public class Movie implements Performance {

    @Override
    public void perform() {
    System.out.println("The movie is performing");
    }

    }
  • 现在我们希望电影开始和结束时有观众的动作,定义一个 Audience

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Aspect
    public class Audience {

    @Pointcut("execution(** concert.Performance.perform(..))")
    public void performance() {}

    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint jp) {
    try {
    System.out.println("Silencing cell phones");
    System.out.println("Taking seats");
    jp.proceed();
    System.out.println("CLAP CLAP CLAP!!!");
    } catch (Throwable e) {
    System.out.println("Demanding a refund");
    }
    }

    }
  • 使用 JavaConfig 类来进行配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Configuration
    @EnableAspectJAutoProxy
    @ComponentScan(basePackages="concert") //(扫描 concert 包下的所有组件)
    public class Config {

    @Bean
    public Audience audience() {
    return new Audience();
    }

    @Bean
    public Performance performance() {
    return new Movie();
    }

    }
  • 编写一个测试类来查看效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes=Config.class)
    public class TestPerformance {

    @Autowired
    private Audience audience;

    @Autowired
    private Performance performance;

    @Test
    public void testAudience() {
    performance.perform();
    }
    }
  • 测试结果如下

    1
    2
    3
    4
    Silencing cell phones
    Taking seats
    The movie is performing
    CLAP CLAP CLAP!!!

CGLIB 动态代理

  • 若目标类没有实现接口,则 Spring AOP 会使用 CGLIB 来动态代理目标类
  • CGLIB 是一个代码生成的类库,可以在运行时动态的生成某个类的子类
  • 通过继承的方式实现动态代理,因此若某个类被标记为 final,则无法通过 CGLIB 实现动态代理,诸如 private 的方法也是不可以作为切面的

具体实现

  • To do

参考链接