设计形式之署理形式-从静态署理到动态署理
署理形式看似离咱们的开发比较远,不像战略形式、工厂形式那么的直观,可是其实在咱们日常开发的许多场景都用到了署理形式,比如:Spring AOP使用了JDK动态署理 和Cglib动态署理 ;Mybatis的插件使用了JDK动态署理 ;
署理形式的优点
见名之意,署理形式的中心便是「署理方针」,这个署理方针相当于一个中介,而咱们被署理的方针叫做「方针方针」,你可以把它想象成委托人。中介起到的效果便是隔离了委托人和客户 (说不定委托人不想和客户碰头-安全性),让他们免去了繁琐的流程 (比如租房吧,租客和房东不需求碰头,他们只需求和房屋中介进行对接就行),这在代码中呢,就叫做解除了「调用方」和「方针方针」的的耦合度。
综上,署理形式的优点如下:
- 经过署理方针将真正的方针方针隔离起来,起到了必定的保护效果
- 解除了调用方和被调用方的直接联络,必定程度上降低了体系的耦合度
- 署理方针可以做方针方针供给功用外的一些工作,达到对方针功用的增强效果
在开发中,最常遇见的三种署理形式:静态署理、JDK动态署理、CGLIB动态署理。
静态署理
静态署理的角色:1、功用接口;2、被署理类;3、署理类
还是拿租房的比如来写对应的代码:
功用接口-把房子租出去
public interface RentOutHouse {
void rentOut();
}
被署理方-委托方,房东
public class Landlord implements RentOutHouse{
@Override
public void rentOut() {
// 中心功用-租房子出去
System.out.println("我是房东,我有一个房子需求租出去");
}
}
署理方针-中介
public class HouseProxy implements RentOutHouse{
// 中介要和房东直接接触
private Landlord landlord;
public HouseProxy(Landlord landlord) {
this.landlord = landlord;
}
@Override
public void rentOut() {
// 中心功用前的功用增强
System.out.println("我是中介,租房之前和房东签合同");
System.out.println("我是中介,租房之前和租房者签合同");
// 中心功用
landlord.rentOut();
// 中心功用后的增强
System.out.println("租出去了之后,我要多收取一些中介费");
}
客户端-租客
public class Client {
public static void process() {
HouseProxy houseProxy = new HouseProxy(new Landlord());
houseProxy.rentOut();
}
public static void main(String[] args) {
process();
}
}
成果
我是中介,租房之前和房东签合同
我是中介,租房之前和租房者签合同
我是房东,我有一个房子需求租出去
租出去了之后,我要多收取一些中介费
静态署理的缺点:
1、每个被署理类都需求一个署理类与之对应,由于署理类中是要包含这个被署理类方针的,所以假如再来一个房东需求将自己的房子租出去,还需求创建一个新的中介去和这个新房东对接,而其实中介对于两个房东要做的增强工作是相同的,所以会形成冗余。
2、 再有,假如我房东现在是把房子租出去了,假如我想撤销租借呢,这时候就需求顶层的那个接口再加一个撤销租借的抽象办法,这时候就芭比Q了,由于你一切的房东和房屋中介都需求去重写这个办法,hhh。
JDK动态署理
JDK动态署理运用了反射的技术,很好的处理了上述静态署理的缺乏,先看看代码和履行的成果:
功用接口-把房子租出去
public interface RentOutHouse {
void rentOut();
}
被署理方-委托方,房东
public class Landlord implements RentOutHouse{
@Override
public void rentOut() {
// 中心功用-租房子出去
System.out.println("我是房东,我有一个房子需求租出去");
}
}
办法阻拦器-InvocationHandler
public class MyInvocationHandler implements InvocationHandler {
private Object object;
public MyInvocationHandler(Object object) {
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 中心功用前的功用增强
System.out.println("我是中介,租房之前和房东签合同");
System.out.println("我是中介,租房之前和租房者签合同");
// 中心功用办法调用
Object invoke = method.invoke(object, args);
// 中心功用后的增强
System.out.println("租出去了之后,我要多收取一些中介费");
return invoke;
}
}
客户端-租客
public class Client {
public static void main(String[] args) {
RentOutHouse landlord = new Landlord();
MyInvocationHandler myInvocationHandler = new MyInvocationHandler(landlord);
RentOutHouse proxy = (RentOutHouse)Proxy.newProxyInstance(Landlord.class.getClassLoader(), Landlord.class.getInterfaces(), myInvocationHandler);
proxy.rentOut();
}
}
留意,这儿的Proxy.newProxyInstance(Landlord.class.getClassLoader(), Landlord.class.getInterfaces(), myInvocationHandler);
是用来生成署理方针的,三个参数分别是:1、类加载器;2、类的悉数接口;3、自定义的办法阻拦器;
InvocationHandler
是办法阻拦器,它会将被署理类的办法进行阻拦,并加上自己的一些增强,中心便是它里边的invoke()
办法。
我这儿写的InvocationHandler
比较简单,其实可以在它里边的invoke()
办法中做一些复杂的判别,比如 "rentout".equals(method.getName())
来指定只有办法名为rentout
时我才去进行增强。
这时你会说,不是说用到了反射么,在哪里?别急,先看看咱们实际生成的署理方针到底是个什么东东:
在租客类中打个断点:
看署理类的类型:@Proxy0@513
,这个类你是找不到的,咱们来把它反编译出来,你就会知道反射用在哪里了~~~
/**
* 生成字节码
*/
private static void saveProxyClass(String path) {
byte[] $proxy0s = ProxyGenerator.generateProxyClass("$Proxy0", Landlord.class.getInterfaces());
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(new File(path + "$Proxy0.class"));
fileOutputStream.write($proxy0s);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
try {
fileOutputStream.flush();
fileOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
看看生成的class文件:
public final class $Proxy0 extends Proxy implements RentOutHouse {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void rentOut() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("com.hss.spring.jdkdynamic.RentOutHouse").getMethod("rentOut");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
像equals、hashCode那几个办法不看,咱们就看咱们署理的办法rentOut()
,你会看到,当咱们调用署理方针的rentOut()
办法的时候,它内部是走了super.h.invoke(this, m3, (Object[])null);
,这儿,super
是指父类,很简单看到是Proxy
类,super.h又是什么?跟进去看看:
可以看到,h其实便是咱们在Client里边经过 Proxy.newProxyInstance()
生成署理方针时传入的自定义的 InvocationHandler
!!!
知道了h是什么,再来看看这儿的m3是什么:
哈哈,这儿便是经过反射拿到咱们接口的办法rentOut()
,这样就把这个署理类方针搞懂了,最后一步,便是走到了InvocationHandler
的invoke()
办法中:
这便是JDK动态署理的悉数流程了,需求留意的是:JDK动态署理要求被署理的类必须完成一个接口。
那么问题来了:为什么JDK动态署理要求被署理的类要完成接口?
答案:首先,要生成署理方针,是必须去知道被署理的类有哪些办法的,可以让被署理类完成接口,或许署理类去承继被署理类,这样署理类就可以得到被署理类中的一切办法。而JDK动态署理之所以需求被署理类完成接口,咱们可以从反编译的class文件中看到,署理类方针现已承继了Proxy
类,java它又是单承继的,所以说就要求被署理类去完成接口,让咱们可以成功的生成署理类。
CGLIB动态署理
CGLIB动态署理和JDK动态署理的本质差异便是:CGLIB不需求被署理类去完成接口(实不完成接口都可以)。
先引入cglib依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.7</version>
</dependency>
被署理方-委托方,房东
public class Landlord {
@Override
public void rentOut() {
// 中心功用-租房子出去
System.out.println("我是房东,我有一个房子需求租出去");
}
}
回调-Interceptor
public class MyInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 中心功用前的功用增强
System.out.println("我是中介,租房之前和房东签合同");
System.out.println("我是中介,租房之前和租房者签合同");
Object obj = methodProxy.invokeSuper(o, objects);
System.out.println("租出去了之后,我要多收取一些中介费");
return obj;
}
}
客户端-租客
public class Client {
public static void main(String[] args) {
Landlord landlord = new Landlord();
// 署理类class文件存入本地磁盘便利咱们反编译查看源码
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./p");
// 经过CGLIB获取署理方针
Enhancer enhancer = new Enhancer();
// 设置署理方针的父类
enhancer.setSuperclass(Landlord.class);
// 设置回调
enhancer.setCallback(new MyInterceptor());
Landlord landlordProxy = (Landlord) enhancer.create();
// 经过署理方针调用方针办法
landlordProxy.rentOut();
}
}
可以看到全体的代码和JDK动态署理十分的类似,差异便是被署理类不做完成接口的要求,Interceptor替换了InvocationHandler,然后在Client中有部分的代码不相同。
中心办法和增强办法的逻辑都是写在Interceptor中的。
这时候你会问,不是说根据承继么,怎么体现的?
看看咱们生成的class文件(太多了,只看中心的部分):
内部调用了Interceptor的intercept()办法。
over!!!