1.代理模式介绍
什么是代理模式?就是用一个新的对象来伪装原来的对象,从而实现一些“不可告人”的动作。
什么情况下会使用代理模式?简单来说,就是不能或者不想直接引用一个对象。什么是不能?比如我在内网中想访问外网的资源,但是因为网关的控制,访问不了。那什么是不想呢?比如我在网页上要显示一张图片,但是图片太大了,会拉慢页面的加载速度,我想用一张小一点的图片代替。
来看一张类结构图:
"- Subject:原对象的抽象
- RealSubject:原对象的实现
- Proxy: 代理对象
通过代理模式,客户端访问时同原来一样,但访问的前后已经做了额外的操作(可能你的信息和数据就被窃取了)。
好了,来看一个正常点的例子。做IT的一般都需要翻墙,比如去YouTube上看点MV啥的(说好的正常呢),但是正常访问肯定是要被屏蔽的,所以就要通过一些工具去穿过重重防守的GTW。一般的方式就是本地的工具将你的访问信息加密后,交给一个未被屏蔽的国外的服务器,然后服务器解密这些访问信息,去请求原始的访问地址,再将请求得到的资源和信息回传给你自己的本地。我们以浏览器来举例。
浏览器接口:
1 | public interface Browser { |
Chrome的实现类:
1 | public class ChromeBrowser implements Browser{ |
如果直接访问肯定是要挂掉的,我们通过解密和加密的两个方法简单模拟翻墙的过程。
1 | public class ChromeBrowser implements Browser{ |
虽然这样就可以访问成功了,但直接将加密和解密的方式写死在原对象里,不仅侵入了原有的代码结构,而且会显得很LOW。那怎么办?代理模式啊。
2.静态代理
根据上面的代理模式的类图,最简单的方式就是写一个静态代理,为ChromeBrowser写一个代理类。
1 | public class ChromeBrowserProxy implements Browser{ |
ChromeBrowserProxy同样实现Browser接口,客户端访问时不再直接访问ChromeBrowser,而是通过它的代理类。
1 | public class StaticProxyTest { |
这种方式解决了对原对象的代码侵入,但是出现了另一个问题。如果我有好几个浏览器,难道每个浏览器的实现类都要写一个代理类吗?太LOW太LOW。我们需要更牛B的方式:JDK动态代理。
3.JDK动态代理
在JDK中提供了一种代理的实现方式,可以动态地创建代理类,就是java.lang.reflect包中的Proxy类提供的newProxyInstance方法。
1 | Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) |
- classLoader是创建代理类的类加载器
- interfaces是原对象实现的接口
- InvocationHandler是回调方法的接口
真正的代理过程通过InvocationHandler接口中的invoke方法来实现
1 | public Object invoke(Object proxy, Method method, Object[] args) |
- proxy是代理对象
- method是执行的方法
- args是执行方法的参数数组
还是Chrome浏览器举例:
1 | public class JdkBrowserProxy implements InvocationHandler{ |
JdkBrowserProxy实现InvocationHandler接口,并通过构造方法传入被代理的对象,然后在invoke方法中实现代理的过程。
来看测试方法
1 | public class JdkDynamicProxyTest { |
JDK的动态代理基本能够解决大部分的需求,唯一的缺点就是它只能代理接口中的方法。如果被代理对象没有实现接口,或者想代理没在接口中定义的方法,JDK的动态代理就无能为力了,此时就需要CGLIB动态代理。
4.CGLIB动态代理
cglib是一种强大的,高性能高品质的代码生成库,用来在运行时扩展JAVA的类以及实现指定接口。
通过cglib提供的Enhancer类的create静态方法来创建代理类
1 | Enhancer.create(Class type, Callback callback) |
- type是原对象的Class对象
- callback是回调方法接口
cglib中的callback通过实现它的MethodInterceptor接口的intercept方法
1 | public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable; |
- obj是被代理的对象
- method是执行的方法
- args是执行方法的参数数组
- proxy用来执行未被拦截的原方法
这次的cglib代理类不局限于上面的浏览器的例子,而是通过泛型来实现通用,并且使用单例模式减少代理类的重复创建。
1 | public class CglibBrowserProxy implements MethodInterceptor{ |
然后在ChromeBrowser添加一个听音乐的方法,它并未在Browser接口定义
1 | public void listenToMusic(){ |
来看下客户端测试
1 | public class CglibDynamicProxyTest { |
可以发现没有使用Browser接口来接受代理对象,而是直接使用ChromeBrowser对象。这样的方式就可以代理ChromeBrowser中未在Chrome接口中的方法。
如果想让一个对象调用它未实现的接口中的方法,即后面AOP里所说的引用增强,原生的cglib怎么实现呢?
5.CGLIB引入增强
引入增强听上去很高大上,其实它的实现原理就以下几步:
- 通过CGLIB创建代理对象,并使其实现指定接口
- 在MethodIntercept的回调方法中,判断执行方法是否为接口中的方法,如果是,则通过反射调用接口的实现类。
创建一个新接口Game,它定义了开始的方法
1 | public interface Game { |
让代理类实现Game接口,并在intercept方法中判断执行方法是接口方法还是原对象的方法
1 | public class CglibIntroductionBrowserProxy implements MethodInterceptor,Game{ |
可以发现执行接口方法时,通过jdk的反射机制来实现的。而调用其自身方法,则是通过cglib来触发的。在上面的intercept方法中也可以在方法执行的前后添加一些操作来扩展或改变原方法。
来看测试类
1 | public class CglibIntroductionDynamicProxyTest { |
最后补充几点
- JDK动态代理的代理对象只能通过接口去接收,如果用原对象接收,会报类型转换异常。
- cglib不能拦截final修饰的方法,调用时只会执行原有方法。
- cglib是在运行时通过操作字节码来完成类的扩展和改变,除了代理,还支持很多强大的操作,比如bean的生成和属性copy,动态创建接口以及融合多个对象等,具体见https://github.com/cglib/cglib/wiki/Tutorial。
参考文档:
- http://design-patterns.readthedocs.io/zh_CN/latest/structural_patterns/proxy.html
- https://my.oschina.net/huangyong/blog/161338