作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
动态代理的使命有两个:
上一篇我们已经顺利通过接口得到代理Class对象,有了代理Class对象意味着代理对象唾手可得。
接下来,我们打算通过代理Class对象得到代理对象。
仔细观察之前的打印信息:
代理Class有一个构造器,需要传入InvocationHandler,虽然我们不知道这是啥,但可以试着传一下(JDK源码自带InvocationHandler):
public class ProxyTest {
public static void main(String[] args) throws Exception {
/*
* 参数1:类加载器,随便给一个
* 参数2:需要生成代理Class的接口,比如Calculator
* */
Class> calculatorProxyClazz = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class);
// 得到唯一的有参构造 $Proxy(InvocationHandler h),和反射的Method有点像,可以理解为得到对应的构造器执行器
Constructor> constructor = calculatorProxyClazz.getConstructor(InvocationHandler.class);
// 用构造器执行器执行构造方法,得到代理对象。构造器需要InvocationHandler入参
Calculator calculatorProxyImpl = (Calculator) constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return 10086;
}
});
// 看,有同名方法!
System.out.println(calculatorProxyImpl.add(1, 2));
}
}
完美,我们顺利得到了梦寐以求的代理对象。
但,InvocationHandler是干嘛的呢?从实验结果看,会发现每次调用代理对象的方法,最终都会调用InvocationHandler的invoke()方法:
怎么做到的呢?
上面不是说了吗,根据代理Class的构造器创建对象时,需要传入InvocationHandler。通过构造器传入一个引用,那么必然有个成员变量去接收。没错,代理对象的内部确实有个成员变量invocationHandler,而且代理对象的每个方法内部都会调用handler.invoke()!也就是说,动态代理为了实现代理对象和增强代码的解耦,把增强代码也抽取出去了,让InvocationHandler作为它与目标对象的桥梁。
JDK动态代理最终生成的其实是Class
注意,静态代理的做法是把目标对象传入代理对象,而动态代理则把增强代码传入代理对象。那么,目标对象怎么办,这样一来虽然能执行增强代码,但执行不到目标方法了!
别慌!来看看invoke()方法的参数有哪些:
为什么参数中没有目标对象呢?其实用大腿想想就能明白:JDK要也懵逼呀,你就传给我一个接口,我给你整出这么一堆东西已经仁至义尽,况且一个接口可以被多个类实现,我哪知道你将来要用这个InvocationHandler给哪个目标对象增强呀!所以JDK怎么可能提前预留目标对象的参数类型呢?
算了,知足吧,好歹有Method代理方法和args参数了,至于目标对象,咱自己解决吧:
至此,我们初步实现动态代理。
上面这种方式,太low了,不忍直视...改进一下:
public class ProxyTest {
public static void main(String[] args) throws Throwable {
CalculatorImpl target = new CalculatorImpl();
// 传入目标对象
Calculator calculatorProxy = (Calculator) getProxy(target);
calculatorProxy.add(1, 2);
}
/**
* 传入目标对象,获取代理对象
*
* @param target
* @return
* @throws Exception
*/
private static Object getProxy(final Object target) throws Exception {
// 参数1:随便找个类加载器给它 参数2:需要代理的接口
Class> proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
Constructor> constructor = proxyClazz.getConstructor(InvocationHandler.class);
return constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法开始执行...");
Object result = method.invoke(target, args);
System.out.println(result);
System.out.println(method.getName() + "方法执行结束...");
return result;
}
});
}
}
改进后的代码可读性大大增强,简单明了。
上面的代码还有问题:虽然传入任意对象我们都可以返回增强后的代理对象,但增强代码是写死的。如果我需要的增强不是打印日志而是其他操作呢?难道重新写一个getProxy()方法吗?所以,我们应该抽取InvocationHander,将增强代码和代理对象解耦(其实重写getProxy()和抽取InvocationHander本质相同,但后者细粒度小一些)。
public class ProxyTest {
public static void main(String[] args) throws Throwable {
// 1.得到目标对象
CalculatorImpl target = new CalculatorImpl();
// 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
// 3.传入目标对象+增强代码,得到代理对象
Calculator calculatorProxy = (Calculator) getProxy(target, logInvocationHandler);
calculatorProxy.add(1, 2);
}
/**
* 传入目标对象+增强代码,获取代理对象
*
* @param target
* @param handler
* @return
* @throws Exception
*/
private static Object getProxy(final Object target, final InvocationHandler handler) throws Exception {
// 参数1:随便找个类加载器给它 参数2:需要代理的接口
Class> proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
Constructor> constructor = proxyClazz.getConstructor(InvocationHandler.class);
return constructor.newInstance(handler);
}
/**
* 日志增强代码
*
* @param target
* @return
*/
private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
return new InvocationHandler() {
@Override
public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法开始执行...");
Object result = method.invoke(target, args);
System.out.println(result);
System.out.println(method.getName() + "方法执行结束...");
return result;
}
};
}
}
上面的代码抽取了两个方法,仔细观察你会发现,getLogInvocationHandler(target)的target参数是必要的,但getProxy(target, invocationHandler)的target参数是没必要的:
所以,代码最后可以优化成这样:
public class ProxyTest {
public static void main(String[] args) throws Throwable {
// 1.得到目标对象
CalculatorImpl target = new CalculatorImpl();
// 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
// 3.传入接口+增强对象(含目标对象),得到代理对象
Calculator calculatorProxy = (Calculator) getProxy(
logInvocationHandler, // 增强对象(包含 目标对象 + 增强代码)
target.getClass().getClassLoader(), // 随便传入一个类加载器
target.getClass().getInterfaces() // 需要代理的接口
);
calculatorProxy.add(1, 2);
}
/**
* 传入接口+增强(已经包含了目标对象),获取代理对象
*
* @param handler
* @param classLoader
* @param interfaces
* @return
* @throws Exception
*/
private static Object getProxy(final InvocationHandler handler, final ClassLoader classLoader, final Class>... interfaces) throws Exception {
// 参数1:随便找个类加载器给它 参数2:需要代理的接口
Class> proxyClazz = Proxy.getProxyClass(classLoader, interfaces);
Constructor> constructor = proxyClazz.getConstructor(InvocationHandler.class);
return constructor.newInstance(handler);
}
/**
* 日志增强代码
*
* @param target
* @return
*/
private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
return new InvocationHandler() {
@Override
public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法开始执行...");
Object result = method.invoke(target, args);
System.out.println(result);
System.out.println(method.getName() + "方法执行结束...");
return result;
}
};
}
}
目前为止,我们学习都是Proxy.getProxyClass():
为了简化代码,我们把这三步封装为getProxy()方法。然而,其实JDK已经提供了一步到位的方法Proxy.newProxyInstance(),你会发现它的参数和我们上面最终封装的getProxy()是一样的:
这说明什么?这说明我们的封装思路已经赶上JDK那帮老秃驴了!
废话不多说,把getProxy()删了,直接用Proxy.newProxyInstance()。
public class ProxyTest {
public static void main(String[] args) throws Throwable {
// 1.得到目标对象
CalculatorImpl target = new CalculatorImpl();
// 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
// 3.传入目标对象+增强代码,得到代理对象(直接用JDK的方法!!!)
Calculator calculatorProxy = (Calculator) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 随便传入一个类加载器
target.getClass().getInterfaces(), // 需要代理的接口
logInvocationHandler // 增强对象(包含 目标对象 + 增强代码)
);
calculatorProxy.add(1, 2);
}
/**
* 日志增强代码
*
* @param target
* @return
*/
private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
return new InvocationHandler() {
@Override
public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法开始执行...");
Object result = method.invoke(target, args);
System.out.println(result);
System.out.println(method.getName() + "方法执行结束...");
return result;
}
};
}
}
完美。
至此,我们又完成了动态代理的第二个目标:代码复用。不仅目标对象与增强代码解耦,代理对象也和增强代码解耦了。
如果你只是想学会动态代理,上面的内容足够了。但我相信,对于JDK生成的代理Class对象和最终生成的代理对象,大家都有点云里雾里,想刨根问底。
所以,这一小节我们一起来聊聊Proxy。
Proxy.getProxyClass()会先克隆接口信息得到新的Class对象,然后进行后续的一系列处理。
最终得到的Class对象其实是Class
部分同学可能会对实例类型的判断有点懵,这里再举一个例子:
myProxy既是AnyClass类型,又是Collection类型。extends相当于亲爹,只能有一个,而implements相当于认干爹,可以多个。但不论是哪种形式,都是对象的爹呀!
我想了个很骚的比喻,希望能解释清楚:
接口Class对象是大内太监,里面的方法和字段比做他的一身武艺,但是他没有XDD(构造器),所以不能new实例,一身武艺后继无人。
那怎么办呢?
正常途径(静态代理):
写一个类,实现该接口。这个就相当于大街上拉了一个人,认他做干爹。一身武艺传给他,只是比他干爹多了小DD,可以new实例。只要再传入目标对象,就能得到增强后的代理对象。
非正常途径(动态代理):
通过妙手圣医Proxy的克隆大法(Proxy.getProxyClass()),克隆一个Class,但是有小DD。所以这个克隆人Class可以创建实例,也就是代理对象。代理Class其实就是附有构造器的接口Class,一样的类结构信息,却能创建实例。
JDK根据接口生成的其实是Proxy的Class对象,然后根据ProxyClass得到proxy代理对象。proxy代理对象实现了接口,同时也是Proxy类型的。proxy对象的原理是:内部维护一个InvocationHandler,而InvocationHandler是对增强代码的抽象。通过抽取InvocationHandler,将代理对象和增强代码解耦。
但此时代理对象内部只有InvocationHandler,没有目标对象,如何最终调用目标方法呢?只需要把目标对象传给InvocationHandler就好啦。
public class ProxyTest {
public static void main(String[] args) throws Throwable {
// 1.得到目标对象
CalculatorImpl target = new CalculatorImpl();
// 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
// 3.传入目标对象+增强代码,得到代理对象(直接用JDK的方法!!!)
Calculator calculatorProxy = (Calculator) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 随便传入一个类加载器
target.getClass().getInterfaces(), // 需要代理的接口
logInvocationHandler // 增强对象(包含 目标对象 + 增强代码)
);
calculatorProxy.add(1, 2);
}
/**
* 日志增强代码
*
* @param target
* @return
*/
private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
return new InvocationHandler() {
@Override
public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法开始执行...");
Object result = method.invoke(target, args);
System.out.println(result);
System.out.println(method.getName() + "方法执行结束...");
return result;
}
};
}
}
其实就是调用链路拉长了,原本代理对象直接调用目标对象,现在是代理对象调InvocationHandler,InvocationHandler再调目标对象。Proxy代理对象内部有InvocationHandler对象,而InvocationHandler对象内部有我们塞进去的目标对象,所以最终通过代理对象可以调用到目标对象,并且得到了增强。所以,代理模式就是俄罗斯套娃...
最后我们还对代码做了优化,当然有些人可能还是觉得第一版好,哈哈。
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬