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]; .... }
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.codeing(); }
}
|
结果:
可以看到就算被代理类实现了接口还是使用的CGLIB代理方式
如何使用JDK代理
首先明确在Spring中是不推荐使用JDK代理的,原因
设置JDK代理,在启动类上加上注解
值得注意的是proxyTargetClass
默认值为false
,即默认使用JDK代理
@EnableAspectJAutoProxy(proxyTargetClass = false)
那我们再次尝试断点,查看aop代理方式
很奇怪,明明在注解中配置了使用JDK代理,但实际却还是使用CGLIB代理,这是为什么
很明显,既然注解无效,那就去找它的自动配置类,通过分析SpringBoot自动配置,找到了AOP的自动配置类
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
可以看到在配置类中,如果没有配置属性spring.aop.proxy-target-class
,那么就会加载CglibAutoProxyConfiguration
关于注解@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 {
String[] value() default {};
String prefix() default "";
String[] name() default {};
String havingValue() default "";
boolean matchIfMissing() default false;
}
|
综上所述,我们如果需要使用JDK代理模式需要在配置文件中配置参数
1
| spring.aop.proxy-target-class=false
|
再再次打断点看一下
首先我们继续保持上面的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); clientImpl.codeing(); client.codeing(); } }
|
可以看到,代理模式又被换到了CGLIB代理,为什么SpringBoot一定要求使用CGLIB代理呢,上面的被代理类和之前的有什么区别呢
答:
我们都知道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
|
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 { 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代理
为什么jdk代理只能代理接口实现类
在Proxy类中会生成代理类,代理类默认继承Proxy类,然后通过实现被代理类实现的接口,
-
得知被代理类的方法,然后通过invoke方法增强被代理类方法
-
综上所诉,我们知道jdk代理生成的代理类默认继承Proxy类同时实现被代理类实现的接口,
-
由于Java的单继承机制,所以无法代理未实现接口的被代理类(无法得知被代理类有哪些方法)
SpringAOP代理机制
在SpringAOP代理机制中无法实现代理被保护(provide/protected)的方法