Java 动态代理

Java的java.lang.reflect包下提供了一个Proxy类和InvocationHandler接口,可以生成JDK动态代理类或对象来完成程序无侵入式扩展(即不通过继承接口编写实现类来完成功能拓展)。

Java动态代理主要使用场景:

  1. 统计方法执行所耗时间。
  2. 在方法执行前后添加日志。
  3. 检测方法的参数或返回值。
  4. 方法访问权限控制。
  5. 方法Mock测试。

动态代理

Java的代理感觉是,给原来的操作前后加上了增强代码,像是类的包装。

静态代理通过编写一个代理类实现,如下面这个例子,ProxysaveLog类作为saveLog的代理类,两者都是实现了Log接口。

这里注意不要和装饰模式搞混:

静态代理在编译时已经确定代理的具体对象,装饰模式是在运行动态的构造

interface Log {void save(String name);
}
class saveLog implements Log{@Overridepublic void save(String log) {System.out.println("Good morning, " + log);}
}
public class ProxysaveLog implements Log {private Log proxied;// 不传入被代理的类,直接声明,所以一个静态代理对应一个类,这是与装饰者模式的主要差别。private ProxysaveLog(){this.proxied = new saveLog();}public static void main(String[] args) {Log log = new ProxysaveLog();log.save("log20220403233728-save");}public void save(String log) {System.out.println("Before invoke saveLog" );proxied.save(log);System.out.println("After invoke saveLog");}
}

静态代理需要手动为每一个目标类编写对应的代理类,而动态代理则直接得到代理类的Class对象,然后通过反射创造实例,从而避免了大量的重复劳动。

创建动态代理类会使用到java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。

java.lang.reflect.Proxy主要用于生成动态代理类Class、创建代理类实例,该类实现了java.io.Serializable接口。

Proxy类:

static Class<?> getProxyClass(ClassLoader loader, //指定代理类的类加载器Class<?>... interfaces  //要实现的代理类的接口列表。
)
//创建一个代理类所对应的Class对象。static Object newProxyInstance(ClassLoader loader,  //指定代理类的类加载器。 Class<?>[] interfaces,    //目标对象实现的接口的类型InvocationHandler h      //事件处理器
)
//返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。

InvocationHandler类:java.lang.reflect.InvocationHandler接口用于调用Proxy类生成的代理类方法。

 Object invoke(Object proxy, Method method, Object[] args)
// 在代理实例上处理方法调用并返回结果。

获取动态代理对象

由于是用反射生成动态代理,所以要想获取动态代理对象,先得获取一个动态代理类的Class对象,再用获取构造方法,最后调用newInstance方法去实例化。

public interface Foo{void foo();
}
public class DynamicAgent {public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {InvocationHandler handler = new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("正在执行的方法:"+method);return null;}};//生成一个动态代理类的Class对象Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(),Foo.class);//获取proxyClass类中带一个InvocationHandler参数的构造器Constructor constructor = proxyClass.getConstructor(InvocationHandler.class);//调用构造器的newInstance方法来创建实例Foo f = (Foo)constructor.newInstance(handler);System.out.println(f.getClass());f.foo();}
}

getProxyClass()方法从传入的接口的Class(被代理)中,“拷贝”类结构信息到一个新的带有构造器的Class对象中(即下图当中的class $Proxy0),就可以像正常的类一样创建对象了。

动态调试可见,constructor实际是获取的Proxy的构造方法,所以接口能够实例化就说得通了,不是凭空捏出个构造方法的。

protected InvocationHandler h;
protected Proxy(InvocationHandler h) {Objects.requireNonNull(h);this.h = h;
}

根据代理Class的构造器创建对象时,需要传入InvocationHandler。通过构造器传入一个引用,就是由这个成员变量h去接受的。

在newInstance方法被调用时,Proxy(InvocationHandler h)构造方法这里的变量h实际为DynamicAgent的一个内部匿名类DynamicAgent$1。因为InvocationHandler也是一个接口,其中的invoke方法是没有被实现的,所以在一开始就先new了一个InvocationHandler()给handler变量,并实现了invoke函数。

InvocationHandler是一个函数式接口(一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口)。

函数式接口可以被隐式转换为 lambda 表达式。

所以像下面这样写也是可以的。

InvocationHandler handler = (proxy, method, args1) -> {System.out.println("正在执行的方法:"+method);return null;
};

而一开始的写法是在Java8之前的Java 匿名类,就是一种语法糖,可以使代码更加简洁。

下面的示例将invoke的实现分开写到被代理类中

再去动态调试handler就是MyInvocationHandler的实例了。

实际当中getProxyClass这个方法并不常用,多使用newProxyInstance方法来实现动态代理,直接返回代理实例,连中间得到代理Class对象的过程都帮你隐藏:

动态生成了代理类 Class 的字节码byte[],然后通过defineClass0方法,如之前提到的ClassLoader当中方法名为defineClassXXXnative方法,动态的向JVM创建一个类对象

所以上面那一坨就可以变成一句代码了(虽然有点长)

Foo f = (Foo)Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class[]{Foo.class},
(proxy, method, args1) -> {System.out.println("正在执行的方法:"+method);
return null;
});

在执行代理对象的任意方法时,实际都是去执行InvocationHandler对象的invoke方法。

上面给出的实例主要是解释下动态代理的过程,离使用还有一些距离,invoke还没有返回值、动态代理实例不能动态生成(需要一个动态工厂类)。

通常在使用动态代理时会有一个或多个实现类如:

public class RunFoo implements Foo{@Overridepublic void foo() {System.out.println("execute Foo");}
}

还会有一个工具类,用于存放方法调用前后的增强方法:

public class FooUtil {//开始的增强方法public void before(){System.out.println("Before invoke saveLog");}//结束的增强方法public void after(){System.out.println("After invoke saveLog");}
}

通过动态代理工厂类实现动态代理对象的自动生成

public class MyProxyFactory {private static Object getProxy(final Object target) throws Exception {Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),(proxy1, method, args) -> {FooUtil util = new FooUtil();util.before();//通过反射以target为主调来执行method方法//回调了target对象的原有方法Object result = method.invoke(target, args);util.after();return result;});return proxy;}
}

用来测试的主程序为:

public class DynamicAgent {public static void main(String[] args) throws Exception {Foo target = new RunFoo();Foo Foo = (Foo)MyProxyFactory.getProxy(target);Foo.foo();}
}

动态代理可以灵活的实现解耦合,程序执行RunFoo当中的foo方法时候在前后插入了增强方法,但在RunFoo的方法中并没有硬编码调用before()和after()方法。

这种动态代理在AOP(面向切面编程)中被称为AOP代理,AOP代理可代替目标对象,其中包含了目标代理的所有方法,但可以在方法前后插入一些通用处理。

动态代理类生成的$ProxyXXX类代码分析

java.lang.reflect.Proxy类是通过创建一个新的Java类(类名为com.sun.proxy.$ProxyXXX)的方式来实现无侵入的类方法代理功能的。

动态代理生成出来的类有如下技术细节和特性:

  1. 动态代理的必须是接口类,通过动态生成一个接口实现类来代理接口的方法调用(反射机制)。
  2. 动态代理类会由java.lang.reflect.Proxy.ProxyClassFactory创建。
  3. ProxyClassFactory会调用sun.misc.ProxyGenerator类生成该类的字节码,并调用java.lang.reflect.Proxy.defineClass0()方法将该类注册到JVM
  4. 该类继承于java.lang.reflect.Proxy并实现了需要被代理的接口类,因为java.lang.reflect.Proxy类实现了java.io.Serializable接口,所以被代理的类支持**序列化/反序列化**。
  5. 该类实现了代理接口类(示例中的接口类是com.anbai.sec.proxy.FileSystem),会通过ProxyGenerator动态生成接口类(FileSystem)的所有方法,
  6. 该类因为实现了代理的接口类,所以当前类是代理的接口类的实例(proxyInstance instanceof FileSystemtrue),但不是代理接口类的实现类的实例(proxyInstance instanceof UnixFileSystemfalse)。
  7. 该类方法中包含了被代理的接口类的所有方法,通过调用动态代理处理类(InvocationHandler)的invoke方法获取方法执行结果。
  8. 该类代理的方式重写了java.lang.Object类的toStringhashCodeequals方法。
  9. 如果动过动态代理生成了多个动态代理类,新生成的类名中的0会自增,如com.sun.proxy.$Proxy0/$Proxy1/$Proxy2


图片来自:https://www.jianshu.com/p/9bcac608c714

https://qiankunli.github.io/2020/04/09/java_dynamic_proxy.html

动态代理类实例序列化问题

动态代理类符合Java对象序列化条件(class Proxy implements java.io.Serializable),并且在序列化/反序列化时会被ObjectInputStream/ObjectOutputStream特殊处理。

类对象和动态代理对象也得实现Serializable接口,所以把上面那个demo稍微改一下:

public class RunFoo implements Foo, Serializable {@Overridepublic void foo() {System.out.println("execute Foo");}
}

再把newProxyInstance方法那的InvocationHandler h参数单独写一个类去调用,不用匿名类或者lambda的写法了,因为要实现Serializable接口

public class MyInvocationHandler implements InvocationHandler, Serializable {private Object target;MyInvocationHandler(Object target){this.target = target;}public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {FooUtil util = new FooUtil();util.before();Object result = method.invoke(target, args);util.after();return result;}
}

测试动态代理类实例序列化的主程序为:

public class ProxySerializationTest {public static void main(String[] args) {try {Foo target = new RunFoo();Foo Foo = (Foo)Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),new MyInvocationHandler(target));ByteArrayOutputStream baos = new ByteArrayOutputStream();// 创建Java对象序列化输出流对象ObjectOutputStream out = new ObjectOutputStream(baos);// 序列化动态代理类out.writeObject(Foo);out.flush();out.close();// 利用动态代理类生成的二进制数组创建二进制输入流对象用于反序列化操作ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());// 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象ObjectInputStream in = new ObjectInputStream(bais);// 反序列化输入流数据为FileSystem对象Foo test = (Foo) in.readObject();System.out.println("反序列化类实例类名:" + test.getClass());System.out.println("反序列化类实例toString:" + test.toString());} catch (IOException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}}
}

动态代理生成的类在反序列化/反序列化时不会序列化该类的成员变量,并且serialVersionUID0L ,也将是说将该类的Class对象传递给java.io.ObjectStreamClass的静态lookup方法时,返回的ObjectStreamClass实例将具有以下特性:

  1. 调用其getSerialVersionUID方法将返回0L
  2. 调用其getFields方法将返回长度为零的数组。
  3. 调用其getField方法将返回null

但其父类(java.lang.reflect.Proxy)在序列化时不受影响,父类中的h变量(InvocationHandler)将会被序列化,这个h存储了动态代理类的处理类实例以及动态代理的接口类的实现类的实例。

动态代理生成的对象(com.sun.proxy.$ProxyXXX)序列化的时候会使用一个特殊的协议:TC_PROXYCLASSDESC(0x7D),这个常量在java.io.ObjectStreamConstants中定义的。在反序列化时也不会调用java.io.ObjectInputStream类的resolveClass方法而是调用resolveProxyClass方法来转换成类对象的。

详细描述请参考:Dynamic Proxy Classes-Serialization

动态代理类的实例序列化在Java反序列漏洞当中是相对重要的一环,比如CC1链中就有用到AnnotationInvocationHandler(JDK1.7及以下)

具体调用链分析在之前的初识Java反序列化漏洞这篇文章已经写过了,这里只是提一嘴。

Javaweb安全——Java动态代理相关推荐

  1. java动态代理【一】

    java动态代理的定义:为其他目标类的方法增加切面的逻辑,即在执行目标类方法的时候,先去执行一段如校验检测的逻辑代码.java通俗一点就是生成一个继承目标类的子类,并在每个调用方法都添加一段逻辑. 应 ...

  2. Java动态代理的实现

    动态代理作为代理模式的一种扩展形式,广泛应用于框架(尤其是基于AOP的框架)的设计与开发,本文将通过实例来讲解Java动态代理的实现过程. 友情提示:本文略有难度,读者需具备代理模式相关基础知识,. ...

  3. Java动态代理与Cglib代理

    为什么80%的码农都做不了架构师?>>>    最近又继续回来死磕Spring源码,以前看的也忘得差不多了,这次先把Spring使用的动态代理cglib看了一下,打好基础知识. cg ...

  4. JAVA 动态代理学习记录

    打算用JAVA实现一个简单的RPC框架,看完RPC参考代码之后,感觉RPC的实现主要用到了两个方面的JAVA知识:网络通信和动态代理.因此,先补补动态代理的知识.---多看看代码中写的注释 参考:Ja ...

  5. 初看Mybatis 源码 (二) Java动态代理类

    先抛出一个问题,用过Mybatis的都知道,我们只需要定义一个Dao的接口,在里面写上一些CRUD相关操作,然后配置一下sql映射文件,就可以达到调用接口中的方法,然后执行sql语句的效果,为什么呢? ...

  6. Java初学者如何迈出AOP第一步--使用Java 动态代理实现AOP

    Java初学者如何迈出AOP第一步--使用Java 动态代理实现AOP xBird 原创  (参与分:36,专家分:90)   发表:2004-9-3 上午9:37   版本:1.0   阅读:160 ...

  7. Java动态代理的应用

    先看一下代理模式,这个应该是设计模式中最简单的一个了,类图 代理模式最大的特点就是代理类和实际业务类实现同一个接口(或继承同一父类),代理对象持有一个实际对象的引用,外部调用时操作的是代理对象,而在代 ...

  8. java动态代理上是否能再进行一层代理

    CGLIB动态代理类 import java.lang.reflect.Method;import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.p ...

  9. Java 动态代理机制分析及扩展--转

    http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/#icomments http://www.ibm.com/developerworks/c ...

最新文章

  1. 关于过滤空格问题(未经测试)
  2. 分布式消息规范 OpenMessaging 1.0.0-preview 发布
  3. MFC获得主窗口和父窗口指针
  4. html卷轴展开,HTML文本框滚动代码4:卷轴变化应用
  5. 高性能滚动scroll(防抖和节流)
  6. 遍历进程并获取进程路径 - 回复 编程少年 的问题
  7. php tcpdf 没有头部,TCPDF - 设置头部Logo不显示问题
  8. wifi mesh测试软件,「可能是」最适合我的Mesh+WiFi6方案:Linksys Velop MX10600体验分享...
  9. Linux vmstat命令实战详解
  10. 灰色预测模型python实例_12-6:数学模型(灰色模型)与Python编程预测
  11. 怎么实现微信多公众号管理?
  12. 化工原理 --- 流体流动 2
  13. 打开一个英文文本文件,编写程序读取内容,将其中的小写字母转换为大写,大写字母转化为小写
  14. 【PAT甲级】1021 Deepest Root (25 分)(暴力,DFS)
  15. H5实现透明通道视频
  16. 高等教师资格证考试复习笔记-高等教育学-(6)-高等学校的教师和学生
  17. Python微信点赞
  18. 设置移动光猫外置路由器(上网+iptv正常使用)
  19. Failed to find config ‘085569ce-73ed-11df-83c3-002264764cea‘
  20. openwrt下有线网设置详细过程

热门文章

  1. F12 开发人员工具调试 HTML 和 CSS
  2. 魔方CFOP公式匹配算法
  3. php中如何显示blob,在PHP中显示(LONGBLOB)内容
  4. java打字游戏课程设计_java课程设计打字游戏
  5. Java图片流导出图片为黑屏,Matisse预览图片黑屏,Glide内存溢出
  6. java中SimpleDateFormat线程安全问题及解决方案
  7. 陈老师深度为你解析,单片机的运行原理
  8. [转]研究生能力自我培养手册
  9. oracle ERP凭证打印样式,Oracle ERP二次开发中特色鲜明的Web打印模式设计与实现
  10. 揭笔记本维修黑幕 浅谈我们该如何应对