Java的java.lang.reflect包下提供了一个Proxy
类和InvocationHandler
接口,可以生成JDK动态代理类或对象来完成程序无侵入式扩展(即不通过继承接口编写实现类来完成功能拓展)。
Java动态代理主要使用场景:
Mock
测试。Java的代理感觉是,给原来的操作前后加上了增强代码,像是类的包装。
静态代理
通过编写一个代理类实现,如下面这个例子,ProxysaveLog类作为saveLog的代理类,两者都是实现了Log接口。
这里注意不要和装饰模式搞混:
静态代理在编译时已经确定代理的具体对象,装饰模式是在运行动态的构造
interface Log {
void save(String name);
}
class saveLog implements Log{
@Override
public 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() {
@Override
public 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
当中方法名为defineClassXXX
的native
方法,动态的向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{
@Override
public 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代理可代替目标对象,其中包含了目标代理的所有方法,但可以在方法前后插入一些通用处理。
java.lang.reflect.Proxy
类是通过创建一个新的Java类(类名为com.sun.proxy.$ProxyXXX)
的方式来实现无侵入的类方法代理功能的。动态代理生成出来的类有如下技术细节和特性:
- 动态代理的必须是接口类,通过
动态生成一个接口实现类
来代理接口的方法调用(反射机制
)。- 动态代理类会由
java.lang.reflect.Proxy.ProxyClassFactory
创建。ProxyClassFactory
会调用sun.misc.ProxyGenerator
类生成该类的字节码,并调用java.lang.reflect.Proxy.defineClass0()
方法将该类注册到JVM
。- 该类继承于
java.lang.reflect.Proxy
并实现了需要被代理的接口类,因为java.lang.reflect.Proxy
类实现了java.io.Serializable
接口,所以被代理的类支持**序列化/反序列化
**。- 该类实现了代理接口类(示例中的接口类是
com.anbai.sec.proxy.FileSystem
),会通过ProxyGenerator
动态生成接口类(FileSystem
)的所有方法,- 该类因为实现了代理的接口类,所以当前类是代理的接口类的实例(
proxyInstance instanceof FileSystem
为true
),但不是代理接口类的实现类的实例(proxyInstance instanceof UnixFileSystem
为false
)。- 该类方法中包含了被代理的接口类的所有方法,通过调用动态代理处理类(
InvocationHandler
)的invoke
方法获取方法执行结果。- 该类代理的方式重写了
java.lang.Object
类的toString
、hashCode
、equals
方法。- 如果动过动态代理生成了多个动态代理类,新生成的类名中的
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 {
@Override
public 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();
}
}
}
动态代理生成的类在
反序列化/反序列化
时不会序列化该类的成员变量,并且serialVersionUID
为0L
,也将是说将该类的Class
对象传递给java.io.ObjectStreamClass
的静态lookup
方法时,返回的ObjectStreamClass
实例将具有以下特性:
- 调用其
getSerialVersionUID
方法将返回0L
。- 调用其
getFields
方法将返回长度为零的数组。- 调用其
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反序列化漏洞这篇文章已经写过了,这里只是提一嘴。