您现在的位置是:首页 > 正文

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

2024-02-01 03:59:17阅读 2

静态代理

什么是静态代理,假设现在有这样一个需求:要求在所有类的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 main(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

网站文章

  • 关于计算机和人物的英语短文,2016年12月英语四级作文范文50例:人脑和电脑

    关于计算机和人物的英语短文,2016年12月英语四级作文范文50例:人脑和电脑

    2016年12月英语四级作文范文50例:人脑和电脑Directions: Write a composition entitled The Brain and the Computer. Yousho...

    2024-02-01 03:58:47
  • 解决vue 路由子组件created和mounted不起作用问题

    解决vue 路由子组件created和mounted不起作用问题判断项目是否启用`keep-alive`启用未启用 判断项目是否启用keep-alive 启用 使用exclude排除组件(我没有成功不...

    2024-02-01 03:58:40
  • hybris笔记

    1.localextensions.xml的extension可以用dir定义也可以name定义,用name定义的话,编译的时候会去找extensioninfo.xml2.更改了items.xml的话,需要重新 ant  build initialize...

    2024-02-01 03:57:59
  • SVN命令使用详解

    标签: it分类: 服务器运维1、检出svn  co  http://路径(目录或文件的全路径) [本地目录全路径]  --username 用户名 --password 密码svn  co  svn://路径(目录或文件的全路径) [本地目录全路径]  --username 用户名 --password 密码svn  checkout 

    2024-02-01 03:57:52
  • WebSocket介绍和Socket的区别

    WebSocket介绍和Socket的区别

    WebSocket介绍与原理 WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助HTTP请求完成。 ——百...

    2024-02-01 03:57:48
  • vue 点击事件失效检查办法

    vue 点击事件失效检查办法

    看你一下你的元素控件的真实size是不是根本没有撑起来加上样式把界面撑起来检测一下margin之类的例如vue控件vue-seamless-scroll 实现列表滚动效果 但是做页面自动滚动的时候 如...

    2024-02-01 03:57:19
  • 电阻衰减网络计算(PI型和T型)

    电阻衰减网络计算(PI型和T型)

    以前比较懒,网上下载个计算器,手动将需要的值输进去,点下计算,结果就出来。从来没想过到底怎么算了,无意中看到一篇文章摘抄下来备用:若衰减器的电压衰减倍数 N = (U1/U2)和特征阻抗Zc决定,那么R1和R2可由下面公式计算得出:对于T型衰减器(上左):对于PI型衰减器(上右):下面举例说明:对于Zc=50oHm的阻抗网络中6dB PI型电阻衰减器的R

    2024-02-01 03:57:11
  • Flink使用(一) Streaming API处理有界流

    前提: 目前,Flink版本支持,批流处理使用一套API完成。 即,使用DataStreamAPI既能处理流数据,又能处理批数据(有界流)。 如何使用DataStream API处理数据,并且以BAT...

    2024-02-01 03:57:07
  • Python 字典(2)

    Python 字典(2)

    一、遍历字典一个字典可能会包含多个键-值对,字典可以以多种方式存储信息,因此有多种遍历字典的方式,比如键-值对、键、值。  1、遍历所有的键-值对  user_01 = {'username':'tizer','first_name':'joker','last_name':'pon',}  以上面的字典为例,遍历键-值对:  使用函数 items():...

    2024-02-01 03:56:38
  • element-ui的el-table,使用sortablejs改造成可拖拽排序

    element-ui的el-table,使用sortablejs改造成可拖拽排序

    element-ui的el-table,使用sortablejs改造成可拖拽排序。

    2024-02-01 03:56:31