导航菜单
首页 » 无极荣耀登陆 » 正文

下雨-「原创」让规划形式飞一瞬间|⑥面试必问署理形式

本文中一切的代码和运转成果都是在amazon corretto openjdk 1.8环境中的,假如你不是运用该环境,或许会略有误差。别的为了代码看起来明晰整齐,将一切代码中的反常处理逻辑悉数拿去了。

一些废话

哈喽,我们好,我是高冷便是范儿,好久不见!今日我们持续来聊规划办法这个论题。前面现已讲过几个办法,假如没有阅览过的朋友能够回忆一下。

前文回忆

【原创】让规划办法飞一瞬间|①开篇

【原创】让规划办法飞一瞬间|②单例办法

【原创】让规划办法飞一瞬间|③工厂办法

【原创】让规划办法飞一瞬间|④原型办法

【原创】让规划办法飞一瞬间|⑤制作者办法


今日我要跟我们聊的是署理办法。信任只需是对GOF23规划办法有简略了解过的,或许看过我github上面曾经学习时记的笔记,或多或少是听说过署理办法的。这一办法能够说是GOF23一切规划办法中运用最广泛,但又最难以了解的一种办法,尤其是其间的动态署理办法,可是其功用之强壮,运用场景之广天然就表现出其重要性。有些场景要是没有运用这一办法,就会变得很难完成。能够这么说,我所了解过的或许阅览过源码的开源结构,底层简直没有不用到署理办法的,尤其是接下去本文要说的要点-动态署理办法。因而,在文章的终究,我也会以一个在Mybatis底层运用动态署理办法处理的经典场景作为本文结束。

署理

首要,我们先来说说署理。何为署理?来看张图。这便是我们日常租房的场景,客户来一个生疏城市需求租一个房子,可是他人生地不熟,底子不知道行情,也不知道地段,更没有房东的联络办法,所以,他会去找类似我爱我家之类的租房中介,而这些个中介手上会有许多房子的信息来历,天然会有个房东的联络办法,然后和房东取得联络,然后到达租房的意图。这个场景便是一个经典的署理办法的表现。


静态署理

已然提到动态署理,天然联想到必定会有静态署理。下面我们就先从简略的开端,以上面租房的这个比方,用Java代码完成静态署理。

首要在署理办法(别管静态仍是动态)结构中,必定会有一个实在人物(Target),也是终究实在履行事务逻辑的那个目标,比方上图中的房东(由于终究租的房子一切权是他的,也是和他去办租房合平等手续),别的会有一个署理人物(Proxy),比方上图中的房产中介(他没有房产一切权),并且这个人物会必定完成一个与实在人物相同的笼统接口(Subject),为什么呢?由于尽管这个租借的房子不是他的,可是是经他之手协助穿针引线租借出去的,也便是说,他和房东都会有租借房产的行为。别的署理人物会持有一个实在人物的引证,又是为什么呢?由于他并不会(或许是不能)实在处理事务逻辑(由于房子不是他的呗),他会将实在的逻辑托付给实在人物处理。可是这个署理人物也不是一无可取,除了房子不是他的,可是他还能够给你干点跑腿的作业嘛,比方帮你选择最好的地段,选择适宜的价格等等,等你租房后呈现漏水,或许电器啥的坏了能够帮你联络修理人员等等。如下代码所示:

//公共笼统接口 - 租借的人
public interface Person {
void rent();
}
//实在人物 - 房东
public class Landlord implements Person{
public void rent() {
System.out.println("客官请进,我家的房子又大又廉价,来租我的吧...");
}
}
//署理人物 - 房产中介
public class Agent implements Person{
Person landlord;
public Agent(Person landlord) {
this.landlord = landlord;
}
public void rent() {
//前置处理
System.out.println("经过前期调研,西湖边的房子环境挺好的...");
//托付实在人物处理
landlord.rent();
//后置处理
System.out.println("房子漏水,帮你联络修理人员...");
}
}
//客户端
public class Client {
public static void main(String[] args) {
Person landlord = new Landlord();
Person agent = new Agent(landlord);
agent.rent();
}
}
//输出成果:
经过前期调研,西湖边的房子环境挺好的...
客官请进,我家的房子又大又廉价,来租我的吧...
房子漏水,帮你联络修理人员...

静态署理办法完成相对比较简略,并且比较好了解,也的确完成了署理的效果。可是很惋惜,简直没有一个开源结构的内部是选用静态署理来完成署理办法的。那是为什么呢?原因很简略,从上面这个比方能够看出,静态署理办法中的实在人物和署理人物紧耦合了。怎样了解?

下面来举个比方协助了解静态署理办法的缺陷,深化了解静态署理的缺陷关于了解动态署理的运用场景是至关重要的。由于动态署理的诞生便是为了处理这一问题。

仍是以上面的租房的场景,假定我现在需求你完成如下需求:有多个房东,并且每个房东都有多套房子租借,你怎样用Java规划?依照上面的静态署理办法的思路,你或许会有如下完成(伪代码),

榜首种计划:

public class Landlord01 implements Person{
public void rent01() { ... }
public void rent02() { ... }
public void rent03() { ... }
}
public class Landlord02 implements Person{
public void rent01() { ... }
public void rent02() { ... }
public void rent03() { ... }
}
public class Landlord03 implements Person{
public void rent01() { ... }
public void rent02() { ... }
public void rent03() { ... }
}
... 或许还有许多房东,省掉
public class Agent01 implements Person{
Person landlord01;
//省掉结构器等信息
public void rent() {landlord01.rent();}
}
public class Agent02 implements Person{
Person landlord02;
//省掉结构器等信息
public void rent() {landlord02.rent();}
}
public class Agent03 implements Person{
Person landlord03;
//省掉结构器等信息
public void rent() {landlord03.rent();}
}
...

上面这种计划是为每个房东配一个对应的中介处理租房相关事宜。这种计划问题十分显着,每一个实在人物都需求手动创立一个署理人物与之对应,而这些署理类的逻辑有或许都是很类似的,因而当实在人物数量十分多时,会构成署理类数量胀大问题和代码重复冗余,计划不可取。

第二种计划:

public class Landlord01 implements Person{
public void rent01() { ... }
public void rent02() { ... }
public void rent03() { ... }
}
public class Landlord02 implements Person{
public void rent01() { ... }
public void rent02() { ... }
public void rent03() { ... }
}
public class Landlord03 implements Person{
public void rent01() { ... }
public void rent02() { ... }
public void rent03() { ... }
}
public class Agent implements Person{
Person landlord01;
Person landlord02;
Person landlord03;
//省掉结构器等信息
public void rent01() { ... }
public void rent02() { ... }
public void rent03() { ... }
}

第二种计划只创立一个署理人物,一同署理多个实在人物,这看上去形似处理了榜首种计划的弊端,可是一同引入了新的问题。那便是构成了署理类的胀大。规划办法中有条重要准则——单一责任准则。这个署理类违反了该准则。当这个署理类为了署理其间某个实在人物时,需求将一切的实在人物的引证悉数传入,明显太不灵活了。仍是不可取。

并且有没有发现静态署理还有两个很大的问题,榜首,当笼统接口一旦修正,实在人物和署理人物有必要悉数做修正,这违反了规划办法的开闭准则。第二,每次创立一个署理人物,需求手动传入一个现已存在的实在人物。可是在有些场景下,我们或许需求在并不知道实在人物的状况下创立出指定接口的署理。

动态署理

前面做了这么多衬托,总算今日本文的主角——动态署理办法要上台了。此处应该有掌声......而动态署理办法的发生便是为了处理上面提到的静态署理一切弊端的。

JDK动态署理的完成要害在于java.lang.reflect.Proxy类,其newProxyInstance(ClassLoader loader,Class

//公共笼统接口和实在人物和静态署理的比方中代码相同,省掉
//自界说调用处理器
public class RentHandler implements InvocationHandler {
Person landlord;

public RentHandler(Person landlord) {
this.landlord = landlord;
}
//客户端对署理目标建议的一切恳求都会被托付给该办法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//前置处理
System.out.println("经过前期调研,西湖边的房子环境挺好的...");
//托付给实在人物处理事务逻辑
method.invoke(landlord, args);
//后置处理
System.out.println("房子漏水,帮你联络修理人员...");
return null;
}
}
//客户端
public class Client2 {
public static void main(String[] args) {
Person landlord = new Landlord();
Person proxy = (Person) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(), //默许类加载器
new Class[]{Person.class}, //署理的接口
new RentHandler(landlord));//自界说调用处理器完成
proxy.rent();
}
}
//输出成果:
经过前期调研,西湖边的房子环境挺好的...
客官请进,我家的房子又大又廉价,来租我的吧...
房子漏水,帮你联络修理人员...

能够看出,动态署理轻松的完成了署理办法,并且输出了和静态署理相同的成果,可是我们并没有写任何的署理类,是不是很奇特?下面我们就来深度剖析JDK完成的动态署理的原理。

Proxy.newProxyInstance()

在上面完成的JDK动态署理代码中,中心的一行代码便是调用Proxy.newProxyInstance(),传入类加载器等参数,然后一顿奇特的操作后竟然就直接回来了我们所需求的署理目标,因而我们就从这个奇特的办法开端说起......

进入这个办法的源码中,以下是这个办法的中心代码,逻辑十分清楚,运用getProxyClass0获取一个Class目标,其实这个便是终究生成回来的署理署理类的Class目标,然后运用反射办法获取有参结构器,并传入我们的自界说InvocationHandler实例创立其目标。由此我们其完成已能够猜想,这个动态生成的署理类会有一个参数为InvocationHandler的结构器,这一点在之后会得到验证。

publ下雨-「原创」让规划形式飞一瞬间|⑥面试必问署理形式ic static Object newProxyInstance(ClassLoader loader, Class
... //省掉一些非空校验,权限校验的逻辑
//回来一个署理类,这个是整个办法的中心,后续会做具体剖析
Class
//运用反射获取其有参结构器,constructorParams是界说在Proxy类中的字段,值为{InvocationHandler.class}
final Constructor
//运用回来创立署理目标
return cons.newInstance(new Object[]{h});
}

那现在很显着了,要害的中心就在于getProxyClass0()办法的逻辑了,所以我们持续深化虎穴检查其源码。

private static Class
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
return proxyClassCache.get(loader, interfaces);
}

最开端便是查验一下完成接口数量,然后履行proxyClassCache.get()。proxyClassCache是一个界说在Proxy中的字段,你就将其作为一个署理类的缓存。这个也好了解,稍后我们会看到,动态署理类生成进程中会随同许多的IO操作,字节码操作还有反射操作,仍是比较耗费资源的。假如需求创立的署理类数量特别多,功用会比较差。所以Proxy供给了缓存机制,将现已生成的署理类缓存,当获取时,会先从缓存获取,假如获取不到再履行生成逻辑。

我们持续进入proxyClassCache.get()。这个办法看起来比较费力,由于我运用的是JDK8,这边用到了许多的Java8新增的函数式编程的语法和内容,由于这边不是专门讲Java8的,所以我就不打开函数式编程的内容了。今后有机会在其它专题胪陈。别的,这边会有许多对缓存的操作,这个不是我们的要点,所以也悉数越过,我们挑要点看,重视一下下面这部分代码:

public V get(K key, P parameter){
营业执照查询... //省掉许多的缓存操作
while (true) {
if (supplier != null) {
V value = supplier.get();
if (value != null) {
return value; ★
}
}
if (factory == null) {
factory = new WeakCache.Factory(key, parameter, subKey, valuesMap); ▲
}
if (supplier == null) {
supplier = valuesMap.putIfAbsent(subKey, factory);
if (supplier == null) {
supplier = factory;
}
} else {
if (valuesMap.replace(subKey, supplier, factory)) {
supplier = factory;
} else {
supplier = valuesMap.get(subKey);
}
}
}
}

这个代码十分有意思,是一个死循环。或许你和我相同,彻底看不懂这代码是啥意思,不要紧,能够仔细调查一下这代码你就会发现山穷水尽。这个办法终究会需求回来一个从缓存或许新创立的署理类,而这整个死循环只需一个出口,没错便是带★这一行,而value是经过supplier.get()取得,Supplier是一个函数式接口,代表了一种数据的获取操作。我们再调查会发现,supplier是经过factory赋值而来的。而factory是经过▲行创立出来的。WeakCache.Factory恰好是Supplier的完成。所以我们进入WeakCache.Factory的get(),中心代码如下,经调查能够发现,回来的数据终究是经过valueFactory.apply()回来的。

public synchronized V get() {
... //省掉一些缓存操作
V value = null;
value = Objects.requireNonNull(valueFactory.apply(key, parameter));
... //省掉一些缓存操作
return value;
}

apply是BiFunction的一个笼统办法,BiFunction又是一个函数式接口。而valueFactory是经过WeakCache的结构器传入,是一个ProxyClassFactory目标,而其刚好便是BiFunction的完成,望文生义,这个类便是专门用来创立署理类的工厂类。



进入ProxyClassFactory的apply()办法,代码如下:

Map<>(interfaces.length);
//对每一个指定的Class校验其是否能被指定的类加载器加载以及校验是否是接口,动态署理只能对接口署理,至于原因,后边会说。
for (Class
Class
interfaceClass = Class.forName(intf.getName(), false, loader);
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}
//下面这一大段是用来指定生成的署理类的包信息
//假如满是public的,便是用默许的com.sun.proxy,
//假如有非public的,一切的非public接口有必要处于同一等级包下面,而该包途径也会成为生成的署理类的包。
String proxyPkg = null;
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
for (Class
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}
if (proxyPkg == null) {
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
long num = nextUniqueNumber.getAndIncrement();
//署理类终究生成的姓名是包名+$Proxy+一个数字
String proxyName = proxyPkg + proxyClassNamePrefix + num;
//生成署理类的中心
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);★
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
}

经过上面代码不难发现,生成署理类的中心代码在★这一行,会运用一个ProxyGenerator生成署理类(以byte[]办法存在)。然后将生成得到的字节数组转换为一个Class目标。进入ProxyGenerator.generateProxyClass()。ProxyGenerator处于sun.misc包,不是开源的包,由于我这边运用的是openjdk,所以能够直接检查其源码,假如运用的是oracle jdk的话,这边只能经过反编译class文件检查。

 public static byte[] generateProxyClass(final String name, Class
ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags);
final byte[] classFile = gen.generateClassFile();
if (saveGenera下雨-「原创」让规划形式飞一瞬间|⑥面试必问署理形式tedFiles) {
//省掉一堆IO操作
}
return classFile;
}

上述逻辑很简略,便是运用一个生成器调用generateClassFile()办法回来署理类,后边有个if判别我简略提一下,这个效果首要是将内存中动态生成的署理类以class文件办法保存到硬盘。saveGeneratedFiles这个字段是界说在ProxyGenerator中的字段,

private final static boolean saveGeneratedFiles =
java.security.AccessController.doPrivileged(
new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles")).booleanValue();

我简略说一下,AccessController.doPrivileged这个玩意会去调用java.security.PrivilegedAction的run()办法,GetBooleanAction这个玩意就完成了java.security.PrivilegedAction,在其run()中会经过Boolean.getBoolean()从体系特点中获取sun.misc.ProxyGenerator.saveGeneratedFiles的值,默许是false,假如想要将动态生成的class文件耐久化,能够往体系特点中设置为true。

我们要点进入ProxyGenerator.generateClassFile()办法,代码如下:

private byte[] generateClassFile() {
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);
for (Class
for (Method m : intf.getMethods()) {
addProxyMethod(m, intf);
}
}
for (List sigmethods : proxyMethods.values()) {
checkReturnTypes(sigmethods);
}
methods.add(generateConstructor());
for (List sigmethods : proxyMethods.values()) {
for (ProxyGenerator.ProxyMethod pm : sigmethods) {
fields.add(new ProxyGenerator.FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;",
ACC_PRIVATE | ACC_STATIC));
methods.add(pm.generateMethod());
}
}
methods.add(generateStaticInitializer());
if (methods.size() > 65535) {
throw new IllegalArgumentException("method limit exceeded");
}
if (fields.size() > 65535) {
throw new IllegalArgumentException("field limit exceeded");
}
cp.getClass(dotToSlash(className));
cp.getClass(superclassName);
for (Class
cp.getClass(dotToSlash(intf.getName()));
}
cp.setReadOnly();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
dout.writeInt(0xCAFEBABE);
// u2 minor_version;
dout.writeShort(CLASSFILE_MINOR_VERSION);
// u2 major_version;
dout.writeShort(CLASSFILE_MAJOR_VERSION);
cp.write(dout); // (write constant pool)
// u2 access_flags;
dout.writeShort(accessFlags);
// u2 this_class;
dout.writeShort(cp.getClass(dotToSlash(className)));
// u2 super_class;
dout.writeShort(cp.getClass(superclassName));
// u2 interfaces_count;
dout.writeShort(interfaces.length);
// u2 interfaces[interfaces_count];
for (Class
dout.writeShort(cp.getClass(
dotToSlash(intf.getName())));
}
// u2 fields_count;
dout.writeShort(fields.size());
// field_info fields[fields_count];
for (ProxyGenerator.FieldInfo f : fields) {
f.write(dout);
}
// u2 methods_count;
dout.writeShort(methods.size());
// method_info methods[methods_count];
for (ProxyGenerator.MethodInfo m : methods) {
m.write(dout);
}
// u2 attributes_count;
dout.writeShort(0);
return bout.toByteArray();
}

假如没有学过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文件保存到硬盘上,如下代码:

public class Client2 {
public static void main(String[] args) {
//参加该特点并设置为true
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
Person landlord = new Landlord();
Person proxy = (Person) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Person.class}, new RentHandler(landlord));
proxy.rent();
}
}

再次运转,奇特的一幕发生了,工程中多了一个类,没错,这便是JDK动态署理生成的署理类,由于我们的接口是public润饰,所以选用默许包名com.sun.proxy,类名以$Proxy最初,后边跟一个数字,和预期彻底符合。完美!


那么就让我们反编译一下这个class文件看看它的内容来一探终究......

下面是反编译得到的署理类的内容,

public final class $Proxy0 extends Proxy implements Person { ★
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws { ②
super(var1);
}
public final boolean equals(Object var1) throws { ④
return (Boolean) super.h.invoke(this, m1, new Object[]{var1});
}
public final void rent() throws { ③
super.h.invoke(this, m3, (Object[]) null);
}
public final String toString() throws { ④
return (String) super.h.invoke(this, m2, (Object[]) null);
}
public final int hashCode() throws { ④
return (Integer) super.h.invoke(this, m0, (Object[]) null);
}
static { ①
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("com.dujc.mybatis.proxy.Person").getMethod("rent");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
}
}

有几个重视点

  • 标示①的是一个静态代码块,当署理类一被加载,会马上初始化,用反射办法获取得到被署理的接口中办法和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的要害代码:

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler { ❶
private final Connection connection;
private Connectio下雨-「原创」让规划形式飞一瞬间|⑥面试必问署理形式nLogger(Connection conn, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.connection = conn; ❷
}
@Override
public Object invoke(Object proxy, Method method, Object[] params) ❸
throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
if ("prepareStatement".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("prepareCall".下雨-「原创」让规划形式飞一瞬间|⑥面试必问署理形式equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("createStatement".equals(method.getName())) {
Statement stmt = (Statement) method.invoke(connection, params);
stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else {
return method.invoke(connection, params);
}
}
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = Connection.clas下雨-「原创」让规划形式飞一瞬间|⑥面试必问署理形式s.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}
}

怎样样?是不是有种了解的感觉?

调查上面代码,能够得出以下几点定论:

  • ConnectionLogger完成了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的代码,
public 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;
}
@Override
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其它的办法都比较简略了,此处不再赘述。

好了,今日关于署理办法的技能共享就到此结束,下一篇我会持续共享另一个规划办法——适配器办法,一同讨论规划办法的奥妙。我们不见不散。


  • 今日的技能共享就共享到这儿,感谢您百忙抽出这么长期阅览我的文章。
  • 别的,我的笔记还有文章也会在我的GitHub上更新。
二维码