前言
什么是代理,在Design patterns In java
这个本书中是这样描述的,简单的说就是为某个对象提供一个代理,以控制对这个对象的访问。在不修改源代码的基础上做方法增强,代理是一种设计模式,又简单的分为两种。
- 静态代理:代理类和委托类在代码运行前关系就确定了,也就是说在代理类的代码一开始就已经存在了。
- 动态代理:动态代理类的字节码在程序运行时的时候生成。
静态代理
先来看一个静态代理的例子,Calculator是一个计算器的接口类,定义了一个加法的接口方法,由CalculatorImpl类实现真正的加法操作.现在如果我们想对这个方法做一层静态的代理,这儿实现了一个简单的代理类实现了计算接口Calculator,构造函数传入的参数是真正的实现类,但是在调用这个代理类的add方法的时候我们在CalculatorImpl的实现方法执行的前后分别做了一些操作。这样的代理方式就叫做静态代理(可以理解成一个简单的装饰模式)。
很明显静态代理的缺点,由于我们需要事先实现代理类,那么每个方法我都都需要去实现。如果我们要实现很多的代理类,那么工作量就太大了。动态代理的产生就是这样而来的。
1 | public interface Calculator { |
动态代理
使用动态代理可以让代理类在程序运行的时候生成代理类,我们只需要为一类代理写一个具体的实现类就行了,所以实现动态代理要比静态代理简单许多,省了不少重复的工作。在JDK的方案中我们只需要这样做可以实现动态代理了。
1 | public class ProxyFactory implements InvocationHandler { |
利用JDK的proxy实现代理动态代理,有几个关键点,一个就是InvocationHandler接口,这个方法中的invoke方法是执行代理时会执行的方法。所以我们所有代理需要执行的逻辑都会写在这里面,invo参数里面的method可以使用java 反射调用真实的实现类的方法,我们在这个方法周围做一些代理逻辑工作就可以了。上面的代码会把Calculator接口的所有方法全部在程序运行时代理。不用我们一个个的去写静态代理的方法。
JDK动态代理的原理
先看Proxy.newProxyInstance(...)
方法中的具体实现(省略大部分方法)。在下面的代码中会通过getProxyClass0(…)方法得到class对象,然后给把InvocationHandler已构造参数实例化代理对象。思路还是挺清晰的,但是如果要一探究竟我们还是得知道代理对象到底是什么样的,如何实现的代理呢?
1 | public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException |
用ProxyGenerator.generateProxyClass(..)
方法生成字节流,然后写进硬盘.假设我把proxyName定义为Calcultor$ProxyCode
.我们先在https://bitbucket.org/mstrobel/procyon/downloads 下载一个反编译的jar包。然后运行下面的代码,我们得到了一个Calcultor$ProxyCode.class
的文件.然后在目录下使用命令java -jar procyon-decompiler-0.5.29.jar Calcultor$ProxyCode.class
就能得到Calcultor$ProxyCode.java
文件。 当然也可以实现在线反编译http://javare.cn/网站反编译然后下载文件
1 | public static void main(String[] args) { |
下面的代码是就是反编译过来的Calcultor$ProxyCode
类。可以发现这个类实现了我们需要代理的接口Calculator
。且他的构造函数确实是需要传递一个InvocationHandler
对象,那么现在的情况就是我们的生成了一个代理类,这个代理类是我们需要代理的接口的实现类。我们的接口中定义了add和reduce方法,在这个代理类中帮我们实现了,并且全部变成了final的。同时覆盖了一些Object类中的方法。那我们现在以reduce这个方法举例,方法中会调用InvocationHandler
类中的invoke方法(也就是我们实现的逻辑的地方)。同时把自己的Method
对象,参数列表等传入进去。
1 | public final class Calcultor$ProxyCode extends Proxy implements Calculator { |
JDK动态代理小结
现在我们对JDK代理有个简单的源码级别的认识,理清楚一下思路:JDK会帮我们在运行时生成一个代理类,这个代理类实际上就是我们需要代理的接口的实现类。实现的方法里面会调用InvocationHandler
类中的invoke方法,并且同时传入自身被调用的方法的的Method对象和参数列表方便我们编码实现方法的调用。比如我们调用reduce方法,那么我们就可以通过Method.Invoke(Object obj, Object... args)
调用我们具体的实现类,再在周围做一些代理做的事儿。就实现了动态代理。我们对JDK的特性做一些简单的认识:
- JDK动态代理只能代理有接口的类,并且是能代理接口方法,不能代理一般的类中的方法
- 提供了一个使用InvocationHandler作为参数的构造方法。在代理类中做一层包装,业务逻辑在invoke方法中实现
- 重写了Object类的
equals、hashCode、toString
,它们都只是简单的调用了InvocationHandler的invoke方法,即可以对其进行特殊的操作,也就是说JDK的动态代理还可以代理上述三个方法 - 在invoke方法中我们甚至可以不用
Method.invoke
方法调用实现类就返回。这种方式常常用在RPC
框架中,在invoke方法中发起通信调用远端的接口等
CGLIB代理
JDK中提供的生成动态代理类的机制有个鲜明的特点是:某个类必须有实现的接口,而生成的代理类也只能代理某个类接口定义的方法。那么如果一个类没有实现接口怎么办呢?这就有CGLIB
的诞生了,前面说的JDK的代理类的实现方式是实现相关的接口成为接口的实现类,那么我们自然而然的可以想到用继承的方式实现相关的代理类。CGLIB就是这样做的。一个简单的CGLIB代理是这样实现的:
1 | Enhancer enhancer=new Enhancer(); |
CGLIB代理原理分析
通过在执行动态代理的代码前面加上一行代码就可以得到生成的代理对象.代理对象的class文件会生成在你定义的路径下。类似Calculator$CalculatorImpl$$EnhancerByCGLIB$$58419779.class
这样结构。
1 | System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "${your path}"); |
之后我们通过反编译得到反编译后的java文件。就上面的例子而言我们传入的superclass是一个接口,并不是实现类。那我们得到的代理类会长成这样:
1 | //如果是接口代理类还是通过实现接口的方式 |
如果传入的并不是接口,而是实现类的话,就会得到下面的代理类:
1 | //如果是普通的类,会采用继承的方式实现 |
但是不管是传入的接口还是传入的代理类,代码的实体都是长得差不多的:
1 | ublic class Calculator$CalculatorImpl$$EnhancerByCGLIB$$2849428a extends CalculatorImpl implements Factory { |
上面这一份代码整个代理的流程仿佛是差不多的,都是在调用方法的时候router到InvokeHandler或者MethodInterceptor。为什么会有两种呢,因为CGLIB提供了filter的机制,可以让不同的方法代理到不同的callback中,如下面这样:
1 | enhancer.setCallbacks(new Callback[]{new MethodInterceptor() { |
这两种callback不一样的地方很显而易见, MethodInterceptor的方法参数多了一个MethodProxy对象,在使用这个对象的时候的时候有两个方法可以让我们调用:
1 | public Object invoke(Object obj, Object[] args) throws Throwable { |
FastClass是Cglib实现的一种通过给方法建立下标索引来访问方法的策略,为了绕开反射。
上面的描述代表MethodPeoxy可以根据对方法建立索引调用方法,而不需要使用传统Method的invoke反射调用,提高了性能,当然额外的得多生成一些类信息,比如在最开始的代理类中我们也可以看到MethodProxy也是有通过索引来做的,这样的话做到了FastClass,FastClass大致是这样实现的:
1 | class FastTest { |
所以在使用MethodInterceptor的时候可以这样使用:
1 | public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { |
编写好ASpectj文件之后,编译代码就能够得到静态织入的class文件了,接下来简单的介绍一下AspectJ是在哪个地方植入代码到class文件的.
AspectJ原理分析
反编译过后得到的java代码如下:
1 | @RequestMapping({"/hello"}) |
上面两个方法都实现了@ RequestMapping注解,类也实现类Mtrace接口。但是因为传入参数的类型不同,所以只有第一个方法被织入了代理的方法,在真正的方法快周围分表调用了before
、after
、afterThrowing
、afterRutnrn
等方法。Aspectj简单的原理就是这样.更加深入的原理解析暂时就不做了。
小结
- Aspectj并不是动态的在运行时生成代理类,而是在编译的时候就植入代码到class文件
- 由于是静态织入的,所以性能相对来说比较好
- Aspectj不受类的特殊限制,不管方法是
private
、或者static
、或者final
的,都可以代理 - Aspectj不会代理除了限定方法之外任何其他诸如
toString()
,clone()
等方法
Spring Aop中的代理
Spring代理实际上是对JDK代理和CGLIB代理做了一层封装,并且引入了AOP概念:Aspect、advice、joinpoint等等,同时引入了AspectJ中的一些注解@pointCut
,@after
,@before
等等.Spring Aop严格的来说都是动态代理,所以实际上Spring代理和Aspectj的关系并不大.
Spring代理中org.springframework.aop.framework.ProxyFactory
是关键,一个简单的使用API编程的Spring AOP代理如下:
1 | ProxyFactory proxyFactory =new ProxyFactory(Calculator.class, new MethodInterceptor() { |
在调用getProxy()
时,会优先得到一个默认的DefaultAopProxyFactory
.这个类主要是决定到底是使用JDK代理还是CGLIB代理:
1 |
|
那么JdkDynamicAopProxy中的invoke方法就是最核心的方法了(实现了InvokeHandler接口):
1 |
|
下面来分析整个代理的拦截器是怎么运行的,ReflectiveMethodInvocation
这个类的proceed()
方法负责递归调用所有的拦截的织入。
1 | public Object proceed() throws Throwable { |
那么要实现织入,只需要控制织入的代码和调用proceed
方法的位置,在Spring中的before
织入是这样实现的:
1 | public class MethodBeforeAdviceInterceptor implements MethodInterceptor, Serializable { |
afterRuturning
是这样实现的:
1 | public class AfterReturningAdviceInterceptor implements MethodInterceptor, AfterAdvice, Serializable { |
下面这幅流程图是一个一个包含上述一个before织入和一个afterReturning织入的流程图:
要实现这种环绕的模式其实很简单,下面提供一个最简单的实现,利用迭代的思想很简单的实现了链式调用。并且可扩展性非常高。和AspectJ的直接静态织入改变代码结构的方式来分别织入before、after等来说。这种方式设计更优雅。但是在SpringMVC
中拦截器却并不是这种方式实现的,哈哈。
1 | public interface MethodInterceptor { |
1 | public interface Invocation { |
小结
Spring AOP封装了JDK和CGLIB的动态代理实现,同时引入了AspectJ的编程方式和注解。使得可以使用标准的AOP编程规范来编写代码外,还提供了多种代理方式选择。可以根据需求来选择最合适的代理模式。同时Spring也提供了XML配置的方式实现AOP配置。可以说是把所有想要的都做出来了,Spring是在平时编程中使用动态代理的不二选择.