AOP

简介: 底层实现方式之一是代理,由代理结合通知和目标,提供增强功能除此以外,aspectj 提供了两种另外的 AOP 底层实现:第一种是通过 ajc 编译器在编译 class 类文件时,就把通知的增强功能,织入到目标类的字节码中第二种是通过 agent 在加载目标类时,修改目标类的字节码,织入增强功能作为对比,代理是运行时生成新的字节码简单比较的话:aspectj 在编译和加载时,修改目标字节码,性能较高aspectj 因为不用代理,能突破一些技术上的限制,例如对构造、对静态方法、对 final 也能增强但 aspectj 侵入性较强,且需要学习新的 aspectj 特有语

底层实现方式之一是代理,由代理结合通知和目标,提供增强功能

除此以外,aspectj 提供了两种另外的 AOP 底层实现:

第一种是通过 ajc 编译器在编译 class 类文件时,就把通知的增强功能,织入到目标类的字节码中

第二种是通过 agent 在加载目标类时,修改目标类的字节码,织入增强功能

作为对比,代理是运行时生成新的字节码

简单比较的话:

aspectj 在编译和加载时,修改目标字节码,性能较高
aspectj 因为不用代理,能突破一些技术上的限制,例如对构造、对静态方法、对 final 也能增强
但 aspectj 侵入性较强,且需要学习新的 aspectj 特有语法,因此没有广泛流行
AOP 实现之 ajc 编译器
Aop的实现原理有很多,并不是只有代理(当然在Spring中就是代理)。

我们直接来看使用ajc实现Aop的代码:

通知类:

@Aspect // ⬅️注意此切面并未被 Spring 管理
public class MyAspect {

private static final Logger log = LoggerFactory.getLogger(MyAspect.class);

@Before("execution(* com.zyb.service.MyService.foo())")
public void before() {
    log.debug("before()");
}

}
1
2
3
4
5
6
7
8
9
10
切点:

@Service
public class MyService {

private static final Logger log = LoggerFactory.getLogger(MyService.class);

public static void foo() {
    log.debug("foo()");
}

}
1
2
3
4
5
6
7
8
9
@SpringBootApplication
public class A09 {

private static final Logger log = LoggerFactory.getLogger(A09.class);

public static void main(String[] args) {

// ConfigurableApplicationContext context = SpringApplication.run(A10Application.class, args);
// MyService service = context.getBean(MyService.class);
//
// log.debug("service class: {}", service.getClass());
// service.foo();
//
// context.close();

    new MyService().foo();

    /*           
       aop 的原理并非代理一种, 编译器也能玩出花样
     */
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
注意💡:

编译器也能修改 class 实现增强
编译器增强能突破代理仅能通过方法重写增强的限制:可以对构造方法、静态方法等实现增强
不在Spring的管理范围内
注意

版本选择了 java 8, 因为目前的 aspectj-maven-plugin 1.14.0 最高只支持到 java 16
一定要用 maven 的 compile 来编译, idea 不会调用 ajc 编译器
AOP 实现之 agent 类加载
agent 通过类加载时修改 class 实现增强

我们对foo、bar方法均进行了增强

我们知道如果是使用代理,那么因为bar方法在foo方法内部是通过this调用的,所以不会被增强,但使用agent类加载的方式就不会出现这种情况,它相当于直接修改了我们MyService的字节吗,我们可以来看看:

注意:

运行时需要在 VM options 里加入 -javaagent:C:/Users/manyh/.m2/repository/org/aspectj/aspectjweaver/1.9.7/aspectjweaver-1.9.7.jar 把其中 C:/Users/manyh/.m2/repository 改为你自己 maven 仓库起始地址

AOP 实现之 proxy
JDK代理
我们知道JDK动态代理要求目标必须实现接口,生成的代理类实现相同接口,因此代理与目标之间是平级兄弟关系。

我们来看看一段具体的代码:

要产生代理对象我们就要使用JDK提供的一个方法叫做Proxy.newProxyInstance方法。

它需要三个参数:

类加载器。我们的普通类是先写java源代码,将源代码编译成字节码,然后经过类加载进行使用。而代理类不一样,它没有源代码,他是在运行期间直接生成的字节码,而这个字节吗也要通过类加载才能运行。而这个操作需要我们的类加载器。
代理要实现的接口数组。其本质就是规定要拦截的方法。
InvocationHandler回调处理程序。我们的代理类创建出来了,它也实现了我们的接口,那么就要实现我们接口中的抽象方法,你必须要规定方法的行为,而InvocationHandler正是将这些行为进行封装。当代理类中的方法被调用时,它就会执行InvocationHandler中的invoke方法,而这个invoke方法又有三个参数:
代理对象自己
正在执行的方法对象
方法传过来的实际参数
public class JdkProxyDemo {

interface Foo {
    void foo();
}

static final class Target implements Foo {
    public void foo() {
        System.out.println("target foo");
    }
}

// jdk 只能针对接口代理
// cglib
public static void main(String[] param) throws IOException {
    // 目标对象
    Target target = new Target();

    ClassLoader loader = JdkProxyDemo.class.getClassLoader(); // 用来加载在运行期间动态生成的字节码
    Foo proxy = (Foo) Proxy.newProxyInstance(loader, new Class[]{Foo.class}, (p, method, args) -> {
        System.out.println("before...");
        // 目标.方法(参数)
        // 方法.invoke(目标, 参数);
        Object result = method.invoke(target, args);
        System.out.println("after....");
        return result; // 让代理也返回目标方法执行的结果
    });

    proxy.foo();
}

}

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
注意代理与目标之间是平级兄弟关系,所以他们之间是不能强转的。

CGLIB代理
我们直接上代码:

cglib中的代理类是通过一个叫Enhancer的类中的create方法来创建的。

创建代理的时候我们需要指定他的父亲。cglib跟我们JDK不一样的地方就在于它是通过父子继承关系来创建的代理。同时还要指定代理类中方法执行的行为,当然这里使用的时候我们一般不直接使用Callback接口,而是使用它的子接口MethodInterceptor。我们重写其中的intercept方法,这个方法有四个参数:

第一个代表代理对象自己
当前代理类中正在执行的方法
方法执行时的实际参数
方法代理
总结一下:

cglib 不要求目标实现接口,它生成的代理类是目标的子类,因此代理与目标之间是子父关系(两者之间可以强转)
限制⛔: final 类无法被 cglib 增强,final 修饰的方法也无法被增强(cglib增强是通过方法的重写)
然后我们来说说刚才intercept中的第四个参数MethodProxy。它可以避免反射调用方法:

    Target proxy = (Target) Enhancer.create(Target.class, (MethodInterceptor) (p, method, args, methodProxy) -> {
        System.out.println("before...");

// Object result = method.invoke(target, args); // 用方法反射调用目标
// methodProxy 它可以避免反射调用
// Object result = methodProxy.invoke(target, args); // 内部没有用反射, 需要目标 (spring用的这种)
Object result = methodProxy.invokeSuper(p, args); // 内部没有用反射, 需要代理
System.out.println("after...");
return result;
});
1
2
3
4
5
6
7
8
9
JDK动态代理进阶
JDK动态代理的内部实现就在newProxyInstance方法中,但是其内部使用的是ASM动态生成代理类的字节码,看不到代理类的java代码是什么样子的。所以这里我们来模拟一个JDK代理实现,也就是自己写一个动态代理类:

我们的初步实现如下:

目标类:

public class A12 {

interface Foo {
    void foo();
    void bar();
}

static class Target implements Foo {
    public void foo() {
        System.out.println("target foo");
    }

    @Override
    public void bar() {
        System.out.println("target bar");
    }
}

interface InvocationHandler {
    void invoke(Method method, Object[] args) throws Throwable;
}

public static void main(String[] param) {
    Foo proxy = new $Proxy0(new InvocationHandler() {
        @Override
        public Object invoke(Method method, Object[] args) throws Throwable{
            // 1. 功能增强
            System.out.println("before...");
            // 2. 调用目标

// new Target().foo();
method.invoke(new Target(), args);
}
});
proxy.foo();
proxy.bar();

}

}

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
代理类:

public class $Proxy0 implements A12.Foo {

public $Proxy0(InvocationHandler h) {
    super(h);
}
@Override
public void foo() {
    try {
        Method foo = Foo.class.getMethod("foo");
        h.invoke(foo, new Object[0]);
    } catch (Throwable e) {
        e.printStackTrace();
    }
}

@Override
public int bar() {
    try {
         Method bar = Foo.class.getMethod("foo");
        h.invoke(bar, new Object[0]);
        return (int) result;
    } catch (Throwable e) {
        e.printStackTrace();
    }
}

}

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
现在我们考虑一下返回值的处理,我们把bar方法的返回值改为int,处理方面比较简单,将我们的invoke方法添加一个返回值即可:

然后异常的处理也要改进一下,运行时异常直接抛,检查异常要转换后抛,另外不需要每次调用方法的时候都去获取一次方法对象,我们可以把它放在静态代码块中,最后我们吧invoke方法中的代理对象参数进行了补充(虽然这个参数一般很少使用):

总结💡:

代理一点都不难,无非就是利用了多态、反射的知识

方法重写可以增强逻辑,只不过这【增强逻辑】千变万化,不能写死在代理内部
通过接口回调将【增强逻辑】置于代理类之外
配合接口方法反射(是多态调用),就可以再联动调用目标方法
限制⛔:代理增强是借助多态来实现,因此成员变量、静态方法、final 方法均不能通过代理实现
JDK动态代理优化:

在InvocationHandler中我们基本上都要使用反射来调用目标方法:

我们知道反射调用性能是比较低的,那么JDK有没有什么优化措施呢?

我们写一段测试代码:

// 运行时请添加 --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED
public class TestMethodInvoke {
public static void main(String[] args) throws Exception {
Method foo = TestMethodInvoke.class.getMethod("foo", int.class);
for (int i = 1; i <= 17; i++) {
show(i, foo);
foo.invoke(null, i);
}
System.in.read();
}

// 方法反射调用时, 底层 MethodAccessor 的实现类
private static void show(int i, Method foo) throws Exception {
    Method getMethodAccessor = Method.class.getDeclaredMethod("getMethodAccessor");
    getMethodAccessor.setAccessible(true);
    Object invoke = getMethodAccessor.invoke(foo);
    if (invoke == null) {
        System.out.println(i + ":" + null);
        return;
    }
    Field delegate = Class.forName("jdk.internal.reflect.DelegatingMethodAccessorImpl").getDeclaredField("delegate");
    delegate.setAccessible(true);
    System.out.println(i + ":" + delegate.get(invoke));
}

public static void foo(int i) {
    System.out.println(i + ":" + "foo");
}

}

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

前 16 次使用反射性能较低
第 17 次调用会生成代理类,优化为非反射调用(直接正常调用)
我们来看看这个代理类:

CGLIB代理进阶
同样我们还是使用代码模拟cglib的真实代理类,其内部逻辑与JDK差不多:

目标类:

代理类:

public class Proxy extends Target {

private MethodInterceptor methodInterceptor;

public void setMethodInterceptor(MethodInterceptor methodInterceptor) {
    this.methodInterceptor = methodInterceptor;
}

static Method save0;
static Method save1;
static Method save2;
static {
    try {
        save0 = Target.class.getMethod("save");
        save1 = Target.class.getMethod("save", int.class);
        save2 = Target.class.getMethod("save", long.class);          
    } catch (NoSuchMethodException e) {
        throw new NoSuchMethodError(e.getMessage());
    }
}

@Override
public void save() {
    try {
        methodInterceptor.intercept(this, save0, new Object[0], null);
    } catch (Throwable e) {
        throw new UndeclaredThrowableException(e);
    }
}

@Override
public void save(int i) {
    try {
        methodInterceptor.intercept(this, save1, new Object[]{i}, null);
    } catch (Throwable e) {
        throw new UndeclaredThrowableException(e);
    }
}

@Override
public void save(long j) {
    try {
        methodInterceptor.intercept(this, save2, new Object[]{j}, null);
    } catch (Throwable e) {
        throw new UndeclaredThrowableException(e);
    }
}

}

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
44
45
46
47
48
我们前面说过JDK的方法反射调用前16次性能比较低,后来有一个优化在内部替换成方法的直接调用。而cglib也有优化,它是通过intercept方法中的最后一个参数MethodProxy来避免反射调用进行优化。

那么接下来我们就来看看这个MethodProxy是怎么实现的

MethodProxy
首先我们弄清楚MethodProxy是怎么创建的。

我们需要在代理类中写出带原始功能的方法,然后调用MethodProxy的create方法创建MethodProxy对象,该方法有5个参数:

目标类型
代理类型
方法参数返回值描述符
带增强功能的方法名
带原始功能的方法名
代码如下:

public class Proxy extends Target {

private MethodInterceptor methodInterceptor;

public void setMethodInterceptor(MethodInterceptor methodInterceptor) {
    this.methodInterceptor = methodInterceptor;
}

static Method save0;
static Method save1;
static Method save2;
static MethodProxy save0Proxy;
static MethodProxy save1Proxy;
static MethodProxy save2Proxy;
static {
    try {
        save0 = Target.class.getMethod("save");
        save1 = Target.class.getMethod("save", int.class);
        save2 = Target.class.getMethod("save", long.class);
        save0Proxy = MethodProxy.create(Target.class, Proxy.class, "()V", "save", "saveSuper");
        save1Proxy = MethodProxy.create(Target.class, Proxy.class, "(I)V", "save", "saveSuper");
        save2Proxy = MethodProxy.create(Target.class, Proxy.class, "(J)V", "save", "saveSuper");
    } catch (NoSuchMethodException e) {
        throw new NoSuchMethodError(e.getMessage());
    }
}

// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 带原始功能的方法
public void saveSuper() {
    super.save();
}
public void saveSuper(int i) {
    super.save(i);
}
public void saveSuper(long j) {
    super.save(j);
}
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 带增强功能的方法
@Override
public void save() {
    try {
        methodInterceptor.intercept(this, save0, new Object[0], save0Proxy);
    } catch (Throwable e) {
        throw new UndeclaredThrowableException(e);
    }
}

@Override
public void save(int i) {
    try {
        methodInterceptor.intercept(this, save1, new Object[]{i}, save1Proxy);
    } catch (Throwable e) {
        throw new UndeclaredThrowableException(e);
    }
}

@Override
public void save(long j) {
    try {
        methodInterceptor.intercept(this, save2, new Object[]{j}, save2Proxy);
    } catch (Throwable e) {
        throw new UndeclaredThrowableException(e);
    }
}

}

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
然后我们调用一下:

可以看到;

method.invoke 是反射调用,必须调用到足够次数才会进行优化
methodProxy.invoke 是不反射调用,它会正常(间接)调用目标对象的方法(Spring 采用)
methodProxy.invokeSuper 也是不反射调用,它会正常(间接)调用代理对象的方法,可以省略目标对象
我们要清楚MethodProxy 优化的点在于:我们原来是使用反射的形式去调用原方法,现在我们通过它可以不使用反射的方法调用原方法,提高了性能。

那么这是怎么实现的呢?

当调用 MethodProxy 的 invoke 或 invokeSuper 方法时, 会动态生成两个类
ProxyFastClass 配合代理对象一起使用, 避免反射
TargetFastClass 配合目标对象一起使用, 避免反射 (Spring 用的这种)
TargetFastClass 记录了目标类中方法(也就是上面例子中的Target类)与编号的对应关系
save(long) 编号 2
save(int) 编号 1
save() 编号 0
首先根据方法名和参数个数、类型, 用 switch 和 if 找到这些方法编号
然后再根据编号去调用目标方法, 又用了一大堆 switch 和 if, 但避免了反射
ProxyFastClass 记录了代理类中方法(也就是上面例子中的Proxy类)与编号的对应关系,不过 Proxy 额外提供了下面几个方法
saveSuper(long) 编号 2,不增强,仅是调用 super.save(long)
saveSuper(int) 编号 1,不增强, 仅是调用 super.save(int)
saveSuper() 编号 0,不增强, 仅是调用 super.save()
查找方式与 TargetFastClass 类似
为什么有这么麻烦的一套东西呢?
避免反射, 提高性能, 代价是一个代理类配两个 FastClass 类, 代理类中还得增加仅调用 super 的一堆方法
用编号处理方法对应关系比较省内存, 另外, 最初获得方法顺序是不确定的, 这个过程没法固定死
我们使用代码模拟一下:

TargetFastClass:

public class TargetFastClass {
static Signature s0 = new Signature("save", "()V");
static Signature s1 = new Signature("save", "(I)V");
static Signature s2 = new Signature("save", "(J)V");

// 获取目标方法的编号
/*
    Target
        save()              0
        save(int)           1
        save(long)          2
    signature 包括方法名字、参数返回值
 */
public int getIndex(Signature signature) {
    if (s0.equals(signature)) {
        return 0;
    } else if (s1.equals(signature)) {
        return 1;
    } else if (s2.equals(signature)) {
        return 2;
    }
    return -1;
}

// 根据方法编号, 正常调用目标对象方法
public Object invoke(int index, Object target, Object[] args) {
    if (index == 0) {
        ((Target) target).save();
        return null;
    } else if (index == 1) {
        ((Target) target).save((int) args[0]);
        return null;
    } else if (index == 2) {
        ((Target) target).save((long) args[0]);
        return null;
    } else {
        throw new RuntimeException("无此方法");
    }
}

public static void main(String[] args) {
    TargetFastClass fastClass = new TargetFastClass();
    int index = fastClass.getIndex(new Signature("save", "(I)V"));
    System.out.println(index);
    fastClass.invoke(index, new Target(), new Object[]{100});
}

}

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
44
45
46
47
getIndex方法的作用:根据方法签名信息获取目标方法的编号

    Target
        save()              0
        save(int)           1
        save(long)          2
    signature 包括方法名字、参数返回值

1
2
3
4
5
invoke方法的作用:根据上一步返回的方法编号去正常调用目标对象的方法。该方法接受三个参数:

方法编号
目标对象
方法的参数列表

然后我们再来模拟一下ProxyFastClass:

public class ProxyFastClass {
static Signature s0 = new Signature("saveSuper", "()V");
static Signature s1 = new Signature("saveSuper", "(I)V");
static Signature s2 = new Signature("saveSuper", "(J)V");

// 获取代理方法的编号
/*
    Proxy
        saveSuper()              0
        saveSuper(int)           1
        saveSuper(long)          2
    signature 包括方法名字、参数返回值
 */
public int getIndex(Signature signature) {
    if (s0.equals(signature)) {
        return 0;
    } else if (s1.equals(signature)) {
        return 1;
    } else if (s2.equals(signature)) {
        return 2;
    }
    return -1;
}

// 根据方法编号, 正常调用目标对象方法
public Object invoke(int index, Object proxy, Object[] args) {
    if (index == 0) {
        ((Proxy) proxy).saveSuper();
        return null;
    } else if (index == 1) {
        ((Proxy) proxy).saveSuper((int) args[0]);
        return null;
    } else if (index == 2) {
        ((Proxy) proxy).saveSuper((long) args[0]);
        return null;
    } else {
        throw new RuntimeException("无此方法");
    }
}

public static void main(String[] args) {
    ProxyFastClass fastClass = new ProxyFastClass();
    int index = fastClass.getIndex(new Signature("saveSuper", "()V"));
    System.out.println(index);

    fastClass.invoke(index, new Proxy(), new Object[0]);
}

}

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
44
45
46
47
48
49
他的不同点在于:

它的getIndex方法获取的不是目标中方法的编号,而是代理中方法的标号
invoke方法中调用的也是代理中的目标原始方法
最后总结一下cglib的反射优化比JDK的反射优化强在哪?

JDK中的优化是一个方法调用就是一个代理,而cglib中,一个代理类只会对应两个FastClass,每个FastClass可以匹配到多个方法。其代理类的数目相比于JDK要少一些。

JDK 和 CGLIB 在 Spring 中的统一
首先我们要明白两个切面概念:

        aspect =
            通知1(advice) +  切点1(pointcut)
            通知2(advice) +  切点2(pointcut)
            通知3(advice) +  切点3(pointcut)
            ...
        advisor = 更细粒度的切面,包含一个通知和切点

1
2
3
4
5
6
我们平时看到的aspect 最终在生效执行之前会被拆解成多个advisor

Spring 中对切点、通知、切面的抽象如下

切点:接口 Pointcut,典型实现 AspectJExpressionPointcut

通知:典型接口为 MethodInterceptor 代表环绕通知

切面:Advisor,包含一个 Advice 通知,PointcutAdvisor 包含一个 Advice 通知和一个 Pointcut



«interface»
Advice
«interface»
MethodInterceptor
«interface»
Advisor
«interface»
PointcutAdvisor
«interface»
Pointcut
AspectJExpressionPointcut
我们写一段代码试验一下:

public class A15 {
public static void main(String[] args) {
/
两个切面概念
aspect =
通知1(advice) + 切点1(pointcut)
通知2(advice) + 切点2(pointcut)
通知3(advice) + 切点3(pointcut)
...
advisor = 更细粒度的切面,包含一个通知和切点
/

    // 1. 备好切点
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression("execution(* foo())");
    // 2. 备好通知
    MethodInterceptor advice = invocation -> {
        System.out.println("before...");
        Object result = invocation.proceed(); // 调用目标
        System.out.println("after...");
        return result;
    };
    // 3. 备好切面
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);

    /*
       4. 创建代理
     */
    Target1 target = new Target1();
    ProxyFactory factory = new ProxyFactory();
    factory.setTarget(target);
    factory.addAdvisor(advisor);
    Target1 proxy = (Target1) factory.getProxy();
    System.out.println(proxy.getClass());
    proxy.foo();
    proxy.bar();

}

interface I1 {
    void foo();

    void bar();
}

static class Target1 implements I1 {
    public void foo() {
        System.out.println("target1 foo");
    }

    public void bar() {
        System.out.println("target1 bar");
    }
}

static class Target2 {
    public void foo() {
        System.out.println("target2 foo");
    }

    public void bar() {
        System.out.println("target2 bar");
    }
}

}

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
注意

要区分这里代码中的 MethodInterceptor,它与之前 cglib 中用的的 MethodInterceptor 是不同的接口
我们打印之后发现使用的是CGLIB的代理:

那么spring底层什么时候使用JDK动态代理,什么时候又使用CGLIB动态代理呢?

在工厂ProxyFactory中:

proxyTargetClass = false(默认为false), 目标实现了接口, 用 jdk 实现
proxyTargetClass = false, 目标没有实现接口, 用 cglib 实现
proxyTargetClass = true, 总是使用 cglib 实现
例外:如果目标是接口类型或已经是 Jdk 代理,使用 JdkDynamicAopProxy
代理相关类图

AopProxyFactory 根据 proxyTargetClass 等设置选择 AopProxy 实现
AopProxy 通过 getProxy 创建代理对象
图中 Proxy 都实现了 Advised 接口,能够获得关联的切面集合与目标(其实是从 ProxyFactory 取得)
调用代理方法时,会借助 ProxyFactory 将通知统一转为环绕通知:MethodInterceptor

使用
创建
创建
«interface»
Advised
ProxyFactory
proxyTargetClass : boolean
Target
Advisor
«interface»
AopProxyFactory
«interface»
AopProxy
+getProxy() : Object
基于CGLIB的Proxy
ObjenesisCglibAopProxy
advised : ProxyFactory
JdkDynamicAopProxy
advised : ProxyFactory
基于JDK的Proxy
切点匹配
我们知道在Spring中切点的典型实现是AspectJExpressionPointcut,在切点表达式中如果我们想匹配方法,也可以匹配注解,例如:

    AspectJExpressionPointcut pt1 = new AspectJExpressionPointcut();
    pt1.setExpression("execution(* bar())");
    System.out.println(pt1.matches(T1.class.getMethod("foo"), T1.class));
    System.out.println(pt1.matches(T1.class.getMethod("bar"), T1.class));

    AspectJExpressionPointcut pt2 = new AspectJExpressionPointcut();
    pt2.setExpression("@annotation(org.springframework.transaction.annotation.Transactional)");
    System.out.println(pt2.matches(T1.class.getMethod("foo"), T1.class));
    System.out.println(pt2.matches(T1.class.getMethod("bar"), T1.class));

1
2
3
4
5
6
7
8
9
当然在Spring中我们常见的Transactional注解其实并不是用这种方法进行匹配的。

因为Transactional注解它有多种使用方法,加在方法、类、接口上都可以:

我们的切点表达式有一定的局限性,不管是execution还是@annotation也好,它们都只能匹配方法的信息:

execution匹配方法的名字、参数、返回值等等
@annotation匹配的是方法上加没加注解
都不能处理类上的信息,此时我们就不能使用AspectJExpressionPointcut这种切点实现类了。Spring中也使用了别的实现,但是这个实现类不是public,我们就自己用代码实现有一下:

    StaticMethodMatcherPointcut pt3 = new StaticMethodMatcherPointcut() {
        @Override
        public boolean matches(Method method, Class<?> targetClass) {
            // 检查方法上是否加了 Transactional 注解
            MergedAnnotations annotations = MergedAnnotations.from(method);
            if (annotations.isPresent(Transactional.class)) {
                return true;
            }
            // 查看类上是否加了 Transactional 注解
            annotations = MergedAnnotations.from(targetClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY);
            if (annotations.isPresent(Transactional.class)) {
                return true;
            }
            return false;
        }
    };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
首先我们继承一个抽象父类StaticMethodMatcherPointcut
我们实现该接口的matches方法即可
这个方法会给我们提供两个参数:方法对象以及方法所在的类对象。
总结一下:

底层切点实现是如何匹配的: 调用了 aspectj 的匹配方法
比较关键的是它实现了 MethodMatcher 接口, 用来执行方法的匹配
从 @Aspect 到 Advisor
@Aspect:

比较高级的切面
可以包含多组通知和切点
Spring内部最终还是会将其转化为Advisor

Advisor:

比较低级的切面,适合框架内部使用,不适合我们编程
只包含一组通知和切点

接下来我们介绍一个Bean后处理器AnnotationAwareAspectJAutoProxyCreator ,它的作用:

找到容器中所有的切面,并把高级切面转换成低级切面
在合适的时机,根据切面使用ProxyFactory创建代理对象(依赖注入之前或初始化之后)
这个bean后处理器有两个重要的方法:

findEligibleAdvisors()方法 找到有【资格】的 Advisors
有【资格】的 Advisor 一部分是低级的, 可以由自己编写
有【资格】的 Advisor 另一部分是高级的, 由解析 @Aspect 后获得
wrapIfNecessary()方法
它内部调用 findEligibleAdvisors, 只要返回集合不空, 则表示需要创建代理
它的调用时机通常在原始对象初始化后执行, 但碰到循环依赖会提前至依赖注入之前执行
测试代码:

结果:

然后我们详细说说代理的创建时机:

代理的创建时机

初始化之后 (无循环依赖时)
实例创建后, 依赖注入前 (有循环依赖时), 并暂存于二级缓存
我们使用代码演示一下这两种情况:

在这里我们对Bean1中的foo方法进行了增强,启动容器之后发现:

我们将Bean1打印出来也可以看到是使用CGLIB创建的代理对象

我们再看第二种情况:

启动容器之后发现:

此时Bean1的代理对象在依赖注入之前就被创建了(也就是在bean2的生命周期期间)。

最后我们要注意:

依赖注入与初始化不应该被增强(也就是调用set和init方法时), 仍应被施加于原始对象

切面的顺序控制使用order即可:

高级切面使用@Order直接即可,但是这个@Order只能放在类上不能放在方法上。
低级界面使用setOrder方法
然后我们再说说如何把高级切面类转化为Advisor。

我们使用代码模拟一下,我们就以@Before注解为例,其他注解同理:

@Before 前置通知会被转换为原始的 AspectJMethodBeforeAdvice 形式, 该对象包含了如下信息
通知代码从哪儿来
切点是什么(有可能需要切点的参数信息)
通知对象如何创建, 本例共用同一个 Aspect 对象
类似的还有
AspectJAroundAdvice (环绕通知)
AspectJAfterReturningAdvice(正常返回通知)
AspectJAfterThrowingAdvice (异常通知)
AspectJAfterAdvice (后置通知)
通知转换
接着前面说,将高级切面类转化为一个个的Advisor时,其里面的通知最终都会转化为MethodInterceptor的环绕通知。

那么为什么这么多通知类型最终都要转换为环绕通知呢?

注意:只有非环绕通知会转化为环绕通知,如果已经是环绕通知则不会进行转换。

AspectJAroundAdvice (环绕通知)
AspectJAfterReturningAdvice(正常返回通知,后置通知)
AspectJAfterThrowingAdvice (异常通知,环绕通知)
AspectJAfterAdvice (后置通知,环绕通知)
而具体的转换步骤是由ProxyFactory 帮我们实现的。

我们使用代码模拟一下:

通过 proxyFactory 的 getInterceptorsAndDynamicInterceptionAdvice() 将其他通知统一转换为 MethodInterceptor 环绕通知
ProxyFactory将目标方法的所有通知转换成了环绕通知放入了一个列表。
调用链执行
我们使用代码模拟一下调用链执行:

通知:

结果:

注意:

通过创建MethodInvocation来创建调用链

因为某些通知内部需要用到调用链对象,所以我们需要把MethodInvocation放入当前线程域中供其他通知使用

而放入当前线程的活又是通过一个环绕通知来做的,这个环绕通知必须在最外层叫做ExposeInvocationInterceptor

调用链过程是一种递归调用,proceed方法调用链中的下一个环绕通知

每个环绕通知内部继续调用proceed方法

调用到没有更多通知了,就调用目标方法

接下来我们来用代码实现一下调用链MethodInvocation:

先创建两个环绕通知:

MyInvocation:

测试代码:

静态通知调用
我们前面所说的都属于静态通知调用,我们整体来回顾一下:

代理对象调用流程如下(以 JDK 动态代理实现为例)

从 ProxyFactory 获得 Target 和环绕通知链,根据他俩创建 MethodInvocation,简称 mi
首次执行 mi.proceed() 发现有下一个环绕通知,调用它的 invoke(mi)
进入环绕通知1,执行前增强,再次调用 mi.proceed() 发现有下一个环绕通知,调用它的 invoke(mi)
进入环绕通知2,执行前增强,调用 mi.proceed() 发现没有环绕通知,调用 mi.invokeJoinPoint() 执行目标方法
目标方法执行结束,将结果返回给环绕通知2,执行环绕通知2 的后增强
环绕通知2继续将结果返回给环绕通知1,执行环绕通知1 的后增强
环绕通知1返回最终的结果
图中不同颜色对应一次环绕通知或目标的调用起始至终结

Proxy
InvocationHandler
MethodInvocation
ProxyFactory
MethodInterceptor1
MethodInterceptor2
Target
invoke()
获得 Target
获得 MethodInterceptor 链
创建 mi
mi.proceed()
invoke(mi)
前增强
mi.proceed()
invoke(mi)
前增强
mi.proceed()
mi.invokeJoinPoint()
结果
后增强
结果
后增强
结果
Proxy
InvocationHandler
MethodInvocation
ProxyFactory
MethodInterceptor1
MethodInterceptor2
Target
代理方法执行时会做如下工作

通过 proxyFactory 的 getInterceptorsAndDynamicInterceptionAdvice() 将其他通知统一转换为 MethodInterceptor 环绕通知
MethodBeforeAdviceAdapter 将 @Before AspectJMethodBeforeAdvice 适配为 MethodBeforeAdviceInterceptor
AfterReturningAdviceAdapter 将 @AfterReturning AspectJAfterReturningAdvice 适配为 AfterReturningAdviceInterceptor
这体现的是适配器设计模式
所谓静态通知,体现在上面方法的 Interceptors 部分,这些通知调用时无需再次检查切点,直接调用即可
结合目标与环绕通知链,创建 MethodInvocation 对象,通过它完成整个调用
动态通知调用
动态通知调用与静态通知的区别:

静态通知调用:

不带参数绑定
执行时不需要切点
动态通知调用:

需要参数绑定
执行时需要切点

注意💡:

所谓动态通知,体现在上面方法的 DynamicInterceptionAdvice 部分,这些通知调用时因为要为通知方法绑定参数,还需再次利用切点表达式
动态通知调用复杂程度高,性能较低
————————————————
版权声明:本文为CSDN博主「十八岁讨厌编程」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zyb18507175502/article/details/131019919

目录
相关文章
|
3月前
|
监控 安全 Java
|
4月前
|
Java 编译器 Spring
|
3月前
|
中间件 Java Go
你知道什么是 AOP 吗?
你知道什么是 AOP 吗?
55 0
|
5月前
|
缓存 Java API
AOP切面编程
AOP切面编程
|
XML Java 数据格式
三、AOP(二)
三、AOP(二)
79 0
|
XML Java 数据格式
三、AOP(一)
三、AOP(一)
78 0
三、AOP(一)
|
Java
AOP详细介绍
AOP(Aspect-Oriented Programming)是一种编程范式,旨在通过将横切逻辑(cross-cutting concerns)从核心业务逻辑中分离出来,以提高代码的可维护性和可重用性。在Java中,AOP可以通过动态代理来实现。
496 0
|
监控 Java 数据安全/隐私保护
什么是AOP?
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,通过将横切关注点(Cross-cutting Concerns)从核心业务逻辑中分离出来,使得代码的组织结构更清晰,易于维护和扩展。
276 0
|
Java Spring
实现一个简单的AOP
实现一个简单的AOP
|
数据安全/隐私保护
你不是说你会Aop吗?
你不是说你会Aop吗?
124 0