3-java安全基础——jdk动态代理

Posted songly_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3-java安全基础——jdk动态代理相关的知识,希望对你有一定的参考价值。

静态代理

什么是静态代理,假设现在有这样一个需求:要求在所有类的addUser方法前后添加打印日志。那么如何在不修改源代码的情况下完成需求?

通常做法是:为每一个目标类创建一个代理类,并让目标类和代理类实现同一个接口。在创建代理对象的时候通把目标对象传入构造中,然后在代理对象内部的方法中调用目标对象的方法前后输出日志。

静态代理实现:

//同一接口
interface UserService {
    public void addUser();
}

//目标类
class UserServiceImpl implements UserService{
    @Override
    public void addUser() {
        System.out.println("addUser()......");
    }
}

//代理类
class UserServiceProxy implements UserService{

    UserServiceImpl userService;

    public UserServiceProxy(UserServiceImpl userService){
        this.userService = userService;
    }

    @Override
public void addUser() {
    //调用前输出日志
        System.out.println("addUser()......before");
        userService.addUser();
        //调用后输出日志
        System.out.println("addUser()......after");
    }
}

public class ProxyTest {
    public static void main(String[] args) throws Exception{
        UserServiceImpl userService = new UserServiceImpl();
        UserServiceProxy userServiceProxy = new UserServiceProxy(userService);
        userServiceProxy.addUser();
    }
}

程序输出结果:

静态代理存在的问题:

      从上图可以看到,静态代理要求每一个目标类都要创建一个代理类并实现同一接口,这意味着如果有多个目标类的话就要创建多个代理对象,那么我们如何少写代理对象呢?

        复习一下对象创建过程:前面在学习反射的动态类加载时说过,当使用new操作创建一个对象时,jvm会通过ClassLoader类加载器把该类的.class文件加载到内存中并为之生成一个Class对象,并且当创建多个对象时,ClassLoader类加载器只会在第一次创建对象时加载.class文并创建Class对象(一个类只有一个Class对象),而Class对象是Class类的实例,也就是说Class类可以描述所有类。

动态代理

思考这样一个问题:能否不写代理类,直接获得代理Class对象,然后根据它创建代理实例?因为我们知道Class对象包含了一个类的完整结构信息(成员变量,成员方法,构造器等等),那么Class对象如何获取?

下面给出一位大佬的解决方案:

代理类和目标类理应实现同一组接口,之所以实现相同接口,是为了尽可能保证代理对象的内部结构和目标对象一致,这样我们对代理对象的操作最终都可以转移到目标对象身上,代理对象只需专注于增强代码的编写。所以,可以这样说:接口拥有代理对象和目标对象共同的类信息。所以,我们可以从接口那得到理应由代理类提供的信息。但是别忘了,接口是无法创建对象的,怎么办?

jdk动态代理提供了InvocationHandler接口和Proxy类来解决以上问题:

java.lang.reflect.InvocationHandler

java.lang.reflect.Proxy

Proxy类有一个getProxyClass静态方法,该函数定义如下:

 public static Class<?> getProxyClass(ClassLoader loader , Class<?>... interfaces)

getProxyClass方法可以获取代理类的Class对象,该方法要求传入一个类加载器和一组接口信息,返回一个代理Class对象。

参数loader:一般为接口的类加载器

参数interfaces:接口的Class对象

我们知道getProxyClass方法在返回代理Class对象之前,肯定要先获得接口的Class对象,getProxyClass方法会从你传入的接口Class中,“拷贝”类结构信息到一个新的Class对象中,但新的Class对象带有构造器,是可以创建对象的。

如上图所示,getProxyClass方法会根据参数loader 和参数interfaces得到接口Class对象(UserService.class),再根据接口Class对象创建一包含接口方法,还有构造器的代理Class对象,这样就可以通过代理Class对象来创建代理对象实例。

根据代理Class对象的构造器创建代理对象实例时需要传入InvocationHandler,因为在调用目标对象方法时,需要用到InvocationHandler(不要问为什么,先记住继续往下看)

Constructor<?> constructors = proxyInstance.getConstructor(InvocationHandler.class);

接下来我们就可以在invocationHandler中手动创建一个目标对象

    public static void main2(String[] args) throws Exception {
        /*
        参数1:接口的类加载器
        参数2:代理对象和目标类需要实现同一接口UserService的Class对象
        根据接口Class创建代理Class对象
         */
        Class<?> proxyInstance = Proxy.getProxyClass(UserService.class.getClassLoader() , UserService.class);
        //得到有参构造,需要传入InvocationHandler,通过InvocationHandler.invoke方法调用目标对象方法
        Constructor<?> constructors = proxyInstance.getConstructor(InvocationHandler.class);
        //代理Class创建代理对象实例
        UserService userServiceProxy = (UserService)constructors.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //创建目标对象
                UserServiceImpl userService = new UserServiceImpl();
                //打印日志
                System.out.println("addUser()......before");
                //invoke方法内部会调用目标实现类的addUser方法
                method.invoke(userService , args);
                //打印日志
                System.out.println("addUser()......after");
                return null;
            }
        });
        //调用目标方法
        userServiceProxy.addUser();
    }

代理对象调用目标方法的大概流程:

但在实际的使用中我们不会用getProxyClass方法,而是使用newProxyInstance方法:

public static Object newProxyInstance(ClassLoader loader , Class<?>[] interfaces , InvocationHandler h);

参数说明:

      loader :目标实现类的类加载器(这里的目标实现类指的就是被代理类)

      interfaces :目标实现类实现的所有接口

       h:需要传入一个invocationHandler

为什么需要参数h?和getProxyClass方法一样,因为代理对象需要借助invocationHandler的invoke方法,然后通过invoke方法可以调用被代理对象的方法。

newProxyInstance方法会根据以上三个参数返回一个Object类型的代理对象实例,然后需要把代理对象转换成UserService类型,这里大家先思考一下:

为什么要进行强制类型转换?

好了,现在我们直接来看下面的代码:

       从代码中可以看到,这里是可以不进行强转的话就无法调用被代理对象中的addUser方法,为什么是用不了addUser方法的方法?

原因在于类型不匹配,既然类型不匹配那转换成UserServiceImpl类型可以吗?可以看到程序抛出ClassCastException异常,类型依然不匹配。

再把代理对象转换成UserService接口类型,最终的动态代理代码:

        可以看到代理对象成功调用addUser方法,那么问题来了,代理对象userServiceProxy所属类型为什么是UserService类型?在前面的动态代理解决方案中有提到过,代理对象和目标类必须实现同一接口UserService,既然代理对象实现了UserService接口,那么就可以通过接口类型指向代理对象(多态的体现),这样自然就可以调用代理对象的addUser方法了。

通过一个示例程序来帮助理解:

      MyProxy类实现了UserService接口,userService是一个接口引用,可以看到在判断userService所属类型时无论MyProxy还是UserService都返回true,原因在于多态,但在运行阶段userService所属的对象类型实际上是MyProxy类的class对象类型。

再回到代理类$Proxy0,为了验证我们的推测,需要查看代理类$Proxy0的源代码。

一般来说代理类$Proxy0是由jvm底层自动生成,通常是看不到的,但要查看代理类$Proxy0的源码还是有办法的,不过需要借助一个反编译工具Luyten进行反编译代理类$Proxy0生成的class文件,得到的源代码如下

import com.test.*;
import java.lang.reflect.*;

public final class $Proxy0 extends Proxy implements UserService
{
    //实现或重写的方法
    private static Method m1;
    private static Method m2;
    private static Method m0;
    private static Method m3;
    
    //构造需要传入一个invocationHandler
public $Proxy0(final InvocationHandler invocationHandler) {
     //调用父类的构造
        super(invocationHandler);
    }
    
    public final boolean equals(final Object o) {
        try {
            return (boolean)super.h.invoke(this, $Proxy0.m1, new Object[] { o });
        }
        catch (Error | RuntimeException error) {
            throw;
        }
        catch (Throwable t) {
            throw new UndeclaredThrowableException(t);
        }
    }
    
    public final String toString() {
        try {
            return (String)super.h.invoke(this, $Proxy0.m2, null);
        }
        catch (Error | RuntimeException error) {
            throw;
        }
        catch (Throwable t) {
            throw new UndeclaredThrowableException(t);
        }
    }
    
    public final int hashCode() {
        try {
            return (int)super.h.invoke(this, $Proxy0.m0, null);
        }
        catch (Error | RuntimeException error) {
            throw;
        }
        catch (Throwable t) {
            throw new UndeclaredThrowableException(t);
        }
    }
    //重写接口的方法
    public final void addUser() {
        try {
            //调用invoke方法
            super.h.invoke(this, $Proxy0.m3, null);
        }
        catch (Error | RuntimeException error) {
            throw;
        }
        catch (Throwable t) {
            throw new UndeclaredThrowableException(t);
        }
    }
    
     //静态代码块
    static {
        try {
            $Proxy0.m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            $Proxy0.m2 = Class.forName("java.lang.Object").getMethod("toString", (Class<?>[])new Class[0]);
            $Proxy0.m0 = Class.forName("java.lang.Object").getMethod("hashCode", (Class<?>[])new Class[0]);
            //通过反射获取接口的方法
            $Proxy0.m3 = Class.forName("com.test.UserService").getMethod("addUser", (Class<?>[])new Class[0]);
        }
        catch (NoSuchMethodException ex) {
            throw new NoSuchMethodError(ex.getMessage());
        }
        catch (ClassNotFoundException ex2) {
            throw new NoClassDefFoundError(ex2.getMessage());
        }
    }
}

        从源码中可以看到,jvm底层生成的代理类$Proxy0继承了Proxy类,实现了UserService接口并重写了接口的所有方法,并且代理类$Proxy0的构造要求传入一个invocationHandler,实际上是调用了Proxy类中的invocationHandler。

      代理类$Proxy0把重写的父类方法或实现的接口方法封装成了成员属性Method,并且每一个方法内部都调用了invocationHandler的invoke方法(应该明白一点:代理类$Proxy0会为UserService中的每一个方法都调用invoke方法)。这里我们只分析addUser方法,发现该方法内部也调用了invocationHandler的invoke方法,传了两个参数,一个是代理类$Proxy0当前对象,另一个是成员属性Method指向的方法(Method里的方法是通过反射获取的)。InvocationHandler是一个接口,因此会调用该接口实现类的invoke方法,这样invocationHandler的invoke方法就可以通过method.invoke方法(通过反射调用目标对象方法)来调用目标对象的addUser方法了。

动态代理流程梳理:

总结:

动态代理和静态代理的区别在于:静态代理中的代理对象是手动创建的,而动态代理中的代理对象是底层自动生成的。

参考资料:

https://www.zhihu.com/question/20794107

以上是关于3-java安全基础——jdk动态代理的主要内容,如果未能解决你的问题,请参考以下文章

Spring AOP基础之JDK动态代理

java开发必学知识:动态代理

Java基础干货动态代理核心代码

Spring 动态代理基础知识

深入浅出JDK动态代理

Java中的JDK动态代理