AOP概述

AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善。

AOP使用的设计模式是代理模式

AOP将公共逻辑(缓存,日志, 事务管理)封装成切面,与业务代码分离,可以减少系统的重复代码和减低耦合度。

切面就是指那些与业务无关,但是业务模块都需要调用的公共逻辑。

AOP实现方式(静态与动态)

静态代理和动态代理

  • 静态代理:其实就是代理类和原来的类都实现了一个公共的接口,在代理类调用了被代理类的方法,在调用之前加上一些代码逻辑达到增强的效果,静态代理的代理类在编译阶段就生成了,也称编译时增强。

    缺点:代理对象要与目标对象实现同一个接口;一旦这个接口增加方法,目标对象和代理对象都需要维护。

  • 动态代理:代理类在程序运行的时候创建,AOP框架不会去修改字节码,而是在内存之中生成一个代理对象,在运行期间对业务方法进行增强,不会生成新的类。

场景模拟

假设现在有一个计算器接口Calculator,包含加减乘除的抽象方法

1
2
3
4
5
6
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}

此接口的实现类:

1
2
3
4
5
6
7
8
9
10
public class CalculatorPureImpl implements Calculator {
@Override
//加法
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
...省略减乘除方法
}

如果此时要给这个计算机加上日志功能呢 ?

正常思维 : 在加减乘除的内部加上日志的输出,例如:

1
2
3
4
5
6
7
8
   public int add(int i, int j) {
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
int result = i + j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] add 方法结束了,结果是:" + result);
return result;
}
//在减法、乘除法中类似,省略...

问题 :

  • 对核心业务功能有干扰,日志属于非核心代码。
  • 附加功能分散在各个业务功能方法中,不利于统一维护(不利于封装)

解决思路 : 解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。

困难 : :要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术 : 代理模式

代理模式 : 通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来

关于代理模式,后面再详细出一篇文章吧。

静态代理

代理模式为目标对象创建代理对象,调用目标方法通过调用代理对象的方法实现,因此目标对象有啥,代理对象就有啥.

创建静态代理类 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//由于代理对象拥有目标对象的所有方法,因此代理对象一定要和目标对象实现相同的接口.
public class CalculatorStaticProxy implements Calculator {
// 将被代理的目标对象声明为成员变量
private Calculator target;
public CalculatorStaticProxy(Calculator target) {
this.target = target;
}
@Override
public int add(int i, int j) {
// 附加功能由代理类中的代理方法来实现,这里实现日志功能.
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑,实现计算功能
int addResult = target.add(i, j);
System.out.println("[日志] add 方法结束了,结果是:" + addResult);
return addResult;
}
}

静态代理确实实现了解耦,将核心代码和非核心代码分开.

测试

1
2
3
4
5
6
7
public class Proxytest{
@Test
public void testProxy() {
CalculatorStaticProxy proxy = new CalculatorStaticProxy(new Calculator());
proxy.add(1,2);
}
}

缺点 : 由于代码都写死了,完全不具备任何的灵活性。

就拿日志功能来说,将来其他类也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。

提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。

动态代理

动态代理的动态 : 指不需要手动创造代理类, 会动态生成目标类的代理类

这里实现的是jdk动态代理(要求必须有接口,最终生成的代理类在com.sun.proxy下,类名为$proxy2)

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
//ProxyFactory是一个工厂类
public class ProxyFactory {
//不知道目标类是什么类,所以这里写Object类
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
//不知道代理对象是啥类型,返回值还是Object
public Object getProxy(){
/**
* 使用newProxyInstance(),创建一个代理实例 : 通过这个方法创建的动态代理对象
* 在使用之前,其中有三个参数:
* 1、classLoader:指定加载动态生成的代理类的类加载器 (类想被执行,就要先经过加载)
* 2、interfaces:获取目标对象实现的所有接口的class对象所组成的数组(目标类和代理类要实现相同的接口)
* 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
*/

ClassLoader classLoader = target.getClass().getClassLoader();
//通过反射获取
Class<?>[] interfaces = target.getClass().getInterfaces();
//重写接口中的抽象方法
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
//invoke是代理对象该如何执行方法,调用的是目标对象的功能并作出添加
//proxy:代理对象 method:代理对象需要实现的方法,即其中需要重写的方法 args:method所对应方法的参数列表
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
//这里就是日志功能
System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args));
//!!!最重要的 !!! 这里就表示了调用目标对象实现功能的过程.
//method是方法,method.invoke就是执行这个方法,参数:1.当前使用的对象, 2.当前使用对象的参数列表
result = method.invoke(target, args);
System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
return result;
}
};
//创建代理类
return Proxy.newProxyInstance(classLoader, interfaces,invocationHandler);
}
}

测试

1
2
3
4
5
6
7
8
9
@Test
public void testDynamicProxy(){
//代理模式一定是通过代理对象进行访问,而不是目标对象
//通过动态代理工厂类创建代理对象
ProxyFactory factory = new ProxyFactory(new CalculatorLogImpl());
//不知道当前动态生成的代理类的类型,但是知道其实现的接口,因此可以向上转型获取其实现的接口的对象
Calculator proxy = (Calculator) factory.getProxy();
proxy.add(1,0);
}

AOP相关术语

首先明确一下AOP要做的事情 :

  1. 把非核心代码抽取出来,交给切面管理
  2. 把它作用到目标对象的方法中

横切关注点

从目标对象(核心代码)中抽取出来的同一类非核心业务。(指前面计算器中的日志功能)

一个方法中,可以有多个横切关注点.

横切关注点是对于目标对象来说的.

通知

每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。

我们要把横切关注点封装到切面中,而在切面中,每一个横切关注点都表示为一个通知方法.

通知是针对切面而言的.

通知分为5种 :

  1. 前置通知:使用@Before注解标识,在被代理的目标方法执行

    返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行

    异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(

    后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行

    环绕通知:使用@Around注解标识,使用try…catch…finally结构围绕整个被代理的目标方法,包

    括上面四种通知对应的所有位置

切面

切面是用来封装横切关注点(对于目标对象而言)封装通知方法(对于切面而言)的类 .

AOP叫做面向切面编程,就是切面或切面中的通知重要.

目标

被代理的目标对象\

要进行功能增强的对象\

要被抽取非核心代码的对象

代理

向目标对象应用通知之后创建的代理对象。

注意 : 代理是AOP帮助我们创建的,不需要通过上面的静态或动态代理方法自己创建)

连接点

抽取横切关注点的位置.

比如从 方法开始 / 捕获异常 / 方法结束的位置抽取出来的

知道连接点的作用 : 从哪儿抽取出来,就要到哪儿

切入点

定位连接点的方式。

每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。

知道连接点之后,通过切入点将通知在代码层面套到连接点 .

切入点是一个表达式,可以用切入点定位连接点

小结

AOP到底应该怎么做?

首先,要想实现AOP,那么目标对象一定是提前就有的.(即我们分析之后,有了需要进行功能增强的目标对象,才会去使用AOP)

其次是代理对象,代理对象不需要自己创建

所以我们要做的是在目标对象中把非核心代码抽取出来,这里的非核心代码就是横切关注点

抽完之后把它放在一个类中 ,.这个类叫做切面

在切面中如何封装横切关注点 ? 每一个横切关注点都是一个方法,而这个方法就叫通知

然后再通过切入点,定位到连接点

这时就可以在不改变目标对象代码的同时,把我们切面中的这些通知,通过切入点表达式,来套到连接点上,来实现功能增强

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
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;

// @Aspect表示这个类是一个切面类,即LoggingAspect是一个切面
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
public class LoggingAspect {
//前置通知:使用@Before注解标识,在被代理的目标方法**前**执行
//在 @Before 注解中,我们使用切点表达式 execution(* com.example.MyService.*(..)) 来指定切点。
@Before("execution(* com.example.MyService.*(..))")
//在前置通知方法 logBeforeMethod() 中,我们定义了要在切点匹配的方法执行前执行的逻辑,即输出日志信息。
public void logBeforeMethod() {
System.out.println("Before method execution: Logging...");
}
}
//在切面中定义了在执行 MyService 类的方法前输出日志的通知。
@Component
class MyService {
public void doSomething() {
System.out.println("Doing something...");
}
}
// 我们在 AopExample 类中使用Spring容器来获取 MyService 的实例并调用其方法。
public class AopExample {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AopExample.class);
MyService myService = context.getBean(MyService.class);
//当 doSomething() 方法被调用时,Spring AOP会自动触发前置通知,输出日志信息。
myService.doSomething();
}
}

AOP的作用

  • 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
  • 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。

AOP的应用场景

  • 日志场景:诊断上下文 log4j ; 辅助信息 :方法执行时间等
  • 统计场景:方法调用次数, 执行异常次数, 数值累加
  • 安防场景:限流降流(阿里的sentinel)
  • 性能场景:超时控制