Java 代码审计 — 3. Dynamic Proxies

参考:

https://mp.weixin.qq.com/s/HtLjYHLAQQz83aoOI7D0ig

https://zhishihezi.net/b/5d644b6f81cbc9e40460fe7eea3c7925

简介

代理的概念

此处代理是一种设计思想,指的是对模块进行封装,向其添加额外的功能。也就是创建一个代理对象进行包装,用该代理对象取代原对象,在后续操作中,任何对原对象的调用都要先通过代理对象。

相当于在不修改原模块代码的基础上,对原模块的功能进行增强。

代理主要使用场景:

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

代理实现有两种类型,静态代理和动态代理。动态代理是静态代理的改进。它可以只使用一个类,一个方法,可以服务多个类的多个方法。

假想有两个类,学生类,汽车类。在不修改原代码的情况下,想添加所有方法的执行日志。

  • 如果采用基于继承方式的静态代理实现。就是新创建两个子类,分别继承、重写其父类的每个方法,为其添加日志输出功能。相当于每个类的每个方法都要重写。
  • 采用动态代理。那就只需要创建一个代理类,一个方法。可以用这个方法服务多个类的多个方法。

在实际中常用的是动态代理,动态代理也有许多实现,此处我们仅关注 jdk 提供的动态代理。

基于 JDK 的动态代理

相关接口和类

import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
package java.lang.reflect;

import java.lang.reflect.InvocationHandler;

public class Proxy implements java.io.Serializable {

    // 创建代理对象
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
    //ClassLoader loader:执行目标类对象的类加载器即可,用于加载目标类及其接口的代码
    //Class<?>[] interfaces:指定目标类对象的所有接口的Class对象的数组,通常使用目标类的Class对象调用getInterfaces()即可得到
	//InvocationHandler h:这个参数类型是一个接口,主要关注它里面唯一一个方法,invoke方法。它会在代理对象调用方法时执行。也就是说,在代理类对象中调用任何方法,都会执行到 invoke()方法。所以在该方法中完成对增强或扩展代码逻辑

	
	// 获取动态代理类
    public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) 
        
    
    // 返回代理对象绑定的调用处理程序
    public static InvocationHandler getInvocationHandler(Object proxy)
    
    //检测某个类是否是动态代理类
    public static boolean isProxyClass(Class<?> cl);

    /**
     * 向指定的类加载器中定义一个类对象
     *
     * @param loader 类加载器
     * @param name   类名
     * @param b      类字节码
     * @param off    截取开始位置
     * @param len    截取长度
     * @return JVM创建的类Class对象
     */
    private static native Class defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);

}
package java.lang.reflect;

import java.lang.reflect.Method;

/**
 * 每个代理实例都具有一个关联的调用处理程序。对代理实例调用方法时,将对方法调用进行编码并
 * 将其指派到它的调用处理程序的 invoke 方法。
 */
public interface InvocationHandler {

    // 在代理实例上处理方法调用并返回结果。在与方法关联的代理实例上调用方法时,将在调用处理程序上调用此方法。
	// proxy:就是代理类对象的一个引用,也就是 Proxy.newProxyInstrance的返回值,此引用几乎不会用到。
    // method:对应的触发invoke执行的方法的Method对象。
    // args   包含传入代理实例上方法调用的参数值的对象数组,如果接口方法不使用参数,则为 null。基本类型的参数被包装在适当基本包装器类(如 java.lang.Integer 或 java.lang.Boolean)的实例中。
	// 返回从代理实例的方法调用返回的值。
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

代码示例

接口

import java.io.Serializable;
public interface SayHelloInterface extends Serializable {
    public String sayHello(String name);
}

接口实现类

public class SayHelloImpl implements SayHelloInterface {

    @Override
    public String sayHello(String name) {
        String ret = "this is SayHelloImpl.sayHello method! Let`s say " + name;
        System.out.println(ret);
        return ret;
    }
}

代理对象类

package Proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class SayHelloInvocation implements InvocationHandler {
    private Object target;

    public SayHelloInvocation() {

    }

    public SayHelloInvocation(Object target) {
        this.target = target;
    }

    public Object bind(Object object) {
        this.target = object;
        return Proxy.newProxyInstance(
                object.getClass().getClassLoader(),
                object.getClass().getInterfaces(),
                this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 只对 sayHello 方法进行代理
        if (!("sayHello".equals(method.getName()))) {
            return method.invoke(target, args);
        }

        if(Proxy.getInvocationHandler(proxy) == this){
            System.out.println("proxy == this");
        }
        
        System.out.println("即将调用[ " + target.getClass().getName() + " ]类的[ " + method.getName() + " ]方法...");
        Object obj = method.invoke(target, args);
        System.out.println("已完成[ " + target.getClass().getName() + " ]类的[ " + method.getName() + " ]方法调用...");

        return obj;
    }
}

代码中调用动态代理类。

package Proxy;

import org.junit.Test;
import java.lang.reflect.Proxy;

public class TestProxy {
    @Test
    public void test1(){
        {
            SayHelloInterface sayHelloProxyInstance = (SayHelloInterface) Proxy.newProxyInstance(
                    SayHelloImpl.class.getClassLoader(),    // 指定动态代理类的类加载器
                    SayHelloImpl.class.getInterfaces(), // new Class[]{SayHelloInterface.class},   // 定义动态代理生成的类实现的接口
                    new SayHelloInvocation(new SayHelloImpl())   // 动态代理处理类
            );
            sayHelloProxyInstance.sayHello("liuyun");
        }

//        {
//            SayHelloInvocation sayHelloInvocation = new SayHelloInvocation();
//            SayHelloInterface sayHelloProxyInstance = (SayHelloInterface) sayHelloInvocation.bind(new SayHelloImpl());
//            sayHelloProxyInstance.sayHello("liuyun");
//        }

//        Object string = ((SayHelloInterface) ((new SayHelloInvocation()).bind(new SayHelloImpl()))).sayHello("liuyun");
    }

}

值得注意的是:

SayHelloInvocation 和 SayHelloImpl 两个类是并列关系。由于不能相互转化,所以要使用双方共同的接口来接收。

动态代理,实际上是在内存中生成了一个对象,该对象实现了指定的目标类对象拥有的接口。所以代理类对象和目标类对象是并列关系。不能相互转换,在后续使用 Spring 框架时,如果配置 JDK 的动态代理,一定要使用接口类型来接收代理类。

创建代理类实例的两种方法

我们可以使用 Proxy.newProxyInstance 方法直接创建动态代理类实例,也可以使用Proxy.getProxyClass() 获取代理类对象再通过反射的方式来创建。

下面我们以 com.anbai.sec.proxy.FileSystem 接口为例,演示如何创建其动态代理类实例。

// 创建UnixFileSystem类实例
FileSystem fileSystem = new UnixFileSystem();

// 使用JDK动态代理生成FileSystem动态代理类实例
FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance(
      FileSystem.class.getClassLoader(),// 指定动态代理类的类加载器
      new Class[]{FileSystem.class}, // 定义动态代理生成的类实现的接口
      new JDKInvocationHandler(fileSystem)// 动态代理处理类
);
// 创建UnixFileSystem类实例
FileSystem fileSystem = new UnixFileSystem();

// 创建动态代理处理类
InvocationHandler handler = new JDKInvocationHandler(fileSystem);

// 通过指定类加载器、类实现的接口数组生成一个动态代理类
Class proxyClass = Proxy.getProxyClass(
      FileSystem.class.getClassLoader(),// 指定动态代理类的类加载器
      new Class[]{FileSystem.class}// 定义动态代理生成的类实现的接口
);

// 使用反射获取Proxy类构造器并创建动态代理类实例
FileSystem proxyInstance = (FileSystem) proxyClass.getConstructor(
      new Class[]{InvocationHandler.class}).newInstance(new Object[]{handler}
);

生成 $ProxyXXX 类代码分析

在内存中生成代理对象的技术。整个代理过程在内存中进行,不需要手动写代理类的代码,也不存在代理类的编译过程,而是直接在 Java 运行期,凭空在 JVM 中生成一个代理类对象,供我们使用。

动态代理,实际上是在内存中生成了一个对象,该对象实现了指定的目标类对象拥有的接口。所以代理类对象和目标类对象是并列关系。

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

动态代理生成的com.sun.proxy.$Proxy0类代码:

copypackage com.sun.proxy.$Proxy0;

import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements FileSystem {

    private static Method m1;

  // 实现的FileSystem接口方法,如果FileSystem里面有多个方法那么在这个类中将从m3开始n个成员变量
    private static Method m3;

    private static Method m0;

    private static Method m2;

    public $Proxy0(InvocationHandler var1) {
        super(var1);
    }

    public final boolean equals(Object var1) {
        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[] list(File var1) {
        try {
            return (String[]) super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final int hashCode() {
        try {
            return (Integer) super.h.invoke(this, m0, (Object[]) null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() {
        try {
            return (String) super.h.invoke(this, m2, (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"));
            m3 = Class.forName("com.anbai.sec.proxy.FileSystem").getMethod("list", Class.forName("java.io.File"));
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

序列化问题

动态代理类符合 Java 对象序列化条件,并且在 序列化/反序列化时 会被ObjectInputStream/ObjectOutputStream特殊处理。

FileSystemProxySerializationTest 示例代码:

package com.anbai.sec.proxy;

import java.io.*;
import java.lang.reflect.Proxy;

/**
 * Creator: yz
 * Date: 2020/1/14
 */
public class FileSystemProxySerializationTest {

   public static void main(String[] args) {
      try {
         // 创建UnixFileSystem类实例
         FileSystem fileSystem = new UnixFileSystem();

         // 使用JDK动态代理生成FileSystem动态代理类实例
         FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance(
               FileSystem.class.getClassLoader(),// 指定动态代理类的类加载器
               new Class[]{FileSystem.class}, // 定义动态代理生成的类实现的接口
               new JDKInvocationHandler(fileSystem)// 动态代理处理类
         );

         ByteArrayOutputStream baos = new ByteArrayOutputStream();

         // 创建Java对象序列化输出流对象
         ObjectOutputStream out = new ObjectOutputStream(baos);

         // 序列化动态代理类
         out.writeObject(proxyInstance);
         out.flush();
         out.close();

         // 利用动态代理类生成的二进制数组创建二进制输入流对象用于反序列化操作
         ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());

         // 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
         ObjectInputStream in = new ObjectInputStream(bais);

         // 反序列化输入流数据为FileSystem对象
         FileSystem test = (FileSystem) in.readObject();

         System.out.println("反序列化类实例类名:" + test.getClass());
         System.out.println("反序列化类实例toString:" + test.toString());
      } catch (IOException e) {
         e.printStackTrace();
      } catch (ClassNotFoundException e) {
         e.printStackTrace();
      }

   }

}

程序执行结果:

反序列化类实例类名:class com.sun.proxy.$Proxy0
反序列化类实例toString:com.anbai.sec.proxy.UnixFileSystem@b07848

动态代理生成的类在反序列化/反序列化时不会序列化该类的成员变量,并且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

上一篇:centos配置 显示中文


下一篇:java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.