Java动态代理和Spring AOP原理浅析
一直对Spring的AOP功能敬而远之,总是用,却从没想过去了解它。想学习下JDK的动态代理,顺便的了解了下AOP,发现其实不算很难,但是设计的是真的妙啊。
References:
- https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html Oracle的Java动态代理官方文档
- http://www.importnew.com/23168.html JDK动态代理详解
- https://www.jianshu.com/p/1712ef4f2717
- https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-proxying Spring代理机制官方文档
- http://cliffmeyers.com/blog/2006/12/29/spring-aop-cglib-or-jdk-dynamic-proxies.html JDK动态代理和CGLIB区别
- https://stackoverflow.com/questions/10664182/what-is-the-difference-between-jdk-dynamic-proxy-and-cglib
- https://juejin.im/entry/5a40abb16fb9a0451e400886 Aspect介绍
- https://www.baeldung.com/spring-aop-vs-aspectj Spring AOP和Aspect对比
- https://segmentfault.com/a/1190000011291179
- 《深入浅出Spring Boot 2.x》
- 《Spring技术内幕(第二版)》
文章篇幅可能有点长。
首先从代理模式说起吧,先来简单复习一下代理模式:
代理模式是一种设计模式,提供了对目标对象额外的访问方式,代理类和目标对象实现相同的接口,然后通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。简言之,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能和简化访问方式。
动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中。目标对象必须实现接口,否则不能使用动态代理。下面看一下例子:
首先是业务的接口(Person)和实现类(Child):
public interface Person {
void play(String thing);
}
public class Child implements Person {
@Override
public void play(String thing) {
if (thing == null || "".equals(thing)) {
throw new IllegalArgumentException();
}
System.out.println("child play: " + thing + ". feel happy.");
}
}
然后实现InvocationHandler接口:
public class ProxyHandler implements InvocationHandler {
private Object target;
public ProxyHandler(Object target) {
this.target = target;
}
public static Object newInstance(Object obj) {
return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new ProxyHandler(obj));
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before play, first wash hands.");
// 对目标对象的调用
Object result = method.invoke(this.target, args);
System.out.println("after play, need to sleep.");
return result;
}
}
这里为了方便使用,直接把获取代理的方法也放在Hanlder的实现类中。然后直接调用就可以看到效果了:
public class TestProxy {
public static void main(String[] args) {
Person proxy = (Person) ProxyHandler.newInstance(new Child());
proxy.play("video games");
}
}
// Output:
before play, first wash hands.
child play: video games. feel happy.
after play, need to sleep.
在main方法中,我们实际调用的是child的play()方法,而before和after是增强功能。那么是什么时候帮我们做了增强的操作呢?
之所以天天叫JDK动态代理,是因为这个代理class是由JDK在运行时动态帮我们生成。在解释代理生成过程前,我们先把-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
这个参数加入到JVM 启动参数中,它的作用是帮我们把JDK动态生成的proxy class 的字节码保存到硬盘中,帮助我们查看具体生成proxy的内容。
Eclipse中操作如下,在Run Configuration中添加VM参数:
运行后在工程目录下生成如下的文件$Proxy0.class
,就是运行时生成的代理类:
反编译之后代码如下:
package com.sun.proxy;
import java.lang.reflect.*;
public final class $Proxy0 extends Proxy implements Person
{
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy0(final InvocationHandler invocationHandler) {
super(invocationHandler);
}
public final boolean equals(final Object o) {
try {
return (boolean)super.h.invoke(this, $Proxy0.m1, new Object[] { o });
}
catch (Error | RuntimeException error) {
throw;
}
catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
}
public final String toString() {
try {
return (String)super.h.invoke(this, $Proxy0.m2, null);
}
catch (Error | RuntimeException error) {
throw;
}
catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
}
public final void play(final String s) {
try {
super.h.invoke(this, $Proxy0.m3, new Object[] { s });
}
catch (Error | RuntimeException error) {
throw;
}
catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
}
public final int hashCode() {
try {
return (int)super.h.invoke(this, $Proxy0.m0, null);
}
catch (Error | RuntimeException error) {
throw;
}
catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
}
static {
try {
$Proxy0.m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
$Proxy0.m2 = Class.forName("java.lang.Object").getMethod("toString", (Class<?>[])new Class[0]);
$Proxy0.m3 = Class.forName("Person").getMethod("play", Class.forName("java.lang.String"));
$Proxy0.m0 = Class.forName("java.lang.Object").getMethod("hashCode", (Class<?>[])new Class[0]);
}
catch (NoSuchMethodException ex) {
throw new NoSuchMethodError(ex.getMessage());
}
catch (ClassNotFoundException ex2) {
throw new NoClassDefFoundError(ex2.getMessage());
}
}
}
这个代理类继承自Proxy,并且实现了我们的Person接口。通过Proxy.newProxyInstance()方法将给定的handler设置给这个代理类。重点就是其中的play()方法,play()调用了handler的invoke()方法,这样就完成了我们需要的增强功能。然后handler中的invoke()方法在增强后重新调用目标对象的方法。这样整个代理过程就算结束了。关于生成动态代理类这部分更详细可以参考上面链接中的“JDK动态代理详解”一文。
对于JDK的动态代理就到此,下面看看Spring的代理机制,和JDK动态代理有什么关系?
Spring官网的代理图解:
Spring AOP使用JDK创建动态代理或CGLIB给目标对象创建代理对象。
- 如果被代理的目标对象实现了至少一个接口,那么Spring就会使用JDK动态代理,目标对象实现接口的方法都会被代理,需要注意的是JDK动态代理类无法强制转换为原始目标类,因为它只是一个简单的动态代理,恰好和目标对象实现了相同的接口。这就需要面向接口编程,因为代理通常会通过这些接口调用。
- 如果目标对象没有实现接口,那么就会用CGLIB创建代理,CGLIB会通过继承的方式创建目标对象的子类作为代理,并重写所有的方法(不只是实现接口的方法),需要注意的是final类和方法不能被代理,因为final类不能被继承,final方法不能被重写。
其实不管是动态代理还是CGLIB,都是创建了一个代理对象,然后通过代理增强目标的功能。概念差不多了,下面通过一个简单的例子模拟一下Spring的AOP。
实例的结构如下,我保留的原始的类名并稍微改变了原本的功能,使其更容易体现出动态代理的结构。
目标接口和目标类依然是上面的Person和Child,就不重复了,看下AopProxy和JdkDynamicAopProxy:
public interface AopProxy {
Object getProxy();
}
JdkDynamicAopProxy实现了InvocationHandler接口并提供了Proxy.newProxyInstance(...)的封装方法。
public class JdkDynamicAopProxy implements AopProxy, InvocationHandler {
private final Object target;
private final List<Interceptor> advised;
public JdkDynamicAopProxy(Object target, List<Interceptor> advised) {
this.target = target;
this.advised = advised;
}
@Override
public Object getProxy() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (advised == null) {
return method.invoke(target, args);
} else {
ReflectiveMethodInvocation invocation = new ReflectiveMethodInvocation(target, method, args, advised);
return invocation.proceed();
}
}
}
可以看到,其核心的invoke方法包含了增强处理,处理包含在Interceptor中,这是个List,表明可以有多个且是链式调用的方式,虽然我这里没有体现出来,但其实是个责任链模式。如果advised不为空,就会构造一个ReflectiveMethodInvocation实例并调用其proceed()方法。
public class ReflectiveMethodInvocation implements MethodInvocation {
private final Object target;
private final Method method;
private final Object[] args;
private final List<Interceptor> advised;
private int currentInterceptorIndex = -1;
public ReflectiveMethodInvocation(Object target, Method method, Object[] args, List<Interceptor> advised) {
this.target = target;
this.advised = advised;
this.method = method;
this.args = args;
}
@Override
public Object proceed() throws Throwable {
if (currentInterceptorIndex < advised.size() - 1) {
Interceptor interceptor = advised.get(++currentInterceptorIndex);
return interceptor.invoke(this);
} else {
return method.invoke(target, args);
}
}
}
ReflectiveMethodInvocation包含了反射和增强通知(advised)的全部参数,所以proceed方法调用了增强通知的invoke方法,并将当前的参数继续传递过去,方便增强通知处理完之后回调proceed方法,继续将责任链进行下去。
public class MethodBeforeAdviceInterceptor implements Interceptor {
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
System.out.println("do sth. before...");
return mi.proceed();
}
}
public class MethodReturningAdviceInterceptor implements Interceptor {
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
Object retVal = mi.proceed();
System.out.println("do sth. after returning...");
return retVal;
}
}
public class MethodThrowingAdviceInterceptor implements Interceptor {
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
} catch (Throwable ex) {
System.out.println("do sth. after throwing...");
throw ex;
}
}
}
以上三个常见的通知拦截器简单表示了增强通知是怎么处理的。可以看到,处理完自己的增强事情后,都回调了MethodInvocation的proceed方法,这个例子中就是ReflectiveMethodInvocation的proceed,然后ReflectiveMethodInvocation继续责任链的下一个增强通知处理,依次处理直到责任链结束。
public class App {
public static void main(String[] args) {
List<Interceptor> interceptorList = new ArrayList<>();
interceptorList.add(new MethodBeforeAdviceInterceptor());
interceptorList.add(new MethodReturningAdviceInterceptor());
interceptorList.add(new MethodThrowingAdviceInterceptor());
AopProxy aopProxy = new JdkDynamicAopProxy(new Child(), interceptorList);
Person childProxy = (Person) aopProxy.getProxy();
childProxy.play("video game"); // Output1
// childProxy.play(null); // Output2
}
}
// Output1:
do sth. before...
child play: video game. feel happy.
do sth. after returning...
// Output2:
do sth. before...
do sth. after throwing...
Exception in thread "main" ...
Caused by: java.lang.IllegalArgumentException
at spring.aop.simulation.service.Child.play(Child.java:8)
... 14 more
示例代码点击下载
main方法中构造了一个通知List模拟增强通知链,例子看起来简单,但实际跑一遍debug走走才能体会到含义。至此就能理解Spring是怎样通过JDK动态代理搞出AOP这么个东西了,并没有什么高深的技术,但这其中的思想真的是令人受益匪浅。至于CGLIB,和动态代理差不多,只不过生成代理类的方式和条件不一样而且,但在AOP方面和动态代理就大同小异了。
最后补充一些关于AspectJ的概念:
一提起AspectJ,其实我感觉绝大多数人都会联想到Spring。毕竟,大多数人都是通过spring才接触到了AspectJ。但AspectJ其实是eclipse基金会的一个项目,官网就在eclipse官网里。
我们知道面向切面编程(Aspect Oriented Programming)有诸多好处,但是在使用AspectJ之前我们一般是怎么编写切面的呢?我想一般 来说应该是三种吧:静态代理,jdk动态代理,cglib。
- 静态代理的重用性太差,一个代理不能同时代理多种类;
- 动态代理可以做到代理的重用,但调用起来还是比较麻烦,除了写切面代码以外,还需要将代理类耦合进被代理类的调用阶段,在创建被代理类的时候都要先创建代理类,再用代理类去创建被代理类,
这就稍微有点麻烦了。比如我们想在现有的某个项目里统一新加入一些切面,这时候就需要创建切面并且侵入原有代码,在创建对象的时候添加代理,还是挺麻烦的。
说到底,这种麻烦出现的本质原因是,代理模式并没有做到切面与业务代码的解耦。虽然将切面的逻辑独立进了代理类,但是决定是否使用切面的权利仍然在业务代码中。这才导致了上面这种问题。(当然,话不能说的这么绝对,如果有那种类似Spring的IoC容器,将类的创建都统一托管起来,我们只需要将切面用配置文件进行注册,容器会根据注册信息在创建bean的时候自动加上代理,这也是比较方便的。不过并不是所有框架都提供IoC机制的吧。。。)既然代理模式这么麻烦,那么AspectJ又是通过什么方式来避免这个麻烦的呢?
我总结AspectJ提供了两套强大的机制:
- 第一套是切面语法。就是网上到处都是的那种所谓”AspectJ使用方法”,这套东西做到了将决定是否使用切面的权利还给了切面。在写切面的时候就可以决定哪些类的哪些方法会被代理,从而从逻辑上不需要侵入业务代码。由于这套语法实在是太有名,导致很多人都误以为AspectJ等于切面语法,其实不然。
- 第二套是织入工具。刚才讲到切面语法能够让切面从逻辑上与业务代码解耦,但是从操作上来讲,当JVM运行业务代码的时候,他甚至无从得知旁边还有个类想横插一刀。。。这个问题大概有两种解决思路,一种就是提供注册机制,通过额外的配置文件指明哪些类受到切面的影响,不过这还是需要干涉对象创建的过程;另外一种解决思路就是在编译期(或者类加载期)我们优先考虑一下切面代码,并将切面代码通过某种形式插入到业务代码中,这样业务代码不就知道自己被“切”了么?这种思路的一个实现就是aspectjweaver,就是这里的织入工具。
Spring AOP旨在提供跨Spring IoC的简单AOP实现,以解决程序员面临的最常见问题。 它不是一个完整的AOP解决方案 - 它只能应用于由Spring容器管理的bean。
另一方面,AspectJ是最初的AOP技术,旨在提供完整的AOP解决方案。 它比Spring AOP更强大但也更复杂。 值得注意的是,AspectJ可以应用于所有域对象。
我们知道Spring里有很多基于动态代理的设计,而我们知道动态代理也可以被用作面向切面的编程,但是Spring AOP本身却支持AspectJ的切面语法,而且spring-aop这个包也引用了AspectJ,我们知道AspectJ是通过织入的方式来实现AOP的。那么Spring AOP究竟是通过织入还是代理来实现aop的呢?
其实spring aop还是通过动态代理来实现aop的,即使不去看他的源码,我们也可以通过简单的实验来得到这个结论。
根据aspectj的使用方式,我们知道,如果要向代码中织入切面,那么我们要么采用ajc编译,要么使用aspectjweaver的agent代理。但是spring既没有依赖任何aspectjtools的相关jar包,虽然依赖了aspectjweaver这个包,但是并没有添加agent代理。当然,也存在一种可能就是spring利用aspectjweaver这个包自己实现了动态织入,但是从可复用的角度讲,spring真的会自己重新造轮子?如果真的重新造了那为啥不脱离aspectj彻底重新造,而是用一半造一半呢?
而且,我们知道用织入和用动态代理有一个很大的区别,如果使用织入的话,那么调业务对象的getClass()方法获得的类名就是这个类本身实现的类名;但是如果使用动态代理的话,调用getClass()方法获得的类名就是动态代理类的类名了。做一个简单的实验我们就可以发现,如果我们使用spring aop来对某一个service进行切面处理,那么调用getClass()方法获得的结果就是:
com.mythsman.test.Myservice$$EnhancerBySpringCGLIB$$3afc9148
显然,虽然spring aop采用了aspectj语法来定义切面,但是在实现切面逻辑的时候还是采用CGLIB生成代理的方法。
以上介绍了Aspect是为了更好的了解Spring AOP的使用,希望能有点用。
这一段看起来可能有些难受,因为我只是截取了引用链接文章中一部分,具体可以常看参考链接中的文章。
提前祝5.1快乐~~!