设计模式|⑥代理模式
今天我要跟大家聊的是代理模式。这一模式可以说是GOF23所有设计模式中应用最广泛,但又最难以理解的一种模式,尤其是其中的动态代理模式,但是其功能之强大,应用场景之广自然就体现出其重要性。有些场景要是没有使用这一模式,就会变得很难实现。可以这么说,我所了解过的或者阅读过源码的开源框架,底层几乎没有不用到代理模式的,尤其是接下去本文要说的重点-动态代理模式。因此,在文章的最后,我也会以一个在Mybatis
底层使用动态代理模式解决的经典场景作为本文结束。
代理
首先,我们先来说说代理。何为代理?来看张图。这就是我们日常租房的场景,客户来一个陌生城市需要租一个房子,但是他人生地不熟,根本不知道行情,也不知道地段,更没有房东的联系方式,所以,他会去找类似我爱我家之类的租房中介,而这些个中介手上会有大量房子的信息来源,自然会有个房东的联系方式,进而和房东取得联系,从而达到租房的目的。这个场景就是一个经典的代理模式的体现。
静态代理
既然说到动态代理,自然联想到肯定会有静态代理。下面我们就先从简单的开始,以上面租房的这个例子,用Java
代码实现静态代理。
首先在代理模式(甭管静态还是动态)结构中,肯定会有一个真实角色(Target
),也是最后真正执行业务逻辑的那个对象,比如上图中的房东(因为最后租的房子所有权是他的,也是和他去办租房合同等手续),另外会有一个代理角色(Proxy
),比如上图中的房产中介(他没有房产所有权),并且这个角色会必然实现一个与真实角色相同的抽象接口(Subject
),为什么呢?因为虽然这个出租的房子不是他的,但是是经他之手帮忙牵线搭桥出租出去的,也就是说,他和房东都会有出租房产的行为。另外代理角色会持有一个真实角色的引用,又是为什么呢?因为他并不会(或者是不能)真正处理业务逻辑(因为房子不是他的呗),他会将真正的逻辑委托给真实角色处理。但是这个代理角色也不是一无是处,除了房子不是他的,但是他还可以给你干点跑腿的工作嘛,比如帮你挑选最好的地段,挑选合适的价格等等,等你租房后出现漏水,或者电器啥的坏了可以帮你联系维修人员等等。如下代码所示:
1 | //公共抽象接口 - 出租的人 |
静态代理模式实现相对比较简单,而且比较好理解,也确实实现了代理的效果。但是很遗憾,几乎没有一个开源框架的内部是采用静态代理来实现代理模式的。那是为什么呢?原因很简单,从上面这个例子可以看出,静态代理模式中的真实角色和代理角色紧耦合了。怎么理解?
下面来举个例子帮助理解静态代理模式的缺点,深入理解静态代理的缺点对于理解动态代理的应用场景是至关重要的。因为动态代理的诞生就是为了解决这一问题。
还是以上面的租房的场景,假设我现在需要你实现如下需求:有多个房东,并且每个房东都有多套房子出租,你怎么用Java
设计?按照上面的静态代理模式的思路,你也许会有如下实现(伪代码),
第一种方案:
1 | public class Landlord01 implements Person{ |
上面这种方案是为每个房东配一个对应的中介处理租房相关事宜。这种方案问题非常明显,每一个真实角色都需要手动创建一个代理角色与之对应,而这些代理类的逻辑有可能都是很相似的,因此当真实角色数量非常多时,会造成代理类数量膨胀问题和代码重复冗余,方案不可取。
第二种方案:
1 | public class Landlord01 implements Person{ |
第二种方案只创建一个代理角色,同时代理多个真实角色,这看上去貌似解决了第一种方案的弊病,但是同时引入了新的问题。那就是造成了代理类的膨胀。设计模式中有条重要原则——单一职责原则。这个代理类违反了该原则。当这个代理类为了代理其中某个真实角色时,需要将所有的真实角色的引用全部传入,显然太不灵活了。还是不可取。
而且有没有发现静态代理还有两个很大的问题,第一,当抽象接口一旦修改,真实角色和代理角色必须全部做修改,这违反了设计模式的开闭原则。第二,每次创建一个代理角色,需要手动传入一个已经存在的真实角色。但是在有些场景下,我们可能需要在并不知道真实角色的情况下创建出指定接口的代理。
动态代理
前面做了这么多铺垫,终于今天本文的主角——动态代理模式要登场了。此处应该有掌声……👏而动态代理模式的产生就是为了解决上面提到的静态代理所有弊病的。
JDK
动态代理的实现关键在于java.lang.reflect.Proxy
类,其newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler h)
方法是整个JDK
动态代理的核心,用于生成指定接口的代理对象。这个方法有三个参数,分别表示加载动态生成的代理类的类加载器ClassLoader
,代理类需要实现的接口interfaces
以及调用处理器InvocationHandler
,这三个参数一个比一个难以理解,说实话,我第一次学动态代理模式时,看到这三个参数也是一脸懵逼的状态。动态代理模式之所以比较难理解关键也是这个原因。放心,后面会一一详解。但在这之前,我们先做一下热身,先用代码简单使用一下JDK
的动态代理功能。代码如下:
1 | //公共抽象接口和真实角色和静态代理的例子中代码相同,省略 |
可以看出,动态代理轻松的实现了代理模式,并且输出了和静态代理相同的结果,然而我们并没有写任何的代理类,是不是很神奇?下面我们就来深度剖析JDK
实现的动态代理的原理。
Proxy.newProxyInstance()
在上面实现的JDK
动态代理代码中,核心的一行代码就是调用Proxy.newProxyInstance()
,传入类加载器等参数,然后一顿神奇的操作后居然就直接返回了我们所需要的代理对象,因此我们就从这个神奇的方法开始说起……
进入这个方法的源码中,以下是这个方法的核心代码,逻辑非常清楚,使用getProxyClass0
获取一个Class
对象,其实这个就是最终生成返回的代理代理类的Class
对象,然后使用反射方式获取有参构造器,并传入我们的自定义InvocationHandler
实例创建其对象。由此我们其实已经可以猜测,这个动态生成的代理类会有一个参数为InvocationHandler
的构造器,这一点在之后会得到验证。
1 | public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { |
那现在很明显了,关键的核心就在于getProxyClass0()
方法的逻辑了,于是我们继续深入虎穴查看其源码。
1 | private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) { |
最开始就是检验一下实现接口数量,然后执行proxyClassCache.get()
。proxyClassCache
是一个定义在Proxy
中的字段,你就将其当做一个代理类的缓存。这个也好理解,稍后大家会看到,动态代理类生成过程中会伴随大量的IO
操作,字节码
操作还有反射
操作,还是比较消耗资源的。如果需要创建的代理类数量特别多,性能会比较差。所以Proxy
提供了缓存机制
,将已经生成的代理类缓存,当获取时,会先从缓存获取,如果获取不到再执行生成逻辑。
我们继续进入proxyClassCache.get()
。这个方法看起来比较费劲,因为我使用的是JDK8
,这边用到了大量的Java8
新增的函数式编程的语法和内容,因为这边不是专门讲Java8
的,所以我就不展开函数式编程的内容了。另外,这边会有很多对缓存的操作,这个不是我们的重点,所以也全部跳过,我们挑重点看,关注一下下面这部分代码:
1 | public V get(K key, P parameter){ |
这个代码非常有意思,是一个死循环。或许你和我一样,完全看不懂这代码是啥意思,没关系,可以仔细观察一下这代码你就会发现柳暗花明。这个方法最后会需要返回一个从缓存或者新创建的代理类,而这整个死循环只有一个出口,没错就是带★这一行,而value是通过supplier.get()
获得,Supplier
是一个函数式接口,代表了一种数据的获取操作。我们再观察会发现,supplier
是通过factory
赋值而来的。而factory
是通过▲行创建出来的。WeakCache.Factory
恰好是Supplier
的实现。所以我们进入WeakCache.Factory
的get()
,核心代码如下,经观察可以发现,返回的数据最终是通过valueFactory.apply()
返回的。
1 | public synchronized V get() { |
apply
是BiFunction
的一个抽象方法,BiFunction
又是一个函数式接口。而valueFactory
是通过WeakCache
的构造器传入,是一个ProxyClassFactory
对象,而其刚好就是BiFunction
的实现,顾名思义,这个类就是专门用来创建代理类的工厂类。
1 | private static final WeakCache<ClassLoader,Class<?>[],Class<?>> |
进入ProxyClassFactory
的apply()
方法,代码如下:
1 | Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); |
通过上面代码不难发现,生成代理类的核心代码在★这一行,会使用一个ProxyGenerator
生成代理类(以byte[]
形式存在)。然后将生成得到的字节数组转换为一个Class对象。进入ProxyGenerator.generateProxyClass()
。ProxyGenerator
处于sun.misc
包,不是开源的包,因为我这边使用的是openjdk
,所以可以直接查看其源码,如果使用的是oracle jdk
的话,这边只能通过反编译class
文件查看。
1 | public static byte[] generateProxyClass(final String name, Class<?>[] interfaces, int accessFlags) { |
上述逻辑很简单,就是使用一个生成器调用generateClassFile()
方法返回代理类,后面有个if
判断我简单提一下,这个作用主要是将内存中动态生成的代理类以class
文件形式保存到硬盘。saveGeneratedFiles
这个字段是定义在ProxyGenerator
中的字段,
1 | private final static boolean saveGeneratedFiles = |
我简单说一下,AccessController.doPrivileged
这个玩意会去调用java.security.PrivilegedAction
的run()
方法,GetBooleanAction
这个玩意就实现了java.security.PrivilegedAction
,在其run()
中会通过Boolean.getBoolean()
从系统属性中获取sun.misc.ProxyGenerator.saveGeneratedFiles
的值,默认是false
,如果想要将动态生成的class
文件持久化,可以往系统属性中设置为true
。
我们重点进入ProxyGenerator.generateClassFile()
方法,代码如下:
1 | private byte[] generateClassFile() { |
如果没有学过Java虚拟机规范中关于字节码文件结构的知识的话,上面这段代码肯定是看得一头雾水,因为本文主要是讲解动态代理,加上个人对Java虚拟机的掌握也是菜鸟级别,所以下面就简单阐述一下关于字节码结构的内容以便大家理解上面这块代码,但是不展开详说。
Class文件结构简述
在Java虚拟机规范中,Class文件是一组二进制流,每个Class文件会对应一个类或者接口的定义信息,当然,Class文件并不是一定以文件形式存在于硬盘,也有可能直接由类加载器加载到内存。每一个Class文件加载到内存后,经过一系列的加载、连接、初始化过程,然后会在方法区中形成一个Class对象,作为外部访问该类信息的的唯一入口。按照Java虚拟机规范,Class文件是具有非常严格严谨的结构规范,由一系列数据项组成,各个数组项之间没有分隔符的结构紧凑排列。每个数据项会有相应的数据类型,如下表就是一个完整Class文件结构的表。
其中名称一列就是组成Class文件的数据项,限于篇幅这边就不展开详细解释每一项了,大家有兴趣可以自己去查点资料了解一下,左边是其类型,主要分两类,像u2,u4这类是无符号数,分别表示2个字节和4个字节。以info结尾的是表结构,表结构又是一个复合类型,由其它的无符号数和其他的表结构组成。
我这边以相对结构简单的field_info
结构举个例子,field_info
结构用来描述接口或者类中的变量。它的结构如下:
其它的表结构method_info
,attribute_info
也都是类似,都会有自己特有的一套结构规范。
好了,简单了解一下Class文件结构后,现在再回到我们的主题来,我们再来研究ProxyGenerator.generateClassFile()
方法内容就好理解了。其实这个方法就做了一件事情,就是根据我们传入的这些个信息,再按照Java虚拟机规范的字节码结构,用IO流的方式写入到一个字节数组中,这个字节数组就是代理类的Class文件。默认情况这个Class文件直接存在内存中,为了更加深入理解动态代理原理,该是时候去看看这个文件到底是啥结构了。怎么看?还记得前面提到过的sun.misc.ProxyGenerator.saveGeneratedFiles
吗?只要我们往系统属性中加入该参数并将其值设为true,就会自动将该方法生成的byte[]形式的Class文件保存到硬盘上,如下代码:
1 | public class Client2 { |
再次运行,神奇的一幕发生了,工程中多了一个类,没错,这就是JDK
动态代理生成的代理类,因为我们的接口是public修饰,所以采用默认包名com.sun.proxy
,类名以$Proxy
开头,后面跟一个数字,和预期完全吻合。完美!🤩
那么就让我们反编译一下这个class文件看看它的内容来一探究竟……
下面是反编译得到的代理类的内容,
1 | public final class $Proxy0 extends Proxy implements Person { ★ |
👉有几个关注点
- 标注①的是一个静态代码块,当代理类一被加载,会立刻初始化,用反射方式获取得到被代理的接口中方法和
Object
中equals()
,toString()
,hashCode()
方法的Method
对象,并将其保存在属性中,为后续请求分派做准备。 - 标注②的是带有一个带有
InvocationHandler
类型参数的构造器,这个也验证了我们之前的猜测,没错,代理类会通过构造器接收一个InvocationHandler
实例,再观察标记★的地方,代理类继承了Proxy
类,其实代理类会通过调用父类构造器将其保存在Proxy
的属性h
中,自然会继承给当前这个代理类,这个InvocationHandler
实例为后续请求分派做准备。同时由此我们也可以得出结论,Proxy
是所有的代理类的父类。另外再延伸,因为Java
是一门单继承语言,所以意味着代理类不可能再通过继承其他类的方式来扩展。所以,JDK
动态代理没法对不实现任何接口的类进行代理,原因就在于此。这或许也是动态代理模式不多的缺点之一。如果需要继承形式的类代理,可以使用CGLIB等类库。 - 标注③的是我们指定接口
Person
中的方法,标注④的是代理类继承自Object
类中的equals()
,toString()
,hashCode()
方法。再观察这些方法内部实现,所有的方法请求全部委托给之前由构造器传入的InvocationHandler
实例的invoke()
方法处理,将当前的代理类实例,各方法的Method
对象和方法参数传入,最后返回执行结果。由此得出结论,动态代理过程中,所指定接口的方法以及Object
中equals()
,toString()
,hashCode()
方法会被代理,而Object
其他方法则并不会被代理,而且所有的方法请求全部都是委托给我们自己写的自定义InvocationHandler
的invoke()
方法统一处理,哇塞,O了,这样的处理实在太优雅了!
动态代理到底有什么用
其实经过上面这一堆讲解,动态代理模式中最核心的内容基本都分析完了,相信大家应该对其也有了一个本质的认知。学以致用,技术再牛逼如果没法用在实际工作中也说实话也只能拿来装逼了。那这个东西到底有什么用呢?其实我以前学完动态代理模式后第一感觉是,嗯,这玩意确实挺牛逼的,但是到底有什么用?没有一点概念。在阅读Spring
或者Mybatis
等经典开源框架中的代码时,时不时也经常会发现动态代理模式的身影,但是还是没有一个直接的感受。直到最近一段时间我在深入研究Mybatis
源码时,看到其日志模块的设计,内部就是使用了动态代理,忽然灵光一闪,大受启发感觉一下子全想通了……这就是冥冥之中注定的吧?😂所以最后我就拿这个例子给大家讲解一下动态代理模式的实际应用场景。
想必使用过Mybatis
这一优秀持久层框架的人都注意到过,每当我们执行对数据库操作,如果日志级别是DEBUG
,控制台会打印出一些辅助信息,比如执行的SQL
语句,绑定的参数和参数值,返回的结果等,你们有没有想过这些信息到底是怎么来的?
在Mybatis
底层的日志模块中,有一块专门用于打印JDBC
相关信息日志的功能。这块功能是由一系列xxxLogger
类构成。其中最顶层的是BaseJdbcLogger
,他有4个子类,继承关系如下图:
看名字应该就能猜出来是干啥了,以ConnectionLogger
为例,下面是ConnectionLogger
的关键代码:
1 | public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler { ❶ |
怎么样?是不是有种熟悉的感觉?🙀
👉观察上面代码,可以得出以下几点结论:
ConnectionLogge
r实现了InvocationHandler
,通过构造器传入真实Connection
对象,这是一个真实对象,并将其保存在属性,后续请求会委托给它执行。其静态方法newInstance()
内部就是通过Proxy.newProxyInstance()
并传入类加载器等一系列参数返回一个Connection
的代理对象给前端。该方法最终会在DEBUG
日志级别下被org.apache.ibatis.executor.BaseExecutor.getConnection()
方法调用返回一个Connection
代理对象。前面说过,
JDK
动态代理会将客户端所有的请求全部派发给InvocationHandler
的invoke()
方法,即上面ConnectionLogger
中的invoke()
方法。invoke()
方法当中,不难发现,Mybatis
对于Object
中定义的方法,统一不做代理处理,直接调用返回。对于prepareStatement()
,prepareCall()
,createStatement()
这三个核心方法会统一委托给真实的Connection
对象处理,并且在执行之前会以DEBUG
方式打印日志信息。除了这三个方法,Connection
其它方法也会被真实的Connection
对象代理,但是并不会打印日志信息。我们以prepareStatement()
方法为例,当真实的Connection
对象调用prepareStatement()
方法会返回PreparedStatement
对象,这又是一个真实对象,但是Mybatis
并不会将该真实对象直接返回,而且通过调用PreparedStatementLogger.newInstance()
再次包装代理,看到这个方法名字,我相信聪明的您都能猜到这个方法的逻辑了。没错,PreparedStatementLogger
类的套路和ConnectionLogger
如出一辙。这边我再贴回PreparedStatementLogger
的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {
private final PreparedStatement statement;
private PreparedStatementLogger(PreparedStatement stmt, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.statement = stmt;
}
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
if (EXECUTE_METHODS.contains(method.getName())) {
if (isDebugEnabled()) {
debug("Parameters: " + getParameterValueString(), true);
}
clearColumnInfo();
if ("executeQuery".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else {
return method.invoke(statement, params);
}
} else if (SET_METHODS.contains(method.getName())) {
if ("setNull".equals(method.getName())) {
setColumn(params[0], null);
} else {
setColumn(params[0], params[1]);
}
return method.invoke(statement, params);
} else if ("getResultSet".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else if ("getUpdateCount".equals(method.getName())) {
int updateCount = (Integer) method.invoke(statement, params);
if (updateCount != -1) {
debug(" Updates: " + updateCount, false);
}
return updateCount;
} else {
return method.invoke(statement, params);
}
}
public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
ClassLoader cl = PreparedStatement.class.getClassLoader();
return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
}
}这个代码的逻辑我就不讲了,思路几乎和
ConnectionLogger
完全一致。无非是拦截的方法不同,因为这次被代理对象是PreparedStatement
,所以这次会去拦截都是PreparedStatement
的方法,比如setXXX()
系列,executeXX()
系列等方法。然后在指定方法执行前后添加需要的DEBUG
日志信息,perfect!以getResultSet()
方法为例,PreparedStatement
对象调用getResultSet()
后,会返回真实的ResultSet
对象,但是一样的套路,并不会直接将该真实对象返回,而是由调用ResultSetLogger.newInstance()
再次将该ResultSet
对象包装,ResultSetLogger
的代码相信聪明的您不需要我再花篇幅讲了。这个时候,再回过头思考一下,这个场景下,如果是采用静态代理是不是根本没法完成了?因为,每一个数据库连接都会产生一个新的
Connection
对象,而每一个Connection
对象每次调用preparedStatement()
方法都会产生一个新的PreparedStatement
对象,而每一个PreparedStatement
对象每次调用getResultSet()
又都会产生一个新的ResultSet
对象,跟上面的多个房东出租房子一个道理,就会产生不计其数处理逻辑极其相似的代理类,所以,这才是开源框架底层不采用静态代理的本质原因!一切都豁然开朗了!😍
结束
好了,关于JDK
动态代理的核心原理部分到这里算全部讲解完毕了,其实我们聊了这么多,都是围绕着java.lang.reflect.Proxy.newProxyInstance()
这个方法展开的。其实在Proxy
类中,还有一个getProxyClass()
方法,这个只需要传入加载代理类的类加载器和指定接口就可以动态生成其代理类,我一开始说到静态代理弊病的时候说过,静态代理创建代理时,真实角色必须要存在,否则这个模式没法进行下去,但是JDK动态代理可以做到在真实角色不存在的情况下就返回该接口的代理类。至于Proxy
其它的方法都比较简单了,此处不再赘述。