JDK动态代理原理:只能代理接口,不能代理类问题

 更新时间:2022年11月21日 09:47:11   作者:一位远方的诗人  
这篇文章主要介绍了JDK动态代理原理:只能代理接口,不能代理类问题。具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

动态代理是许多框架底层实现的基础,比如Spirng的AOP等,其实弄清楚了动态代理的实现原理,它就没那么神奇了,下面就来通过案例和分析JDK底层源码来揭秘她的神秘面纱,让她呈现在我们面前。

一、代理模式定义

存在一个代理对象,并且这个代理对象持有真实对象的引用,以实现对真实对象的访问控制。

1、举个例子,现在公司一般都有VPN,使我们在家也能访问到公司的内网(比如连接公司的数据库等),实现居家办公。这里VPN相当于一个代理,而公司内网相当于被代理对象,也就是真实对象。

我们不能直接访问公司内网(真实对象),但是我们通过VPN(代理对象),输入身份信息,确认无误后就可以访问到公司内网。

这就是代理模式的作用----控制对象的访问。 

代理模式的分类

1、静态代理

该代理对象持有被代理对象的引用,客户端通过调用代理对象的方法间接实现调用真实对象的方法。

代理对象可以在调用真实对象的方法前面加入一些操作:比如身份验证,如果身份验证没有通过,则不能访问真实对象的方法,否则可以调用真实对象的方法;也可以在调用真实对象方法后,加入一些操作,比如记录访问日志。

真实对象接口,提供两个服务方法

/**
 *
 * People 真实对象的接口包含两个方法
 *
 */
public interface People {
    voidsayHello(String msg);
 
    voidsayBye(String msg);
}
 
真实对象接口的具体实现
 
/**
 *
 *Student
 * 真实对象接口的实现
 */
public class Student implements People {
 
    @Override
    public void sayHello(String msg) {
      System.out.println("Hello "+msg);
    }
 
    @Override
    public void sayBye(String msg) {
        System.out.println("ByeBye "+msg);
    } 
}

代理对象:

/**
 *
 *StaticProxy
 * 代理对象,控制对真实对象的访问控制
 */
public class StaticProxy implements People {
    //真实对象,客户端不能直接访问
    private Peoplepeople;
   
    publicStaticProxy(){
        this.people=new Student();
    }
 
    @Override
    public void sayHello(String msg) {
        booleanfriendFlag=true;
        if(friendFlag){
            people.sayHello(msg);
        }
        System.out.println("记录访问日志");
      
    }
 
    @Override
    public void sayBye(String msg) {
        booleanfriendFlag=true;
        if(friendFlag){
            people.sayBye(msg);
        }
        System.out.println("记录访问日志");
    }
}

客户端调用及结果:

public class Test_Demo {
    public static void main(String[] args) {
        //创建静态代理对象
        StaticProxy proxy = new StaticProxy();
        //调用静态代理对象方法
        proxy.sayHello("nihao");
        proxy.sayBye("zaijian");       
    }
}

输出结果为:
nihao
记录访问日志
zaijian
记录访问日志

上面就是静态代理的一个实现,通过静态代理,实现了访问控制,但是在每个真实对象方法之前都加入了访问控制代码来验证权限。

如果有很多个方法,则要在每个方法调用前都加入验证权限的代码,这样非常的不灵活且有大量的重复代码,即使把验证权限抽象出来做过方法或者类,但是还是得在每个方法前加一段调用权限验证的代码,比如,一个客户端只用其中的一个方法,但是代理中两个方法都要加入权限控制,要满足其他客户端的调用需求,上面接口中只有两个方法还好,但是如果有上百个方法那岂不是很臃肿。

那么有什么办法解决了,那就是动态代理。 

2、动态代理

动态的生成代理类,而不用像静态代理一样,在编译期间进行定义类。动态代理更加灵活,不用显示的在所有方法前面或者后面加入权限验证、记录日志等操作。

动态代理的实现和静态代理一样,不同的是代理类的创建方式不同:

  • (1)静态代理是直接新增一个代理类;
  • (2)动态代理是通过JDK的Proxy和一个调用处理器InvocationHandler来实现的,通过Proxy来生成代理类实例,而这个代理实例通过调用处理器InvocationHandler接收不同的参数灵活调用真实对象的方法。

因此: 我们需要做的是创建调用处理器,该调用处理器必须实现JDK的InvocationHandler

1、 动态代理的实现如下:

(1)被代理接口:

public interface Car {
    public void run();
    public void laba(String str);
}

(2)被代理接口实现类:

public class BenChi implements Car {
    @Override
    public void run() {
        System.out.println("奔驰启动快");
    }
    @Override
    public void laba(String str) {
        System.out.println("过路口要减速鸣笛"+str);
    }
}

(3)代理类:

public class P_Class implements InvocationHandler {
    private Car car;
    public P_Class(Car car){
        this.car = car;
    }
    public Car createProxy(){
        Car car_proxy = (Car) Proxy.newProxyInstance(car.getClass().getClassLoader(), 
                car.getClass().getInterfaces(), this);
        return car_proxy;
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if("run".equals(method.getName())){
            System.out.println("开车不喝酒,喝酒不开车");
            method.invoke(car, args);
            System.out.println("安全伴我行");
            System.out.println(proxy.getClass().getName());            
        }
        return null;
    }
}

(4)测试类:

public class Test_Demo {
    public static void main(String[] args) {
        Car benchi = new BenChi();
        Car benchi_proxy = new P_Class(benchi).createProxy();
        benchi_proxy.run();
        benchi.laba("dididididi");    
    }
}

输出结果:
 
开车不喝酒,喝酒不开车
奔驰启动快
安全伴我行
com.sun.proxy.$Proxy0
过路口要减速鸣笛dididididi

2、Proxy动态生成一个代理实例源码分析:

/**
     * 
     * 通过Proxy动态生成一个代理实例
     *return:Object
     */
    public Object getProxy(){
        /*
         * Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

注意:loader类加载器用于加载代理类的字节码;interfaces为代理类需要实现的接口;h为代理对象实际调用的方法即invoke方法。
         * 第一个参数的作用就是 获取当前类的类加载器,作用是用来生成类的
         * 第二个参数是获取真实对象的所有接口    获取所有接口的目的是用来生成代理的,因为代理要实现所有的接口
         * 第三个参数是 调用处理器  这里传入调用处理器,是因为生成代理实例需要 调用处理器    为什么需要调用处理器,因为生成的代理不能直接调用真实对象的方法,而是通过调用处理器来调用真实对象的方法,具体就是通过上面定义的P_Class重写父类InvocationHandler的invoke方法
         */
       return  Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this );
    }
}

这样则实现了动态代理,客户端调用代理不同的方法,都实现了对真实对象的间接调用,并且经过了代理对象的权限验证。

但是我们只在一个地方加入了权限验证的代码,并没有在每个方法前面都加入,这样更加灵活和优雅。

但是我们重头到尾都没有看到像静态代理类那样的一个动态代理类,那么JDK的Proxy是怎么得到动态代理类的实例的呢?真的不建立一个类,就能获取该类的实例吗?

二、JDK动态原理详解

这是不可能的,Java中必须要有类,才会有该类的实例。其实不是没有代理类,而是JDK在运行期间帮我们生成了一个代理类的字节码,通过类加载器加载这个字节码,然后执行引擎进行一系列处理后生成代理类,再进行实例化。     

下面就来看JDK是怎么生成代理类并且实例化的:

核心代码就是:

Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this );

看看JDK的底层实现

贴出代码,去掉了异常和判断

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
      
     
        Class<?> cl = getProxyClass(loader, interfaces);
       
// Class[] constructorParams =
         { InvocationHandler.class };
 
            Constructor cons = cl.getConstructor(constructorParams);
            return cons.newInstance(new Object[] { h });
        } 

 1、生成代理类

通过源码可以知道,生成代理类是通过如下方法实现的:

Class<?> cl =getProxyClass(loader, interfaces);

2、生成代理类字节码

再追踪一下,这个方法里面内容很多,但是最关键的就是下面这个方法:

   byte[] proxyClassFile = ProxyGenerator.generateProxyClass(

                    proxyName, interfaces);

通过这个方法就生成了代理类的字节码,只不过调用完就不存在了。所以我们看不到它的源码。

注意:生成的动态代理类其实跟静态代理类还是有区别的,静态代理是我们直接控制真实对象的方法调用,而动态代理是通过调用处理器的invoke方法来调用真实对象的方法,而这个invoke方法就是我们自己覆写的方法。

可以看出是通过反射实现的,通过传入的不同的方法对象和参数来调用真实对象的不同方法。

刚开始我和网上很多人一样都有一个疑问,对于invoke方法发参数,Method和args在我们覆写的invoke方法中都有用到,但是对于第一个参数,代理对象proxy没有用,所以不知道这个东西调用处理器传给我们有什么用。

第一个参数proxy的作用:

(1)可以通过反射获取代理对象的信息,同时可以反复调用代理对象。

注意:this指向的是当前类,而不是代理类即$Proxy0。

proxy解释说明参考文档连接:http://stackoverflow.com/questions/22930195/understanding-proxy-arguments-of-the-invoke-method-of-java-lang-reflect-invoca

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java函数式编程(六):Optional

    Java函数式编程(六):Optional

    这篇文章主要介绍了Java函数式编程(六):Optional,本文是系列文章的第6篇,其它文章请参阅本文底部的相关文章,需要的朋友可以参考下
    2014-09-09
  • 详解Java利用实现对称加密(DES、3DES、AES)

    详解Java利用实现对称加密(DES、3DES、AES)

    本篇文章主要介绍了Java利用实现对称加密(DES、3DES、AES),具有一定的参考价值,有兴趣的可以了解一下。
    2017-01-01
  • java使用xstream实现xml文件和对象之间的相互转换

    java使用xstream实现xml文件和对象之间的相互转换

    xml是一个用途比较广泛的文件类型,在java里也自带解析xml的包,但是本文使用的是xstream来实现xml和对象之间的相互转换,xstream是一个第三方开源框架,使用起来比较方便,对java xml和对象转换相关知识感兴趣的朋友一起看看吧
    2023-09-09
  • JVM内存分配及String常用方法解析

    JVM内存分配及String常用方法解析

    这篇文章主要介绍了JVM内存分配及String常用方法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • Collections.shuffle()方法实例解析

    Collections.shuffle()方法实例解析

    这篇文章主要介绍了Collections.shuffle()方法实例解析,分享了相关代码示例,小编觉得还是挺不错的,具有一定借鉴价值,需要的朋友可以参考下
    2018-01-01
  • Mybatis-Plus时间范围查询方式详解

    Mybatis-Plus时间范围查询方式详解

    这篇文章主要介绍了Mybatis-Plus时间范围查询方式详解,通过两种方式结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-09-09
  • 基于Spring boot @Value 注解注入属性值的操作方法

    基于Spring boot @Value 注解注入属性值的操作方法

    这篇文章主要介绍了结合SpEL使用@Value-基于配置文件或非配置的文件的值注入-Spring Boot的相关知识,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-07-07
  • MyBatis学习教程(五)-实现关联表查询方法详解

    MyBatis学习教程(五)-实现关联表查询方法详解

    本文给大家介绍mybatis关联查询,包括一对一关联查询,一对多关联查询,代码简单易懂,感兴趣的朋友一起学习吧
    2016-05-05
  • Java实现用户不可重复登录功能

    Java实现用户不可重复登录功能

    这篇文章主要介绍了Java实现用户不可重复登录功能,非常不错,具有参考借鉴价值,需要的朋友参考下
    2016-12-12
  • 详解Java8 Collect收集Stream的方法

    详解Java8 Collect收集Stream的方法

    这篇文章主要介绍了Java8-Collect收集Stream的方法,提到了收集器的作用,连接收集器的方法,需要的朋友可以参考下
    2018-04-04

最新评论