JAVA动态代理

栏目: Java · 发布时间: 6年前

内容简介:动态代理的作用其实就是在不修改原代码的前提下,对已有的方法进行增强。关键点:我们用一个很简单的例子来说明:

文档更新说明

  • 2018年09月24日 v1.0 初稿

    代理在生活中很常见,比如说婚介网站,其实就是找对象的代理;还有社保代理、人事代理;还有找黄牛抢票,其实也是一种代理;而这些代理,在 JAVA 中也是有对应实现的。

1、为什么要动态代理

动态代理的作用其实就是在不修改原代码的前提下,对已有的方法进行增强。

关键点:

  • 不修改原来已有的代码(满足 设计模式 的要求)
  • 对已有方法进行增强

2、举个栗子

我们用一个很简单的例子来说明: Hello 类,有一个 introduction 方法。

现在我们的需求就是不修改 Hello 类的 introduction 方法,在 introduction 之前先 sayHello ,在 introduction 之后再 sayGoodBye

3、实现方式

JAVA中,实现动态代理有两种方式,一种是JDK提供的,一种是第三方库 CgLib 提供的。特点如下:

CgLib

3.1、JDK动态代理

JDK动态代理需要实现接口,然后通过对接口方法的增强来实现动态代理

所以要使用JDK动态代理的话,我们首先要创建一个接口,并且被代理的方法要在这个接口里面

3.1.1、创建一个接口

我们创建一个接口如下:

Personal.java

public interface Personal {

    /**
     * 被代理的方法
     */
    void introduction();

}

3.1.2、实现接口

创建接口实现类,并且完成 introduction 方法

PersonalImpl.java

public class PersonalImpl implements Personal {
    @Override
    public void introduction() {
        System.out.println("我是程序员!");
    }
}

3.1.3、创建代理类

JDK代理的关键就是这个代理类了,需要实现 InvocationHandler

在代理类中,所有方法的调用都好分发到 invoke 方法中。我们在 invoke 方法完成对方法的增强即可

JDKProxyFactory.java

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

public class JDKProxyFactory<T> implements InvocationHandler {

    /**
     * 目标对象
     */
    private T target;

    /**
     * 构造函数传入目标对象
     *
     * @param target 目标对象
     */
    public JDKProxyFactory(T target) {
        this.target = target;
    }

    /**
     * 获取代理对象
     *
     * @return 获取代理
     */
    public T getProxy() {
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 对方法增强
        System.out.println("大家好!");
        // 调用原方法
        Object result = method.invoke(target, args);
        // 方法增强
        System.out.println("再见!");
        return result;
    }
}

就这样,JDK动态代理的代码就完成了,接下来写一份测试代码

3.1.4、编写测试代码

为了方便测试,我们编写一个 test 方法

同时为了查看class文件,还添加了一个 generatorClass 方法,这个方法可以将动态代理生成的 .class 输出到文件

ProxyTest.java

import org.junit.Test;
import sun.misc.ProxyGenerator;

import java.io.FileOutputStream;
import java.io.IOException;

public class ProxyTest {

    @Test
    public void testJdkProxy() {
        // 生成目标对象
        Personal personal = new PersonalImpl();
        // 获取代理对象
        JDKProxyFactory<Personal> proxyFactory = new JDKProxyFactory<>(personal);
        Personal proxy = proxyFactory.getProxy();

        // 将proxy的class字节码输出到文件
        generatorClass(proxy);

        // 调用代理对象
        proxy.introduction();
    }

    /**
     * 将对象的class字节码输出到文件
     *
     * @param proxy 代理类
     */
    private void generatorClass(Object proxy) {
        FileOutputStream out = null;
        try {
            byte[] generateProxyClass = ProxyGenerator.generateProxyClass(proxy.getClass().getSimpleName(), new Class[]{proxy.getClass()});
            out = new FileOutputStream(proxy.getClass().getSimpleName() + ".class");
            out.write(generateProxyClass);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                }
            }
        }

    }

}

3.1.5、查看运行结果

可以看到,运行 test 方法之后,控制台打印出如下:

大家好!
我是程序员!
再见!

我们在 introduction 方法前和后都成功增加了功能,让这个 程序员 的自我介绍瞬间变得更加有礼貌了。

3.1.6、探探动态代理的秘密

动态代理的代码并不多,那么JDK底层是怎么帮我们实现的呢?

在测试的时候我们将动态生成的代理类的 class 字节码输出到了文件,我们可以反编译看看。

结果有点长,就不全部贴出来了,不过我们可以看到,里面有一个 introduction 方法如下:

/**
* the invocation handler for this proxy instance.
* @serial
*/
protected InvocationHandler h;

protected Proxy(InvocationHandler h) {
    Objects.requireNonNull(h);
    this.h = h;
}

public final void introduction() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

原来,生成的代理对象里面,引用了我们的 InvocationHandler ,然后在将 introduction 方法里面调用了 InvocationHandlerintroduction ,而 InvocationHandler 是由我们编写的代理类,在这里我们增加了 sayHellosayGoodBye 操作,然后还调用了原对象的 introduction 方法,就这样完成了动态代理。

3.2、CgLib动态代理

CgLib 动态

3.2.1、创建被代理对象

由于 CgLib 不需要实现接口,所以我们不需要创建接口文件了(当然,你要有接口也没有问题)

直接创建目标类,实现 introduction 方法

PersonalImpl.java

public class PersonalImpl {
    public void introduction() {
        System.out.println("我是程序员!");
    }
}

3.2.2、创建代理类

同样,我们也需要创建代理类,并且在这里实现增强的逻辑,这次我们不是实现 InvocationHandler 接口了,而是实现 CgLib 提供的接口 MethodInterceptor ,都是类似的, MethodInterceptor 中,全部方法调用都会交给 intercept 处理,我们在 intercept 添加处理逻辑即可。

CgLibProxyFactory.java

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CgLibProxyFactory<T> implements MethodInterceptor {

    /**
     * 获取代理对象
     *
     * @param tClass 被代理的目标对象
     * @return 代理对象
     */
    public T getProxyByCgLib(Class<T> tClass) {
        // 创建增强器
        Enhancer enhancer = new Enhancer();

        // 设置需要增强的类的类对象
        enhancer.setSuperclass(tClass);

        // 设置回调函数
        enhancer.setCallback(this);

        // 获取增强之后的代理对象
        return (T) enhancer.create();
    }

     /**
     *  代理类方法调用回调
     *  
     * @param obj 这是代理对象,也就是[目标对象]的子类
     * @param method [目标对象]的方法
     * @param args 参数
     * @param proxy 代理对象的方法
     * @return 返回结果,返回给调用者
     * @throws Throwable
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

        System.out.println("大家好!");

        Object result = proxy.invokeSuper(obj, args);

        System.out.println("再见!");

        return result;
    }
}

3.2.3、编写测试代码

在刚才的测试方法中,我们添加一个 cglib 的测试方法:

@Test
public void testCgLibProxy() {
    // 生成被代理的目标对象
    PersonalImpl personal = new PersonalImpl();
    
    // 获取代理类
    CgLibProxyFactory<PersonalImpl> proxyFactory = new CgLibProxyFactory<>();
    PersonalImpl proxy = proxyFactory.getProxyByCgLib((Class<PersonalImpl>) personal.getClass());

    // 将proxy的class字节码输出到文件
    generatorClass(proxy);

    // 调用代理对象
    proxy.introduction();
}

3.2.4、查看运行结果

运行测试用例,可以看到跟JDK的实现一样的效果

大家好!
我是程序员!
再见!

3.2.5、探探动态代理的秘密

跟JDK的测试一样,我们也来看看生成的 class 文件

public final void introduction() throws  {
    try {
        super.h.invoke(this, m7, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}

可以发现,与JDK的动态代理并没有区别。

4、如何选择

既然有两种实现方式,那么到底应该怎么选择呢?

就两个原则:

  • 目标类有接口实现的, JDKCgLib 都可以选择,你开心就好
  • 目标类没有实现任何接口,那只能用 CgLib

5、后记

其实在第一次看到动态代理的时候,我就想不明白,我们都把目标类 new 出来了,为什么还要将目标类丢给代理类呢?为什么不直接调用目标类对应的方法呢?

后来才发现,原来我没搞清楚动态代理的使用场景,场景很清晰,就是:

  • 不修改原来已有的代码(满足设计模式的要求)
  • 对已有方法进行 增强

关键是 增强 ,代理类里面我们是可以添加很多处理逻辑的,从而实现增强效果。就像黄牛抢票比我们厉害些一样。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

总开关

总开关

[美] 吴修铭 / 顾佳 / 中信出版社 / 2011-8 / 49.00元

当收音机经历从真空管收音机到半导体收音机,再到电晶体收音机的发展升级时,人们觉得自己的资讯来源美满得无可复加了。当约翰.洛吉.贝尔德发明了电视以后,在很长一段时间内,人们都认为电视就是他们所拥有的﹑也是所愿意拥有的最好的资讯媒介。 时至今日,互联网的震撼不亚于以往任何媒介,它给我们带来了最大的信息量,最便捷的自我表达,最迅速的沟通。互联网似乎比以往任何媒介都具有优越性。在互联网成为这个时代主......一起来看看 《总开关》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

SHA 加密
SHA 加密

SHA 加密工具