0x00 前言
JAVA安全系列文章主要为了回顾之前学过的java知识,构建自己的java知识体系,并实际地将java用起来,达到熟练掌握java编程,并能用java编写工具的目的。此系列文章需要读者具备一定java基础,不定时更新。相关详情可通过我的公众号文章进行查看:
本文是JAVA安全系列文章第九篇,学习代理模式与动态代理,重点应理解动态代理的代码实现逻辑。
0x01 代理模式与静态代理
一、代理模式的定义
代理模式也叫委托模式,是Java常见23种设计模式之一。所谓代理模式就是给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问,代理模式是一种结构型设计模式。
一个典型的代理模式通常需要三个角色:
(1)共同的接口或共同的继承类(Subject)
(2)真实对象(RealSubject)
(3)代理对象(ProxySubject)
代理模式图可这样表示:
上图中代理类与真正实现的类(委托类/被代理类)都是继承了共同的接口或共同的类,这样的好处在于代理类可以与实际的类有相同的方法,可以保证客户端使用的透明性。
代理其实就是一个中间人,生活中有很多代理模式的例子:房东将房子交给中介管理,我们要租房就得找中介;我们要打官司,需要委托律师来代理一切事宜;明星要出道需要一个经纪人来帮他代理......
二、代理模式实现原理
使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
三、代码实现
首先,我们得有一个接口(比如ClothFactory,衣服工厂),有两个类(被代理类,比如NikeClothFactory;代理类,比如ProxyClothFactory)实现了这个接口,想通过代理类的方法调用到被代理类的同名方法,那么代理对象得将被代理对象包装起来。
怎么通过代码实现包装呢?代理对象中应有一个属性为被代理类对象,通过构造器传入,且在代理对象的方法中调用了被代理对象的同名方法,这样就实现了通过代理来调用原始对象。具体代码如下:
//共同接口
interface ClothFactory{
void produceCloth();
}
//被代理类
class NikeClothFactory implements ClothFactory{
@Override
public void produceCloth() {
System.out.println("Nike工厂生产了一批运动服");
}
}
//代理类
class ProxyClothFactory implements ClothFactory{
private ClothFactory factory;//用被代理类对象进行实例化
public ProxyClothFactory(ClothFactory factory) {
this.factory = factory;
}
@Override
public void produceCloth() {
System.out.println("代理工厂做一些准备工作");
factory.produceCloth();
System.out.println("代理工厂做一些后续的收尾工作");
}
}
测试代码:
//测试类
public class StaticProxyTest {
public static void main(String[] args) {
//创建被代理类的对象
ClothFactory nikeClothFactory = new NikeClothFactory();
//创建代理类的对象
ClothFactory proxyClothFactory = new ProxyClothFactory(nikeClothFactory);
//通过代理对象对被代理对象进行访问
proxyClothFactory.produceCloth();
}
}
运行结果:
代理工厂做一些准备工作 Nike工厂生产了一批运动服 代理工厂做一些后续的收尾工作
四、代理模式的优点
从上面的代码我们可以看出,我们可以在不修改被代理类代码的情况下,通过扩展代理类进行功能的附加与增强。
五、静态代理特点
上面的代码中,代理类和目标对象的类(被代理类)都是在编译期间确定下来,这叫做静态代理。这种代理方式不好的地方在于不利于程序的扩展:
(1)假如被代理类中有多个方法,那么代理类中也要重复实现很多个方法,这就很繁琐;
(2)同时,每一个代理类只能为一个接口服务,假如有多个接口的实现类需要代理,这样一来程序开发中必然产生过多的代理。
那么能不能通过一个代理类完成全部的代理功能呢?有,那就是动态代理。
0x02 反射的应用—动态代理
一、定义
动态代理是指客户通过代理类调用其他对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。
动态创建类,不得不让我们联想到JAVA强大的反射机制了,如果忘了反射机制是什么,可以回看之前的文章。
二、JDK动态代理的实现逻辑(重点)
JDK中有一个类提供了动态创建代理类和实例的静态方法,这就是Proxy类,位于java.lang.reflect包中。还记得反射的话,对这个包一定不会陌生。
我们可以先想想,要想实现动态代理,我们需要解决什么问题?
问题一:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象?
答:通过反射来实现动态创建代理类及其对象。
我们来看看JDK中是怎么实现的,Proxy提供了一个newProxyInstance的静态方法来解决这个问题:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
我们可以通过注释和阅读关键代码来读懂这里面的逻辑:
newProxyInstance顾名思义是创建代理类实例,它接收三个参数,第一个参数是loader,定义代理类的类加载器;第二个参数是interfaces,代理类实现的接口列表;第三个参数是InvocationHandler h,调用处理器。该方法执行的逻辑是先将参数loader和interfaces传给getProxyClass0(loader, intfs)来查找或产生指定代理类的class对象,再通过反射API getConstructor(constructorParams)获取构造器对象,最后newInstance得到代理类实例,确切的说是返回由指定类加载器定义并实现指定接口的代理类的代理实例,且该实例中有指定的调用处理器。
这就是Proxy.newProxyInstance()通过反射来实现动态创建代理类及其对象的逻辑。
问题二:当通过代理类的对象调用方法时,如何动态的去调用被代理类的同名方法?
答:通过反射动态调用被代理类的同名方法。这就要说说上面没仔细说明的InvocationHandler h,调用处理器了,确切的说是调度方法时调用的调用处理函数,人话就是具体代理的逻辑。我们来看看这个InvocationHandler到底是什么:
public interface InvocationHandler {
/**
* Processes a method invocation on a proxy instance and returns
* the result. This method will be invoked on an invocation handler
* when a method is invoked on a proxy instance that it is
* associated with.
*
* @param proxy the proxy instance that the method was invoked on
*
* @param method the {@code Method} instance corresponding to
* the interface method invoked on the proxy instance.
* @param args an array of objects containing the values of the
* arguments passed in the method invocation on the proxy instance,
* or {@code null} if interface method takes no arguments.
*
* @return the value to return from the method invocation on the
* proxy instance.
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
它是一个接口,仅有一个invoke方法。invoke和Method这俩字眼一看就知道是反射调用。通过阅读注释,可以知道该接口是处理代理实例上的方法调用并返回结果。当在与之关联的代理实例上调用方法时,将在调用处理程序中调用此方法。 也就是说通过Proxy.newProxyInstance创建的代理实例都会有一个与之关联的调用处理器InvocationHandler,无论调用该代理实例的何种方法,都会调用到InvocationHandler.invoke()。
接下来看看传的参数吧。第一个参数proxy,调用方法的代理实例,就是通过Proxy.newProxyInstance创建的代理实例;第二个参数method,(反射)方法对象,方法名是与在代理实例上调用的接口方法对应的(被代理)实例方法,即被代理类的同名方法;第三个参数args,就是该方法的参数列表,若该方法没有参数则传进来的就是null。
回答了上面的两个问题,那整体的逻辑也就清楚了:通过传递定义代理类的类加载器、代理类实现的接口的class对象、调用处理器这三个参数给Proxy.newProxyInstance()动态获得指定类加载器定义并实现指定接口的含有指定调用处理器的代理类的代理实例,无论通过该代理实例调用何种方法,都会自动调用指定调用处理器的invoke()方法,我们只要将被代理类要执行的方法的功能声明在invoke中就可以了。
三、动态代理的代码实现
明白了上面的原理,代码实现就比较明朗了。
首先,我们声明一个接口(比如Human),写一个类(被代理类,比如SuperMan)实现此接口:
interface Human{
String getBelief();
void eat(String food);
}
class SuperMan implements Human{
@Override
public String getBelief() {
return "I believe I can fly!";
}
@Override
public void eat(String food) {
System.out.println("我喜欢吃" + food);
}
}
其次,由于创建代理实例需要传入调用处理器实例,写一个类实现InvocationHandler接口,由于该接口的invoke()要调用被代理对象的方法,故该实现类中需要有一个属性为被代理对象,可以通过构造器或写一个方法传入,此处通过bind()方法传入被代理对象:
class MyInvocationHandler implements InvocationHandler{
private Object obj; //需要使用被代理类的对象进行赋值
public void bind(Object obj){
this.obj = obj;
}
//当我们通过代理类的对象调用方法时,会自动调用invoke()方法
//将被代理类执行的方法的功能声明在invoke()中
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//method:即为代理类对象调用的方法,此方法也是被代理对象要调用的方法
//obj:被代理类的对象
Object returnValue = method.invoke(obj, args);
//上述方法的返回值作为当前类中invoke()的返回值
return returnValue;
}
}
想一想,此处的obj能不能写为Human类型?当然不能,因为我们想要的效果是能够代理任意类,写成Human就只能代理Human接口的实现类了,就不动态了,故应为Object类型。
然后,写一个类ProxyFactory,将动态获取代理实例的代码封装到静态方法getProxyInstance(Object obj)(obj为被代理对象)中:创建MyInvocationHandler实例,通过bind()方法将被代理对象传入,再通过Proxy.newProxyInstance获取代理实例。代码:
class ProxyFactory {
//调用此方法,返回一个代理类对象,解决问题一
public static Object getProxyInstance(Object obj){ //obj 被代理类的对象
MyInvocationHandler handler = new MyInvocationHandler();
handler.bind(obj);
return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),handler);
}
}
最后写一个测试类:
public class ProxyTest {
public static void main(String[] args) {
SuperMan superMan = new SuperMan();
//proxyInstance:代理类的对象
Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
//当通过代理类对象调用方法时,会自动调用被代理类中同名的方法
String belief = proxyInstance.getBelief();
System.out.println(belief);
proxyInstance.eat("四川麻辣烫");
}
}
运行结果:
I believe I can fly! 我喜欢吃四川麻辣烫
动态代理的动态性体现在哪呢?
第一,我们从代码中并找不到代理类的具体代码,因为它是在代码运行时动态生成的;
第二,通过动态代理,我们无需修改代码,只要传入任意一个被代理对象实例即可获取到代理实例并调用方法,比如再传一个NikeClothFactory对象,就可轻松实现代理:
此处是将代码进行了一下封装,不封装的话可以这样写:
public class ProxyTest {
public static void main(String[] args) {
SuperMan superMan = new SuperMan();
MyInvocationHandler handler = new MyInvocationHandler();
handler.bind(superMan);
Human proxyInstance = (Human)Proxy.newProxyInstance(superMan.getClass().getClassLoader(), superMan.getClass().getInterfaces(), handler);
String belief = proxyInstance.getBelief();
System.out.println(belief);
proxyInstance.eat("四川麻辣烫");
}
四、动态代理的使用场景和优点
使用场合:调试、远程方法调用,在Spring等框架中也有应用
从上面的分析和案例中,可以知道动态代理相比静态代理的优点:
抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,使得我们可以更加灵活和统一的处理众多的方法。
0x03 AOP与动态代理(扩展了解)
AOP(Aspect Orient Programming,面向切面编程)
假如有三个代码段中都有相同的代码,如下图所示:
这时为了简化代码,我们会将相同的代码封装到一个方法中,比如这样:
有这样一种情况,某几段代码段,前后的代码是固定一样的,中间的一段代码会调用不同方法,比如这样:
这时简化代码就需要用到动态代理。在动态代理中增加通用方法,中间变化的代码就进行动态调用。比如在0x02的代码中增加一个类,将通用方法写在该类中,修改动态代理增加通用方法,就形成了AOP编程:
class HumanUtil{
public void method1(){
System.out.println("=================通用方法一=================");
}
public void method2(){
System.out.println("=================通用方法二=================");
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
HumanUtil humanUtil = new HumanUtil();
humanUtil.method1();
//method:即为代理类对象调用的方法,此方法也是被代理对象要调用的方法
//obj:被代理类的对象
Object returnValue = method.invoke(obj, args);
humanUtil.method2();
//上述方法的返回值作为当前类中invoke()的返回值
return returnValue;
}
}
运行结果:
这就是面向切面编程,Spring框架中有用到,相信后面在学习Spring框架安全的时候还会再遇到,此处作为扩展了解即可。
0x04 提问与解答
这里我有两个疑问,第一,我们动态生成的代理类到底是长什么样的?第二,为什么调用代理对象的方法就会进入到handler的invoke()方法中,也没看到代码中哪里有写呀,只是注释中这样写,到底是怎么调用过去的?
本人技术水平有限,无法很好回答这个问题,我自己通过调试,做出以下回答:
我在Proxy.newProxyInstance()#getProxyClass0处下了断点,进行调试发现,我们获取到的cl为com.javasec.proxy.$Proxy0,获取到的构造器对象public com.javasec.proxy.$Proxy0(java.lang.reflect.InvocationHandler):
com.javasec.proxy为我的代码所在的包,也就是说在这个包下产生了一个$Proxy0的匿名类,那么这个匿名类又是咋生成出来的呢?就在Proxy的私有内部类ProxyClassFactory中,它仅有一个apply方法:
在apply()下个断点,再次追踪到这:
通过ProxyGenerator.generateProxyClass()方法产生字节码,再通过defineClass0()进行加载,这就到了产生代理类的关键地方了,跟进generateProxyClass():
public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
final byte[] var4 = var3.generateClassFile();
if (saveGeneratedFiles) {
...
}
private byte[] generateClassFile() {
this.addProxyMethod(hashCodeMethod, Object.class);
this.addProxyMethod(equalsMethod, Object.class);
this.addProxyMethod(toStringMethod, Object.class);
Class[] var1 = this.interfaces;
int var2 = var1.length;
...
return var13.toByteArray()
...
}
private void addProxyMethod(Method var1, Class<?> var2) {
String var3 = var1.getName();
Class[] var4 = var1.getParameterTypes();
Class var5 = var1.getReturnType();
Class[] var6 = var1.getExceptionTypes();
String var7 = var3 + getParameterDescriptors(var4);
Object var8 = (List)this.proxyMethods.get(var7);
..
}
ProxyGenerator在sun.misc包中,是class文件,无法进行调试。
代码逻辑不是我这种菜鸟理得清的,我放弃了(狗头),我是不是钻了牛角尖了(流泪)?从这些代码来看,saveGeneratedFiles默认是false,因为在我的磁盘上并没有产生一个Proxy0的class文件。产生的代理类很完善,有方法名,参数类型,返回类型,异常类型等。
其实没必要纠结产生的代理类究竟长啥样,也没必要纠结调用代理对象的方法时到底怎么就调用了handler.invoke()。可以大致理解,产生的代理类精髓应该是这样的:
class Proxy0 implements interfaceA {
private InvocationHandler handler;
public Proxy0(InvocationHandler handler) {
this.handler = handler;
}
public void method(Object[] args){
...
handler.invoke(Object proxy, Method method, Object[] args);
...
}
}
0x05 总结
1.代理模式就是给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问;
通过代理模式,可以在不修改真实对象代码的情况下,通过扩展代理类进行功能的附加与增强。
2.代理模式的实现原理:
使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
3.静态代理的代理类和目标对象的类(被代理类)都是在编译期间确定下来;
动态代理则是代理类的具体代码是在运行时动态生成的,它是反射的一种应用。
相比静态代理,动态代理优点是:
抽象角色中(接口)声明的所有方法都被转移到调用处理器invoke方法中处理,使得我们可以更加灵活和统一的处理众多的方法。
4.着重理解动态代理的实现逻辑:通过传递定义代理类的类加载器、代理类实现的接口的class对象、调用处理器这三个参数给Proxy.newProxyInstance()动态获得指定类加载器定义并实现指定接口的含有指定调用处理器的代理类的代理实例,无论通过该代理实例调用何种方法,都会自动调用指定调用处理器的invoke()方法,我们只要将被代理类要执行的方法的功能声明在invoke中就可以了。
5.个人认为,动态代理的实现逻辑对初学者来说还是比较难理解的。而网上很多写动态代理的文章基本都是给你讲讲Proxy.newProxyInstance()和InvocationHandler.invoke()这两个方法传递的参数及参数意义,就直接将代码扔出来了,让人看完还是觉得很懵逼,所以就有了本文。
我敢说,这篇文章是所有写动态代理文章中最详细的了,虽然可能有点钻牛角尖了。如果看完对动态代理还是感觉理解不到位的话,推荐看下尚硅谷讲动态代理的视频:
https://www.bilibili.com/video/BV1F741157LA/
p4-p7就是讲动态代理的,本文也是主要来源于此,我觉得讲得很清楚了。
6.欲知动态代理在反序列化中作用几何,且听下回分解~
Java安全系列文集
第6篇:JAVA安全|基础篇:反射机制之常见ReflectionAPI使用
第8篇:JAVA安全|Gadget篇:TransformedMap CC1链
如果喜欢小编的文章,记得多多转发,点赞+关注支持一下哦~,您的点赞和支持是我最大的动力~
原文始发于微信公众号(沃克学安全):JAVA安全|基础篇:反射的应用—动态代理
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论