Java Web安全之java基础-Java动态代理

Java Web安全之java基础-Java动态代理

Java 动态代理

Java反射提供了一种类动态代理机制,可以通过代理接口实现类来完成程序无侵入式扩展。

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

  1. 统计方法执行所耗时间。
  2. 在方法执行前后添加日志。
  3. 检测方法的参数或返回值。
  4. 方法访问权限控制。
  5. 方法Mock测试。
Java Web安全之java基础-Java动态代理

动态代理API

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

java.lang.reflect.Proxy类主要方法如下:

package java.lang.reflect;

import java.lang.reflect.InvocationHandler;

/**
 * Creator: yz
 * Date: 2020/1/15
 */
public class Proxy implements java.io.Serializable {

  // 省去成员变量和部分类方法...

    /**
     * 获取动态代理处理类对象
     *
     * @param proxy 返回调用处理程序的代理实例
     * @return 代理实例的调用处理程序
     * @throws IllegalArgumentException 如果参数不是一个代理实例
     */
    public static InvocationHandler getInvocationHandler(Object proxy)
            throws IllegalArgumentException {
        ...
    }

    /**
     * 创建动态代理类实例
     *
     * @param loader     指定动态代理类的类加载器
     * @param interfaces 指定动态代理类的类需要实现的接口数组
     * @param h          动态代理处理类
     * @return 返回动态代理生成的代理类实例
     * @throws IllegalArgumentException 不正确的参数异常
     */
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
            throws IllegalArgumentException {
        ...
    }

    /**
     * 创建动态代理类
     *
     * @param loader     定义代理类的类加载器
     * @param interfaces 代理类要实现的接口列表
     * @return 用指定的类加载器定义的代理类,它可以实现指定的接口
     */
    public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) {
        ...
    }

    /**
     * 检测某个类是否是动态代理类
     *
     * @param cl 要测试的类
     * @return 如该类为代理类,则为 true,否则为 false
     */
    public static boolean isProxyClass(Class<?> cl) {
        return java.lang.reflect.Proxy.class.isAssignableFrom(cl) && proxyClassCache.containsValue(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);

}

java.lang.reflect.InvocationHandler接口用于调用Proxy类生成的代理类方法,该类只有一个invoke方法。

java.lang.reflect.InvocationHandler接口代码(注释直接搬的JDK6中文版文档):

package java.lang.reflect;

import java.lang.reflect.Method;

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

    /**
     * 在代理实例上处理方法调用并返回结果。在与方法关联的代理实例上调用方法时,将在调用处理程序上调用此方法。
     *
     * @param proxy  在其上调用方法的代理实例
     * @param method 对应于在代理实例上调用的接口方法的 Method 实例。Method 对象的声明类将是在其中声明
     *               方法的接口,该接口可以是代理类赖以继承方法的代理接口的超接口。
     * @param args   包含传入代理实例上方法调用的参数值的对象数组,如果接口方法不使用参数,
     *               则为 null。基本类型的参数被包装在适当基本包装器类(如 java.lang.Integer
     *               或 java.lang.Boolean)的实例中。
     * @return 从代理实例的方法调用返回的值。如果接口方法的声明返回类型是基本类型,
     * 则此方法返回的值一定是相应基本包装对象类的实例;否则,它一定是可分配到声明返回类型的类型。
     * 如果此方法返回的值为 null 并且接口方法的返回类型是基本类型,则代理实例上的方法调用将抛出
     * NullPointerException。否则,如果此方法返回的值与上述接口方法的声明返回类型不兼容,
     * 则代理实例上的方法调用将抛出 ClassCastException。
     * @throws Throwable 从代理实例上的方法调用抛出的异常。该异常的类型必须可以分配到在接口方法的
     *                   throws 子句中声明的任一异常类型或未经检查的异常类型 java.lang.RuntimeException 或
     *                   java.lang.Error。如果此方法抛出经过检查的异常,该异常不可分配到在接口方法的 throws 子句中
     *                   声明的任一异常类型,代理实例的方法调用将抛出包含此方法曾抛出的异常的
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

}

使用java.lang.reflect.Proxy动态创建类对象

前面章节我们讲到了ClassLoaderUnsafe都有一个叫做defineClassXXXnative方法,我们可以通过调用这个native方法动态的向JVM创建一个类对象,而java.lang.reflect.Proxy类恰好也有这么一个native方法,所以我们也将可以通过调用java.lang.reflect.ProxydefineClass0方法实现动态创建类对象。

ProxyDefineClassTest示例代码:

package com.anbai.sec.proxy;

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

import static com.anbai.sec.classloader.TestClassLoader.TEST_CLASS_BYTES;
import static com.anbai.sec.classloader.TestClassLoader.TEST_CLASS_NAME;

/**
 * Creator: yz
 * Date: 2020/1/15
 */
public class ProxyDefineClassTest {

    public static void main(String[] args) {
        // 获取系统的类加载器,可以根据具体情况换成一个存在的类加载器
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();

        try {
            // 反射java.lang.reflect.Proxy类获取其中的defineClass0方法
            Method method = Proxy.class.getDeclaredMethod("defineClass0", new Class[]{
                    ClassLoader.class, String.class, byte[].class, int.class, int.class
            });

            // 修改方法的访问权限
            method.setAccessible(true);

            // 反射调用java.lang.reflect.Proxy.defineClass0()方法,动态向JVM注册
            // com.anbai.sec.classloader.TestHelloWorld类对象
            Class helloWorldClass = (Class) method.invoke(null, new Object[]{
                    classLoader, TEST_CLASS_NAME, TEST_CLASS_BYTES, 0, TEST_CLASS_BYTES.length
            });

            // 输出TestHelloWorld类对象
            System.out.println(helloWorldClass);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

程序运行结果:

class com.anbai.sec.classloader.TestHelloWorld

创建代理类实例

我们可以使用Proxy.newProxyInstance来创建动态代理类实例,或者使用Proxy.getProxyClass()获取代理类对象再反射的方式来创建,下面我们以com.anbai.sec.proxy.FileSystem接口为例,演示如何创建其动态代理类实例。

Proxy.newProxyInstance示例代码:

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

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

Proxy.getProxyClass反射示例代码:

// 创建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}
);

动态代理添加方法调用日志示例

假设我们有一个叫做FileSystem接口,UnixFileSystem类实现了FileSystem接口,我们可以使用JDK动态代理的方式给FileSystem的接口方法执行前后都添加日志输出。

com.anbai.sec.proxy.FileSystem示例代码:

package com.anbai.sec.proxy;

import java.io.File;
import java.io.Serializable;

/**
 * Creator: yz
 * Date: 2020/1/14
 */
public interface FileSystem extends Serializable {

    String[] list(File file);

}

com.anbai.sec.proxy.UnixFileSystem示例代码:

package com.anbai.sec.proxy;

import java.io.File;

/**
 * Creator: yz
 * Date: 2020/1/14
 */
public class UnixFileSystem implements FileSystem {

    /* -- Disk usage -- */
    public int spaceTotal = 996;

    @Override
    public String[] list(File file) {
        System.out.println("正在执行[" + this.getClass().getName() + "]类的list方法,参数:[" + file + "]");

        return file.list();
    }

}

com.anbai.sec.proxy.JDKInvocationHandler示例代码:

package com.anbai.sec.proxy;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * Creator: yz
 * Date: 2020/1/14
 */
public class JDKInvocationHandler implements InvocationHandler, Serializable {

    private final Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 为了不影响测试Demo的输出结果,这里忽略掉toString方法
        if ("toString".equals(method.getName())) {
            return method.invoke(target, args);
        }

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

        return obj;
    }

}

com.anbai.sec.proxy.FileSystemProxyTest示例代码:

package com.anbai.sec.proxy;

import java.io.File;
import java.lang.reflect.Proxy;
import java.util.Arrays;

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

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

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

        System.out.println("动态代理生成的类名:" + proxyInstance.getClass());
        System.out.println("----------------------------------------------------------------------------------------");
        System.out.println("动态代理生成的类名toString:" + proxyInstance.toString());
        System.out.println("----------------------------------------------------------------------------------------");

        // 使用动态代理的方式UnixFileSystem方法
        String[] files = proxyInstance.list(new File("."));

        System.out.println("----------------------------------------------------------------------------------------");
        System.out.println("UnixFileSystem.list方法执行结果:" + Arrays.toString(files));
        System.out.println("----------------------------------------------------------------------------------------");

        boolean isFileSystem     = proxyInstance instanceof FileSystem;
        boolean isUnixFileSystem = proxyInstance instanceof UnixFileSystem;

        System.out.println("动态代理类[" + proxyInstance.getClass() + "]是否是FileSystem类的实例:" + isFileSystem);
        System.out.println("----------------------------------------------------------------------------------------");
        System.out.println("动态代理类[" + proxyInstance.getClass() + "]是否是UnixFileSystem类的实例:" + isUnixFileSystem);
        System.out.println("----------------------------------------------------------------------------------------");
    }

}

程序执行结果:

动态代理生成的类名:class com.sun.proxy.$Proxy0
----------------------------------------------------------------------------------------
动态代理生成的类名toString:[email protected]
----------------------------------------------------------------------------------------
即将调用[com.anbai.sec.proxy.UnixFileSystem]类的[list]方法...
正在执行[com.anbai.sec.proxy.UnixFileSystem]类的list方法,参数:[.]
已完成[com.anbai.sec.proxy.UnixFileSystem]类的[list]方法调用...
----------------------------------------------------------------------------------------
UnixFileSystem.list方法执行结果:[javaweb-sec.iml, javaweb-sec-source, pom.xml, README.md, .gitignore, gitbook, .git, jni, .idea]
----------------------------------------------------------------------------------------
动态代理类[class com.sun.proxy.$Proxy0]是否是FileSystem类的实例:true
----------------------------------------------------------------------------------------
动态代理类[class com.sun.proxy.$Proxy0]是否是UnixFileSystem类的实例:false
----------------------------------------------------------------------------------------

动态代理类生成的$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

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

package 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:[email protected]

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

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。