开心一刻
周末,带着老婆儿子一起逛公园。儿子一个人跑在前面,吧唧一下不小心摔了一跤,脑袋瓜子摔了个包,稀里哗啦的哭道:“爸爸,我会不会摔成傻子!”
我指了指我头上的伤痕安慰道:“不会的,你看,这是爸爸小时候摔的。”
话还没有说话,小家伙哭的更厉害了:“那就是说我长大后就会和你一样傻了,我不要,我不要!”
老婆忍不住发飙:“别哭了,你怎么会变傻呢?你看你爸,你爸傻吗?”
我赶紧回应道:“是啊,你看我多聪明!”
儿子:“真的,不骗我?”
老婆:“当然!”
儿子:“可是如果老爸不是傻子,当年怎么会娶你这个母老虎呢?”
我、老婆:……
什么是代理模式
所谓代理,就是一个人或者一个机构代表另一个人或者另一个机构采取行动。在一些情况下,一个客户不想或者不能直接引用一个对象,而代理对象可以在客户端和目标对象之间起到中介的左右。
代理模式:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问,通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。说简单点,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能和简化访问方式。一般而言会分三种:静态代理、动态代理和CGLIB代理
代理模式结构如下:
静态代理
静态代理需要代理对象和被代理对象实现一样的接口,我们来看个例子就清楚了
示例代理:static-proxy
代理类:UserDaoProxy.java
View Code
UserDaoProxy代理IUserDao类型,此时也只能代理IUserDao类型的被代理对象。测试结果就不展示了,相信大家看了代码也知道了
优点:可以在不修改目标对象的前提下扩展目标对象的功能
缺点:如果需要代理多个类,每个类都会有一个代理类,会导致代理类无限制扩展;如果类中有多个方法,同样的代理逻辑需要反复实现、应用到每个方法上,一旦接口增加方法,目标对象与代理对象都要进行修改
一个静态代理只能代理一个类,那么有没有什么方式可以实现同一个代理类来代理任意对象呢?肯定有的,也就是下面讲到的:动态代理
动态代理
代理类在程序运行时创建的代理方式被成为动态代理。 也就是说,这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。下面我们一步一步手动来实现动态代理。下面的示例都是直接针对接口的,就不是针对接口的具体实现类了,静态代理示例中,UserDaoProxy代理的是IUserDao的实现类:UserDaoImpl,那么动态代理示例就直接针对接口了,下面示例针对的都是UserMapper接口,模拟的mybatis,但不局限于UserMapper接口
代理类源代码持久化
1、先利用反射动态生成代理类,并持久化代理类到磁盘(也就是生成代理类的java源文件),generateJavaFile方法如下www.zztjyy.com
View Code
生成的代理类:$Proxy0.java 如下
View Code
这个代理类的生成过程是我们自己实现的,实现不难,但排版太繁琐,我们可以用javapoet来生成代理类源代码,generateJavaFileByJavaPoet方法如下
View Code
生成的代理类:JavaPoet$Proxy0.java 如下
View Code
利用javapoet生成的代理类更接近我们平时手动实现的类,排版更符合我们的编码习惯,看上去更自然一些;两者的实现过程是一样的,只是javapoet排版更好
2、既然代理类的源代码已经有了,那么需要对其编译了,compileJavaFile方法如下
View Code
会在指定目录下看到:$Proxy0.class
3、加载$Proxy0.class,并创建其实例对象(代理实例对象)
View Code
有了代理实例对象,我们就可以利用它进行操作了,演示结果如下
完整工程地址:proxy-java-file,完整流程图如下
此时的Proxy类能创建任何接口的实例,解决了静态代理存在的代理类泛滥、多个方法中代理逻辑反复实现的问题;但有个问题不知道大家注意到:$Proxy0.java有必要持久化到磁盘吗,我们能不能直接编译内存中的代理类的字符串源代码,得到$Proxy0.class呢?
www.4000131666.com/
代理类源代码不持久化
$Proxy0.java和$Proxy0.class是没必要生成到磁盘的,我们直接编译内存中的代理类的字符串源代码,同时直接在内存中加载$Proxy0.class,不用写、读磁盘,可以提升不少性能
完整工程地址:proxy-none-java-file,此时的流程图如下
Proxy.java源代码如下
View Code
相比有代理类源代码持久化,核心的动态代理生成过程不变,只是减少了.java和.class文件的持久化;其中用到了第三方工具:com.itranswarp.compile(我们也可以拓展jdk,实现内存中操作),完成了字符串在内存中的编译、class在内存中的加载,直接用jdk的编译工具,会在磁盘生成$Proxy0.class
测试结果如下
可以看到,没有.java和.class的持久化
此时就完美了吗?如果现在有另外一个接口ISendMessage,代理逻辑不是
System.out.println("数据库操作, 并获取执行结果...")
我们该怎么办? 针对ISendMessage又重新写一个Proxy?显然还不够灵活,说的简单点:此种代理可以代理任何接口,但是代理逻辑确是固定死的,不能自定义,这样会造成一种代理逻辑会有一个代理工厂(Proxy),会造成代理工厂的泛滥
代理逻辑接口化,供用户自定义
既然无代理类源代码持久化中的代理逻辑不能自定义,那么我们就将它抽出来,提供代理逻辑接口
完整工程地址:proxy-none-java-file-plus,流程图与无代理类源代码持久化中一样,此时代理类的生成过程复杂了不少,涉及到代理逻辑接口:InvacationHandler的处理
generateJavaFile(...)方法
View Code
测试结果如下
此时各组件之间关系、调用情况如下
此时Proxy就可以完全通用了,可以生成任何接口的代理对象了,也可以实现任意的代理逻辑;至此,我们完成了一个简易的仿JDK实现的动态代理
JDK的动态代理
我们来看看JDK下动态代理的实现,示例工程:proxy-jdk,测试结果就不展示了,我们来看看JDK下Proxy.newInstance方法,有三个参数
1、Classloader:类加载器,我们可以使用自定义的类加载器;上述手动实现示例中,直接在Proxy写死了;
2、Class>[]:接口类数组,这个其实很容易理解,我们应该允许我们自己实现的代理类同时实现多个接口。我们上述手动实现中只传入一个接口,是为了简化实现;
3、InvocationHandler:这个没什么好说的,与我们的实现一致,用于自定义代理逻辑
我们来追下源码,看看JDK的动态代理是否与我们的手动实现是否一致
与我们的自定义实现差不多,利用反射,逐个接口、逐个方法进行处理;ProxyClassFactory负责生成代理类的Class对象,主要有apply方法负责,调用了
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
来生成代理类的Class;ProxyGenerator中有个是有静态常量:saveGeneratedFiles,标识是否持久化代理类的class文件,默认值是false,也就是不持久化,我们可以通过设置jdk系统参数,实现JDK的动态代理持久化代理类的class文件
CGLIB代理
对cglib不做深入研究了,只举个使用案例:proxy-cglib,使用方式与JDK的动态代理类似,实现的效果也基本一致,但是实现原理上还是有差别的
JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口,而CGLIB没有这个限制,具体区别不是本文范畴了,大家自行去查阅资料
应用场景
长篇大论讲了那么多,我们却一直没有讲动态代理的作用,使用动态代理我们可以在不改变源码的情况下,对目标对象的目标方法进行前置或后置增强处理。这有点不太符合我们的一条线走到底的编程逻辑,这种编程模型有一个专业名称叫AOP,面向切面编程,具体案例有如下:
1、spring的事务,事务的开启可以作为前置增强,事务的提交或回滚作为后置增强,数据库的操作处在两者之前;
2、日志记录,我们可以在不改变原有实现的基础上,对目标对象进行日志的输出,可以前置处理,记录参数情况,也可以后置处理,记录返回的结果;
3、web编程,传入参数的校验;
4、web编程,权限的控制也可以用aop来实现;
只要明白了AOP,那么哪些场景能使用动态代理也就比较明了了
总结
1、示例代码中的Proxy是代理工厂,负责生产代理对象的,不是代理对象类
2、手动实现动态代理,我们分了三版
第一版:代理类源代码持久化,为了便于理解,我们将代理类的java文件和class文件持久化到了磁盘,此时解决了静态代理中代理类泛滥的问题,我们的代理类工厂(Proxy)能代理任何接口;
第二版:代理类源代码不持久化,代理类的java文件和和class文件本来就只是临时文件,将其去掉,不用读写磁盘,可以提高效率;但此时有个问题,我们的代理逻辑却写死了,也就说一个代理类工厂只能生产一种代理逻辑的代理类对象,如果我们有多种代理逻辑,那么就需要有多个代理类工厂,显然灵活性不够高,还有优化空间;
第三版:代理逻辑接口化,供用户自定义,此时代理类工厂就可以代理任何接口、任何代理逻辑了,反正代理逻辑是用户自定义传入,用户想怎么定义就怎么定义;
3、示例参考的是mybatis中mapper的生成过程,虽然只是简单的模拟,但流程却是一致的,有兴趣的可以看看我前两篇博客,结合起来看更好理解
参考