首页 > 编程语言 >Java使用反射生成JDK动态代理

Java使用反射生成JDK动态代理

时间:2022-11-20 19:22:38浏览次数:73  
标签:反射 Java target JDK 对象 代理 InvocationHandler 动态 方法

Java使用反射生成JDK动态代理

1.* 使用反射生成JDK动态代理

在Java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过使用这个类和接口可以生成JDK动态代理类或动态代理对象。

1.*.& 使用Proxy和InvocationHandler创建动态代理

Proxy提供了用于创建动态代理类和代理对象的静态方法,它也是所有动态代理类的父类。如果在程序中为一个或多个接口动态地生成实现类,就可以使用Proxy来创建动态代理类;如果需要为一个或多个接口动态地创建实例,也可以使用Proxy来创建动态代理实例。

Proxy提供了如下两个方法来创建动态代理类和动态代理实例:

  • static Class<?> getProxyClass(ClassLoader loader, Class<?>...interfaces):创建一个动态代理类所对应的Class对象,该代理类将实现interfaces所指定的多个接口。第一个ClassLoader参数指定生成动态代理类的类加载器。
  • static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h):直接创建一个动态代理对象,该代理对象的实现类实现了interfaces指定的系列接口,执行代理对象的每个方法时都会被替换执行InvocationHandler对象的invoke方法。

实际上,即使采用第一个方法获取了一个动态代理类之后,当程序需要通过该代理类来创建对象时一样需要传入一个InvocationHandler对象。也就是说,系统生成的每个代理对象都有一个与之关联的InvocationHandler对象。

提示:计算机是很“蠢”的,当程序使用反射方式为指定接口生成系列动态代理对象时,这些动态代理对象的实现类实现了一个或多个接口。动态代理对象就需要实现一个或多个接口里定义的所有方法,但问题是:系统怎么知道如何实现这些方法?这个时候就轮到InvocationHandler对象登场了,当执行动态代理对象里的方法时,实际上会替换成调用InvocationHandler对象的invoke方法。

程序中可以采用先生成一个动态代理类,然后通过动态代理类来创建代理对象的方式生成一个动态代理对象。代码片段如下:

点击查看代码
//创建一个InvocationHandler对象
InvocationHandler handler = new MyInvocationHandler(...);
//使用Proxy生成一个动态代理类proxyClass
Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), new Class[] {Foo.class});
//获取proxyClass类中带一个InvocationHandler参数的构造器
Constructor ctor = proxyClass. getConstructor(new Class[] {InvocationHandler.class});
//调用ctor的newInstance方法来创建动态实例
Foo f = (Foo) ctor.newInstance(new Object[] {handler});

上面代码也可以简化成如下代码:

点击查看代码
//创建一个InvocationHandler对象
InvocationHandler handler = new MyInvocationHandler(...);
//使用Proxy直接生成一个动态代理对象
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class[] {Foo.class}, handler);

下面程序示范了使用Proxy和InvocationHandler来生成动态代理对象:

查看代码

上面程序首先提供了一个Person接口,该接口中包含了walk()和sayHello()两个抽象方法,接着定义了一个简单的InvocationHandler实现类,定义该实现类时需要重写invoke()方法,调用代理对象的所有方法时都会被替换成调用该invoke()方法。该invoke()方法中的三个参数解释如下:

  • proxy:代表动态代理对象
  • method:代表正在执行的方法
  • args:代表调用目标方法时传入的实参

上面程序中先是创建了一个InvocationHandler对象,然后根据InvocationHandler对象创建了一个动态代理对象。运行上面程序,会看到如图所示的运行效果:

image

从图可以看出,不管程序是执行代理对象的walk()方法,还是执行代理对象的sayHello()方法,实际上都是执行InvocationHandler对象的invoke()方法。

看完了上面的示例程序,可能会觉得这个程序没有太大的实用价值,难以理解Java动态代理的魅力。实际上,在普通编程过程中,确实无须使用动态代理,但在编写框架或底层基础代码时,动态代理的作用就非常大。

1.*.& 动态代理和AOP

根据前面介绍的Proxy和InvocationHandler,实在很难看出这种动态代理的优势。下面介绍一种更实用的动态代理机制。

只要我们开发一个实际使用的软件系统,就总会存在相同代码段重复出现的情况,在这种情况下,对于许多刚开始从事软件开发的人而言,他们的做法是:选中那些代码,一路“复制“、“粘贴”,立即实现了系统功能,如果仅仅从软件功能上来看,他们确实已经完成了软件开发。

通过这种“复制”、“粘贴”方式开发出来的软件如图所示:

image

采用图示结构实现的软件系统,在软件开发期间可能会觉得无所谓,但如果有一天需要修改程序的深色代码的实现,则意味着打开3份源代码进行修改。如果有100个地方甚至1000个地方使用了这段深色代码段,那么修改、维护这段代码的工作量将变成噩梦。

在这种情况下,大部分稍有经验的开发者都会将这段深色代码段定义成一个方法,然后让另外三段代码段直接调用该方法即可。在这种方式下,软件系统的结构如图:

image

对于如图所示的软件系统,如果需要修改深色部分的代码,则只要修改一个地方即可,而调用该方法的代码段,不管有多少个地方调用了该方法,都完全无须任何修改,只要被调用方法被修改了,所有调用该方法的地方就会自然改变,通过这种方式,大大降低了软件后期维护的复杂度。

但采用这种方式来实现代码复用依然产生一个重要问题:代码段1、代码段2、代码段3和深色代码段分离开了,但代码段1、代码段2和代码段3又和一个特定方法耦合了!最理想的效果是:代码块1、代码块2和代码块3既可以执行深色代码部分,又无须在程序中以硬编码方式直接调用深色代码的方法,这时就可以通过动态代理来达到这种效果。

由于JDK动态代理只能为接口创建动态代理,所以下面先提供一个Dog接口,该接口代码非常简单,仅仅在该接口里定义了两个方法:

点击查看代码
public interface Dog {
    //info方法声明
    void info();
    //run方法声明
    void run();
}

上面接口里只是简单地定义了两个方法,并未提供方法实现。如果我们直接使用Proxy为该接口创建动态代理对象,则动态代理对象的所有方法的执行效果又将完全一样。在这种情况下,我们先为该Dog接口提供一个简单的实现类GunDog,代码如下:

点击查看代码
public class GunDog implements Dog {
    //info方法实现,仅仅打印一个字符串
    public void info() {
        System.out.println("我是一只猎狗");
    }
    //run方法实现,仅仅打印一个字符串
    public void run() {
        System.out.println("我奔跑迅速");
    }
}

上面代码没有丝毫的特别之处,该Dog的实现类仅仅为每个方法提供了一个简单实现。回到开始我们需要实现的功能:让代码段1、代码段2和代码段3既可以执行深色代码部分,又无须在程序中以硬编码方式直接调用深色代码的方法。此时我们假设info()、run()两个方法代表代码段1、代码段2,那么要求:程序执行info()、run()方法时能调用某个通用方法,但又不想以硬编码方式调用该方法。

下面提供一个DogUtil类,该类里包含两个通用方法:

点击查看代码
public class DogUtil {
    //第一个拦截器方法
    public void method1() {
        System.out.println("=====模拟第一个通用方法=====");
    }
    //第二个拦截器方法
    public void method2() {
        System.out.println("=====模拟通用方法二=====");
    }
}

借助于Proxy和InvocationHandler就可以实现,当程序调用info()方法和run()方法时,系统可以“自动”将method1()和method2()两个通用方法插入info()和run()方法中执行。

这个程序的关键在于下面的MyInvokationHandler类,该类是一个InvocationHandler实现类,该实现类的invoke()方法将会作为代理对象的方法实现:

点击查看代码
public class MyInvokationHandler implements InvocationHandler {
    //需要被代理的对象
    private Object target;
    public void setTarget(Object target){
        this.target = target;
    }
    //执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
        DogUtil du = new DogUtil();
        //执行DogUtil对象中的method1方法
        du.method1();
        //以target作为主调来执行method方法
        Object result = method.invoke(target , args);
        //执行DogUtil对象中的method2方法
        du.method2();
        return result;
    }
}

上面程序实现invoke()方法时包含了一行关键代码,这行代码通过反射以target作为主调来执行method方法,这就是回调了target对象的原有方法。在该关键代码之前调用DogUtil对象的method1()方法,在该关键代码之后调用DogUtil对象的method2()方法。

下面再为程序提供一个MyProxyFactory类,该对象专为指定的target生成动态代理实例:

点击查看代码
public class MyProxyFactory {
    //为指定的target生成动态代理对象
    public static Object getProxy(Object target) throws Exception {
        //创建一个MyInvokationHandler对象
        MyInvokationHandler handler = new MyInvokationHandler();
        //为MyInvokationHandler设置target对象
        handler.setTarget(target);
        //创建并返回一个动态代理
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler);
    }
}

上面的动态代理工厂类提供了一个getProxy()方法,该方法为target对象生成一个动态代理对象,这个动态代理对象与target实现了相同的接口,所以具有相同的public方法,从这个意义上来看,动态代理对象可以当成target对象使用。当程序调用动态代理对象的指定方法时,实际上将变为执行MyInvokationHandler对象的invoke()方法。例如,调用动态代理对象的info()方法,程序将开始执行invoke()方法,其执行步骤如下:

  1. 创建DogUtil实例。
  2. 执行DogUtil实例的method1()方法。
  3. 使用反射以target作为调用者执行info()方法。
  4. 执行DogUtil实例的method2()方法。

看到上面的执行过程,可以发现:当使用动态代理对象来代替target对象时,代理对象的方法就实现了前面的要求,程序执行info()、run()方法时既能“插入”method1()、method2()通用方法,但GunDog的方法中又没有以硬编码方式调用method1()和method2()方法。

下面提供一个主程序来测试这种动态代理的效果:

点击查看代码
public class Test {
    public static void main(String[] args) throws Exception {
        //创建一个原始的GunDog对象,作为target
        Dog target = new GunDog();
        //以指定的target来创建动态代理对象
        Dog dog = (Dog) MyProxyFactory.getProxy(target);
        dog.info();
        dog.run();
    }
}

上面程序中的dog对象实际上是动态代理对象,只是该动态代理对象也实现了Dog接口,所以也可以当成Dog对象使用。程序执行dog的info()和run()方法时,实际上会先执行DogUtil的method1()方法,再执行target对象的info()和run()方法,最后执行DogUtil的method2()方法。运行上面程序,会看到如图所示的运行结果:

image

通过图示的运行结果来看,不难发现采用动态代理可以非常灵活地实现解耦。通常而言,当我们使用Proxy生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的实际意义。通常都是为指定的目标对象生成动态代理。

这种动态代理在AOP(Aspect Orient Programming,面向切面编程)中被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理。

AOP代理包含的方法与目标对象包含的方法示意图如图所示:

image

标签:反射,Java,target,JDK,对象,代理,InvocationHandler,动态,方法
From: https://www.cnblogs.com/hzhiping/p/16908293.html

相关文章

  • Java通过反射生成并操作对象
    Java通过反射生成并操作对象1.*使用反射生成并操作对象Class对象可以获得该类里的方法(由Method对象表示)、构造器(由Constructor对象表示)、Field(由Field对象表示),这3个类都......
  • Java通过反射查看类信息
    Java通过反射查看类信息1.*通过反射查看类信息Java程序中的许多对象在运行时都会出现两种类型:编译时类型和运行时类型,例如代码:“Personp=newStudent()”,这行代码将......
  • java——线程同步机制——解决线程安全问题——同步代码块
                                      解决线程安全问题——同步代码块卖票案例出现了线程安......
  • Linux下安装Java运行环境
    1.下载java8的包,并上传到服务器/usr/local目录下wget命令是一个从网络上下载文件的自由工具,它支持http协议,https协议和ftp协议。因此我们可以通过wget命令来下载JDK。wge......
  • java注解详解以及如何获取注解的上的信息
    目录一、Java自定义注解详解1.定义注解:2.元注解介绍@Target详细介绍@Relation详细介绍@Documented介绍@Inherited介绍3.注解可用的类型4.默认值限制5.创建一个简单的自定义......
  • Java中使用脚本引擎运行脚本语言
    在Java中运行脚本语言,例如JavaScript。步骤:1、创建脚本引擎管理器ScriptEngineManager2、从管理器中获取一个引擎ScriptEngine3、通过put(key,valu......
  • JAVA接口
    JDK1.8之前接口是接口,类是类。它们是同一层次的概念。接口中没有构造器。接口如何声明:interface在JDK1.8之前,接口中只有两部分内容,(1)常量:固定修饰符:publicstaticfina......
  • JavaWeb笔记
    1.JavaEE项目的三层架构web层                    com.atguigu.web/servlet/controllerservice层         ......
  • jenkins配置从节点后运行报错java.net.ConnectException: Connection timed out: conn
    修改jenkins配置中的ip系统管理-系统配置  修改ip与访问地址相同 ......
  • JDK源码分析实战系列-PriorityQueue
    完全二叉树一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则......