0%

SpringAOP再理解

SpringAOP再理解

前言

今天在公司遇到了,MybatisPlus分页拦截器PaginationInterceptor限制limit最大为500(版本在3.4以前,3.4版本之后不存在这个问题),当时遇到这个问题第一时间就想到了通过aop代理处理limit属性的方法。然后就一发不可收拾,完全走错了路,好在最终是解决了,MybatisPlus的文档写的很清楚,不过这次经历让我对SpringAOP有了更深的认识。

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
@Setter
@Accessors(chain = true)
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class PaginationInterceptor extends AbstractSqlParserHandler implements Interceptor {
...

protected long limit = 500L;

....

@Override
public Object intercept(Invocation invocation) throws Throwable {
....
if (this.limit > 0 && this.limit <= page.getSize()) {
//处理单页条数限制
handlerLimit(page);
}

String originalSql = boundSql.getSql();
Connection connection = (Connection) invocation.getArgs()[0];
....
}

/**
* 处理超出分页条数限制,默认归为限制数
*
* @param page IPage
*/
protected void handlerLimit(IPage<?> page) {
page.setSize(this.limit);
}

....

}

实现方式

SpringAOP的主要实现方式有两种

  • JDK代理

    利用反射机制生成一个实现代理接口的类,在调用具体方法前调用InvokeHandler来处理。
    CGlib 动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

    JDK代理实现的动态代理可以参考我之前写的文章

  • CGLIB

JDK代理已经在Spring5中被抛弃,同时功能性也没有CGLIB强大(原因在下面阐述),所以接下来我们详解CGLIB代理

CGLIB代理

什么是CGLIB

代理为控制要访问的目标对象提供了一种途径。当访问对象时,它引入了一个间接的层。JDK自从1.3版本开始,就引入了动态代理,并且经常被用来动态地创建代理。JDK的动态代理用起来非常简单,但它有一个限制,就是使用动态代理的对象必须实现一个或多个接口。如果想代理没有实现接口的继承的类,该怎么办?现在我们可以使用CGLIB包

CGLIB是一个强大的高性能的代码生成包。它广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截)。最流行的OR Mapping工具hibernate也使用CGLIB来代理单端single-ended(多对一和一对一)关联(对集合的延迟抓取,是采用其他机制实现的)。EasyMock和jMock是通过使用模仿(mock)对象来测试java代码的包。它们都通过使用CGLIB来为那些没有接口的类创建模仿(mock)对象。

CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。除了CGLIB包,脚本语言例如Groovy和BeanShell,也是使用ASM来生成java的字节码。当然不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。

简而言之就是,CGLIB代理较于JDK代理,通过生成代理类继承被代理类的方式,不受被代理类接口的限制,更加自由。完美解决了JDK代理的致命短板

简单实验

首先我们创建SpringBoot项目,只需引入AOP依赖即可

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

为了证明Spring5已经抛弃JDK代理,这边我实现的代理类继承接口

IClient

1
2
3
public interface IClient {
String codeing();
}

ClientImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class ClientImpl implements IClient {

@Override
public String codeing() {
System.out.println("继承接口==============>干活中");
testNameTest();
proNameTest();
return "Impl";
}

public void testNameTest() {
System.out.println("TESTNAMETEST");
}

protected void proNameTest() {
System.out.println("proNameTest");
}
}

定义切面和环绕方法

AOP的基础操作这里就不讲了,推荐使用环绕通知方法,比其他通知方法强大许多,可以获取修改参数,决定方法是否只需,修改返回值全部在一个通知内完成。

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

@Around("execution(* com.example.deom.aop.ClientImpl.codeing(..))")
public Object aopPro(ProceedingJoinPoint pjp) {
System.out.println("代理pro方法");
Object proceed;
try {
proceed = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
proceed = "修改返回值";
return proceed;
}
}

调用被代理类Client的codeing方法,并设置断点

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootApplication
public class DeomApplication {

public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(DeomApplication.class, args);
Client client = run.getBean(Client.class);
//直接将原始Client的bean替换为生成的代理类
//执行codeing方法时是执行的代理类的codeing方法
client.codeing();
}

}

结果:

可以看到就算被代理类实现了接口还是使用的CGLIB代理方式

image-20211118235615645

image-20211118235623657

如何使用JDK代理

首先明确在Spring中是不推荐使用JDK代理的,原因

设置JDK代理,在启动类上加上注解

值得注意的是proxyTargetClass默认值为false,即默认使用JDK代理

@EnableAspectJAutoProxy(proxyTargetClass = false)

那我们再次尝试断点,查看aop代理方式

image-20211118235548341

很奇怪,明明在注解中配置了使用JDK代理,但实际却还是使用CGLIB代理,这是为什么

很明显,既然注解无效,那就去找它的自动配置类,通过分析SpringBoot自动配置,找到了AOP的自动配置类

org.springframework.boot.autoconfigure.aop.AopAutoConfiguration

image-20211118233000006

可以看到在配置类中,如果没有配置属性spring.aop.proxy-target-class,那么就会加载CglibAutoProxyConfiguration

image-20211118234243083

关于注解@ConditionalOnProperty的解释如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {

// 数组,获取对应property名称的值,与name不可同时使用
String[] value() default {};

// 配置属性名称的前缀,比如spring.http.encoding
String prefix() default "";

// 数组,配置属性完整名称或部分名称
// 可与prefix组合使用,组成完整的配置属性名称,与value不可同时使用
String[] name() default {};

// 可与name组合使用,比较获取到的属性值与havingValue给定的值是否相同,相同才加载配置
String havingValue() default "";

// 缺少该配置属性时是否可以加载。如果为true,没有该配置属性时也会正常加载;反之则不会生效
boolean matchIfMissing() default false;

}

综上所述,我们如果需要使用JDK代理模式需要在配置文件中配置参数

1
spring.aop.proxy-target-class=false

再再次打断点看一下

image-20211118235346527

为什么不推荐使用JDK代理

首先我们继续保持上面的JDK代理

我们来更改一下被代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class Client {

public String codeing() {
System.out.println("未继承接口========>工作中!!!");
pro();
return "null";
}

protected String pro() {
System.out.println("protected类型");
return "protected";
}
}

切面

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

@Around("execution(* com.example.deom.aop.Client.codeing(..))")
public Object around(ProceedingJoinPoint pjp) {
System.out.println("环绕方法前");
Object proceed;
try {
proceed = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
proceed = "修改返回值";
return proceed;
}
}

调用Client类的codeing方法

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootApplication
public class DeomApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(DeomApplication.class, args);
IClient clientImpl = run.getBean(IClient.class);
Client client = run.getBean(Client.class);
//直接将原始Client的bean替换为生成的代理类
//执行codeing方法时是执行的代理类的codeing方法
clientImpl.codeing();
client.codeing();
}
}

可以看到,代理模式又被换到了CGLIB代理,为什么SpringBoot一定要求使用CGLIB代理呢,上面的被代理类和之前的有什么区别呢

image-20211119000437089

答:

我们都知道JDK代理有这一个致命的显示,代理类必须继承Proxy类(至于为什么可以参考文章,感觉大部分网上的说话都是自圆其说,总结下来为了区分代理类,同时避免每个代理类都包含一些重复的特征),那么代理类想要知道被代理类有哪些方法并对其进行增强,只能通过实现被代理类实现的接口。

以下是文章提到的被代理类源码分析

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
66
67
68
69
70
71
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class ClientImplProxy extends Proxy implements InvocationHandler {
//每一个方法
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;


public ClientImplProxy(InvocationHandler var1) throws {
super(var1);
}

public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}


public final Object invoke(Object var1, Method var2, Object[] var3) throws Throwable {
//调用其逻辑控制器的invoke方法
return (Object)super.h.invoke(this, m3, new Object[]{var1, var2, var3});
}

public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("java.lang.reflect.InvocationHandler").getMethod("invoke", Class.forName("java.lang.Object"), Class.forName("java.lang.reflect.Method"), Class.forName("[Ljava.lang.Object;"));
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

那么这就迎来了一个问题,在MVC框架中,我们经常会使用以下方式书写服务实现类

1
2
3
4
5
6
class Iservice{}	

class ServiceImpl implements Iservice{}

@AutoWired
Iservice service;

使用服务实现类实现接口的方式,这种情况下使用JDK代理当然没问题,但是如果你一不小心写了以下形式的@AutoWired

并且使用JDK代理,那么下面的代码是不是就会报错呢。

JDK代理所生成的代理类只能赋值给被代理类实现的接口类型的变量,故下方代码会报错。

而使用CGLIB代理就不会产生这种错误,CGLIB的代理类是通过继承被代理类的方式实现的,无论是赋值给被代理类还是其实现的接口,都不会有问题。

1
2
3
class Service{}
@AutoWired
Service service;

aop实验结论

在SpringFramework5中 aop的代理方式存在两个jdk代理/cglib代理

  •  在Spring项目中,aop的代理方式会根据被代理类是否继承接口来选择jdk代理还是cglib代理
    
  •  在SpringBoot项目中,aop的代理方式默认cglib代理,关于SpringBoot项目中为什么默认使用cglib代理可以参考[这篇文章](https://www.cnblogs.com/coderxiaohei/p/11758239.html)
    

为什么jdk代理只能代理接口实现类

在Proxy类中会生成代理类,代理类默认继承Proxy类,然后通过实现被代理类实现的接口,

  •  得知被代理类的方法,然后通过invoke方法增强被代理类方法
    
  •  综上所诉,我们知道jdk代理生成的代理类默认继承Proxy类同时实现被代理类实现的接口,
    
  •  由于Java的单继承机制,所以无法代理未实现接口的被代理类(无法得知被代理类有哪些方法)
    

SpringAOP代理机制

在SpringAOP代理机制中无法实现代理被保护(provide/protected)的方法

  •      对于jdk代理-通过被代理类实现的接口得到被代理类的方法,接口中无法定义被保护(provide/protected)方法
    
  •      对于cglib代理-cglib代理的原理是继承被代理类,但是使用代理类是通过**对象.方法**的形式调用,那么对应的由于被保护方法只能通过类内部调用,无法通过**对象实例.被保护方法**调用。
    

文章作者:xpp011

发布时间:2021年11月18日 - 21:11

原始链接:http://xpp011.cn/2021/11/18/6500dde1.html

许可协议: 转载请保留原文链接及作者。