什么是代理
我们大家都知道微商代理,简单地说就是代替厂家卖商品,厂家“委托”代理为其销售商品。关于微商代理,首先我们从他们那里买东西时通常不知道背后的厂家究竟是谁,也就是说,“委托者”对我们来说是不可见的;其次,微商代理主要以朋友圈的人为目标客户,这就相当于为厂家做了一次对客户群体的“过滤”。我们把微商代理和厂家进一步抽象,前者可抽象为代理类,后者可抽象为委托类(被代理类)。通过使用代理,通常有两个优点,并且能够分别与我们提到的微商代理的两个特点对应起来:
优点一:可以隐藏委托类的实现;
优点二:可以实现客户与委托类间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。
代理模式
代理模式:给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。
代理模式角色分为 3 种:
Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;
RealSubject(真实主题角色):真正实现业务逻辑的类;
Proxy(代理主题角色):用来代理和封装真实主题;
代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层
如果根据字节码的创建时机来分类,可以分为静态代理和动态代理:
静态代理
就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。动态代理
源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件。
静态代理
代码示例
我们先通过实例来学习静态代理,然后理解静态代理的缺点,再来学习本文的主角:动态代理
编写一个接口 UserService ,以及该接口的一个实现类 UserServiceImpl:
1 | public interface UserService { |
我们将通过静态代理对 UserServiceImpl 进行功能增强,在调用 select 和 update 之前记录一些日志。写一个代理类 UserServiceProxy,代理类需要实现 UserService
1 | public class UserServiceProxy implements UserService { |
测试;
1 | public class Client1 { |
输出:
1 | log start time [Thu Dec 20 14:13:25 CST 2018] |
通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。
静态代理缺点
虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。
1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大;
新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类;
2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
动态代理
生成原理
Java虚拟机类加载过程主要分为五个阶段:加载
、验证
、准备
、解析
、`初始化。其中加载阶段需要完成以下3件事情:
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据访问入口
由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:
从ZIP包获取,这是JAR、EAR、WAR等格式的基础
从网络中获取,典型的应用是 Applet
运行时计算生成,这种场景使用最多的是动态代理技术,在
java.lang.reflect.Proxy
类中,就是用了ProxyGenerator.generateProxyClass
来为特定接口生成形式为*$Proxy
的代理类的二进制字节流由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类
从数据库中获取等等
所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案。
常见的字节码操作类库
这里有一些介绍:java-source.net/open-source…
Apache BCEL (Byte Code Engineering Library):是Java classworking广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。
ObjectWeb ASM:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。
CGLIB(Code Generation Library):是一个功能强大,高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。
Javassist:是Java的加载时反射系统,它是一个用于在Java中编辑字节码的类库; 它使Java程序能够在运行时定义新类,并在JVM加载之前修改类文件。
为了让生成的代理类与目标对象(真实主题角色)保持一致性,从现在开始将介绍以下两种最常见的方式:
通过实现接口的方式 -> JDK动态代理
通过继承类的方式 -> CGLIB动态代理
注:使用ASM对使用者要求比较高,使用Javassist会比较麻烦
JDK动态代理
JDK动态代理主要涉及两个类:java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
,我们仍然通过案例来学习.
编写一个调用逻辑处理器 LogHandler 类,提供日志增强功能,并实现 InvocationHandler 接口;在 LogHandler 中维护一个目标对象,这个对象是被代理的对象(真实主题角色);在 invoke 方法中编写方法调用的逻辑处理
1 | import java.lang.reflect.InvocationHandler; |
编写客户端,获取动态生成的代理类的对象须借助 Proxy 类的 newProxyInstance 方法:
1 | import proxy.UserService; |
运行结果
1 | log start time [Thu Dec 20 16:55:19 CST 2018] |
InvocationHandler和Proxy核心方法介绍
java.lang.reflect.InvocationHandler
Object invoke(Object proxy, Method method, Object[] args)
定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用
java.lang.reflect.Proxy
static InvocationHandler getInvocationHandler(Object proxy)
用于获取指定代理对象所关联的调用处理器
static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
返回指定接口的代理类
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
构造实现指定接口的代理类的一个新实例,所有方法会调用给定处理器对象的 invoke 方法
static boolean isProxyClass(Class<?> cl)
返回 cl 是否为一个代理类
代理类
JDK自动生成的代理类到底长什么样子呢?借助上面第6步操作,可以把代理类保存下来一探究竟, target 的类路径下找到 UserServiceProxy2.class,双击后IDEA的反编译插件会将该二进制class文件转变成java文件:
1 | import java.lang.reflect.InvocationHandler; |
从 UserServiceProxy 的代码中我们可以发现:
UserServiceProxy2
继承了Proxy
类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法由于
UserServiceProxy2
继承了Proxy
类,所以每个代理类都会关联一个InvocationHandler
方法调用处理器类和所有方法都被
public final
修饰,所以代理类只可被使用,不可以再被继承每个方法都有一个
Method
对象来描述,Method
对象在static
静态代码块中创建,以m+
数字 的格式命名调用方法的时候通过
super.h.invoke(this, m1, (Object[])null)
调用,其中的super.h.invoke
实际上是在创建代理的时候传递给Proxy.newProxyInstance
的LogHandler
对象,它继承InvocationHandler
类,负责实际的调用处理逻辑
而 LogHandler 的 invoke 方法接收到 method、args 等参数后,进行一些处理,然后通过反射让被代理的对象 target 执行方法
1 |
|
JDK动态代理执行方法调用的过程简图如下:
CGLIB动态代理
maven引入CGLIB包,然后编写一个UserDao类,它没有接口,只有两个方法,select() 和 update()
1 | public class UserDao { |
编写一个 LogInterceptor ,继承了 MethodInterceptor,用于方法的拦截回调
1 | public class LogInterceptor implements MethodInterceptor { |
测试:
1 | public class CglibClient { |
结果:
1 | log start time [Fri Nov 29 10:51:17 CST 2019] |
CGLIB 创建动态代理类的模式是:
查找目标类上的所有非final 的public类型的方法定义;
将这些方法的定义转换成字节码;
将组成的字节码转换成相应的代理的class对象;
实现
MethodInterceptor
接口,用来处理对代理类上所有方法的请求
JDK与CGLIB动态代理对比
JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。
Cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。
JDK动态代理优势
最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
代码实现简单。
基于类似 cglib 框架的优势
- 无需实现接口,达到代理类无侵入
- 只操作我们关心的类,而不必为其他相关类增加工作量。
- 高性能
参考
[1] Java 动态代理详解
[2] Java动态代理
[3] Java反射机制详解
[4] 从代理模式再出发!Proxy.newProxyInstance的秘密