java | 什么是动态代理?

简介: 最近在复习 Java 相关,回顾了下代理模式。代理模式在 Java 领域很多地方都有应用,它分为静态代理和动态代理,其中 Spring AOP 就是动态代理的典型例子。动态代理又分为接口代理和 cglib (子类代理),结合我的理解写了几个 demo 分享给你们,这是昨晚修仙到 3 点写出来的文章,不点在看,我觉得说不过去了。

静态代理


我还是以找黄牛帮我排队买张学友的演唱会门票的例子,写个 demo 说明。现在有一个 Human 接口,无论是我还是黄牛都实现了这个接口。


public interface Human {
    void eat();
    void sleep();
    void lookConcert();
}


例如,我这个类,我会吃饭和睡觉,如以下类:


public class Me implements Human{
    @Override
    public void eat() {
        System.out.println("eat emat ....");
    }
    @Override
    public void sleep() {
        System.out.println("Go to bed at one o'clock in the morning");
    }
    @Override
    public void lookConcert() {
        System.out.println("Listen to Jacky Cheung's Concert");
    }
}


有黄牛类,例如:


public class Me implements Human{
    @Override
    public void eat() {
    }
    @Override
    public void sleep() {
    }
    @Override
    public void lookConcert() {
    }
}


现在我和黄牛都已经准备好了,怎么把这二者关联起来呢?我们要明确的是黄牛是要帮我买票的,买票必然就需要帮我排队,于是有以下黄牛类:注意这里我们不关心,黄牛的其他行为,我们只关心他能不能排队买票。


public class HuangNiu implements Human{
    private Me me;
    public HuangNiu() {
        me = new Me();
    }
    @Override
    public void eat() {
    }
    @Override
    public void sleep() {
    }
    @Override
    public void lookConcert() {
        // 添加排队买票方法
        this.lineUp();
        me.lookConcert();
    }
    public void lineUp() {
        System.out.println("line up");
    }
}


最终的 main 方法调用如下:


public class Client {
    public static void main(String[] args) {
        Human human = new HuangNiu();
        human.lookConcert();
    }
}


结果如下:


640.png


由此可见,黄牛就只是做了我们不愿意做的事(排队买票),实际看演唱会的人还是我。客户端也并不关心代理类代理了哪个类,因为代码控制了客户端对委托类的访问。客户端代码表现为 Human human = new HuangNiu();


由于代理类实现了抽象角色的接口,导致代理类无法通用。比如,我的狗病了,想去看医生,但是排队挂号很麻烦,我也想有个黄牛帮我的排队挂号看病,但是黄牛它不懂这只狗的特性(黄牛跟狗不是同一类型,黄牛属于 Human 但狗属于 Animal 类)但排队挂号和排队买票相对于黄牛来说它两就是一件事,这个方法是不变的,现场排队。那我们能不能找一个代理说既可以帮人排队买票也可以帮狗排队挂号呢?


答案肯定是可以的,可以用动态代理。


基于接口的动态代理


如静态代理的内容所描述的,静态代理受限于接口的实现。动态代理就是通过使用反射,动态地获取抽象接口的类型,从而获取相关特性进行代理。因动态代理能够为所有的委托方进行代理,因此给代理类起个通用点的名字 HuangNiuHandle。先看黄牛类可以变成什么样?


public class HuangNiuHandle implements InvocationHandler {
    private Object proxyTarget;
    public Object getProxyInstance(Object target) {
        this.proxyTarget = target;
        return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object methodObject = null;
        System.out.println("line up");
        methodObject = method.invoke(proxyTarget, args);
        System.out.println("go home and sleep");
        return methodObject;
    }
}


这个时候的客户端代码就变成这样了


public class Client {
    public static void main(String[] args) {
        HuangNiuHandle huangNiuHandle = new HuangNiuHandle();
        Human human = (Human) huangNiuHandle.getProxyInstance(new Me());
        human.eat();
        human.run();
        human.lookConcert();
        System.out.println("------------------");
        Animal animal = (Animal) huangNiuHandle.getProxyInstance(new Dog());
        animal.eat();
        animal.run();
        animal.seeADoctor();
    }
}


使用动态代理有三个要点,


  1. 必须实现 InvocationHandler 接口,表明该类是一个动态代理执行类。


  1. InvocationHandler 接口内有一实现方法如下: public Object invoke(Object proxy, Method method, Object[] args) 。使用时需要重写这个方法


  1. 获取代理类,需要使用 Proxy.newProxyInstance 这个方法去获取Proxy对象(Proxy 类类型的实例)。


注意到 Proxy.newProxyInstance 这个方法,它需要传入 3 个参数。解析如下:


// 第一个参数,是类的加载器
// 第二个参数是委托类的接口类型,证代理类返回的是同一个实现接口下的类型,保持代理类与抽象角色行为的一致
// 第三个参数就是代理类本身,即告诉代理类,代理类遇到某个委托类的方法时该调用哪个类下的invoke方法
Proxy.newProxyInstance(Class loader, Class<?>[] interfaces, InvocationHandler h)


再来看看 invoke 方法,用户调用代理对象的什么方法,实质上都是在调用处理器的 invoke 方法,通过该方法调用目标方法,它也有三个参数:


// 第一个参数为 Proxy 类类型实例,如匿名的 $proxy 实例
// 第二个参数为委托类的方法对象
// 第三个参数为委托类的方法参数
// 返回类型为委托类某个方法的执行结果
public Object invoke(Object proxy, Method method, Object[] args)


调用该代理类之后的输出结果:


640.png


由结果可知,黄牛不仅帮了(代理)我排队买票,还帮了(代理)我的狗排队挂号。所以,你看静态代理需要自己写代理类(代理类需要实现与目标对象相同的接口),还需要一一实现接口方法,但动态代理不需要。


注意,我们并不是所有的方法都需要黄牛这个代理去排队。我们知道只有我看演唱会和我的狗去看医生时,才需要黄牛,如果要实现我们想要的方法上面添加特定的代理,可以通过 invoke 方法里面的方法反射获取 method 对象方法名称即可实现,所以动态代理类可以变成这样:


public class HuangNiuHandle implements InvocationHandler {
    private Object proxyTarget;
    public Object getProxyInstance(Object target) {
        this.proxyTarget = target;
        return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object methodObject = null;
        if ("lookConcert".equals(method.getName()) ||
        "seeADoctor".equals(method.getName())) {
            System.out.println("line up");
            // 调用目标方法
            methodObject = method.invoke(proxyTarget, args);
        } else {
            // 不使用第一个proxy参数作为参数,否则会造成死循环
            methodObject = method.invoke(proxyTarget, args);
        }
        return methodObject;
    }
}


结果如下:可以看到我们只在特定方法求助了黄牛

640.png

由此可见,动态代理一般应用在记录日志等横向业务。


值得注意的是:


  1. 基于接口类的动态代理模式,必须具备抽象角色、委托类、代理三个基本角色。委托类和代理类必须由抽象角色衍生出来,否则无法使用该模式。


  1. 动态代理模式最后返回的是具有抽象角色(顶层接口)的对象。在委托类内被 private 或者 protected 关键修饰的方法将不会予以调用,即使允许调用。也无法在客户端使用代理类转换成子类接口,对方法进行调用。也就是说上述的动态代理返回的是委托类(Me)或 (Dog)的就接口对象 (Human)或 (Animal)。


  1. 在 invoke 方法内为什么不使用第一个参数进行执行回调。在客户端使用 getProxyInstance(new Child( ))时,JDK 会返回一个 proxy 的实例,实例内有InvokecationHandler 对象及动态继承下来的目标 。客户端调用了目标方法,有如下操作:首先 JDK 先查找 proxy 实例内的 handler 对象 然后执行 handler 内的 invoke 方法。


根据 public Object invoke 这个方法第一个参数 proxy 就是对应着 proxy 实例。如果在 invoke 内使用 method.invoke(proxy,args) ,会出现这样一条方法链,目标方法→invoke→目标方法→invoke…,最终导致堆栈溢出。


基于子类的动态代理


为了省事,我这里并没有继承父类,但在实际开发中是需要继承父类才比较方便扩展的。与基于接口实现类不同的是:


  1. CGLib (基于子类的动态代理)使用的是方法拦截器 MethodInterceptor ,需要导入 cglib.jar 和 asm.jar 包


  1. 基于子类的动态代理,返回的是子类对象


  1. 方法拦截器对 protected 修饰的方法可以进行调用


代码如下:


public class Me {
    public void eat() {
        System.out.println("eat meat ....");
    }
    public void run() {
        System.out.println("I run with two legs");
    }
    public void lookConcert() {
        System.out.println("Listen to Jacky Cheung's Concert");
    }
    protected void sleep() {
        System.out.println("Go to bed at one o'clock in the morning");
    }
}


Dog 类


public class Dog {
    public void eat() {
        System.out.println("eat Dog food ....");
    }
    public void run() {
        System.out.println("Dog running with four legs");
    }
    public void seeADoctor() {
        System.out.println("The dog go to the hospital");
    }
}


黄牛代理类,注意 invoke() 这里多了一个参数 methodProxy ,它的作用是用于执行目标(委托类)的方法,至于为什么用 methodProxy ,官方的解释是速度快且在intercep t内调用委托类方法时不用保存委托对象引用。


public class HuangNiuHandle implements MethodInterceptor {
    private Object proxyTarget;
    public Object getProxyInstance(Object target) {
        this.proxyTarget = target;
        return Enhancer.create(target.getClass(), target.getClass().getInterfaces(), this);
    }
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object methodObject = null;
        if ("lookConcert".equals(method.getName()) ||
                "seeADoctor".equals(method.getName())) {
            System.out.println("line up");
            // 调用目标方法
            methodObject = methodProxy.invokeSuper(proxy, args);
        } else {
            methodObject = method.invoke(proxyTarget, args);
        }
        return methodObject;
    }
}


client 类


public class Client {
    public static void main(String[] args) {
        HuangNiuHandle huangNiuHandle = new HuangNiuHandle();
        Me me = (Me) huangNiuHandle.getProxyInstance(new Me());
        me.eat();
        me.run();
        me.sleep();
        me.lookConcert();
        System.out.println("------------------");
        Dog dog = (Dog) huangNiuHandle.getProxyInstance(new Dog());
        dog.eat();
        dog.run();
        dog.seeADoctor();
    }
}


结果:

640.png


注意到 Me 类中被 protected 修饰的方法 sleep 仍然可以被客户端调用。这在基于接口的动态代理中是不被允许的。


静态代理与动态代理的区别


静态代理需要自己写代理类并一一实现目标方法,且代理类必须实现与目标对象相同的接口。


动态代理不需要自己实现代理类,它是利用 JDKAPI,动态地在内存中构建代理对象(需要我们传入被代理类),并且默认实现所有目标方法。


源码下载:


https://github.com/turoDog/review_java.git


后语


如果本文对你哪怕有一丁点帮助,请帮忙点好看,你的好看是我坚持写作的动力。

相关文章
|
2月前
|
缓存 Java 测试技术
day27:Java零基础 - 动态代理
【7月更文挑战第27天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
28 2
day27:Java零基础 - 动态代理
|
28天前
|
开发者 C# 容器
【独家揭秘】当WPF邂逅DirectX:看这两个技术如何联手打造令人惊艳的高性能图形渲染体验,从环境搭建到代码实践,一步步教你成为图形编程高手
【8月更文挑战第31天】本文通过代码示例详细介绍了如何在WPF应用中集成DirectX以实现高性能图形渲染。首先创建WPF项目并使用SharpDX作为桥梁,然后在XAML中定义承载DirectX内容的容器。接着,通过C#代码初始化DirectX环境,设置渲染逻辑,并在WPF窗口中绘制图形。此方法适用于从简单2D到复杂3D场景的各种图形处理需求,为WPF开发者提供了高性能图形渲染的技术支持和实践指导。
70 0
|
1月前
|
设计模式 Java C++
揭秘!JDK动态代理VS CGLIB:一场关于Java代理界的‘宫心计’,你站哪队?
【8月更文挑战第24天】Java 动态代理是一种设计模式,允许在不改动原类的基础上通过代理类扩展功能。主要实现方式包括 JDK 动态代理和 CGLIB。前者基于接口,利用反射机制在运行时创建代理类;后者采用继承方式并通过字节码技术生成子类实现类的代理。两者在实现机制、性能及适用场景上有明显差异。JDK 动态代理适用于有接口的场景,而 CGLIB 更适合代理未实现接口的类,尽管性能更优但存在一些限制。开发者可根据需求选择合适的代理方式。
82 0
|
2月前
|
开发框架 Java Android开发
Java中的类反射与动态代理详解
Java中的类反射与动态代理详解
|
2月前
|
Java 数据安全/隐私保护
Java中的动态代理机制详解
Java中的动态代理机制详解
|
2月前
|
Java
Java中的反射与动态代理机制详解
Java中的反射与动态代理机制详解
|
3月前
|
监控 Java Spring
Java 动态代理详解与实战示例
Java 动态代理详解与实战示例
64 1
|
2月前
|
设计模式 Java 程序员
解析Java中的动态代理与静态代理的区别
解析Java中的动态代理与静态代理的区别
|
2月前
|
设计模式 Java C++
Java中的静态代理与动态代理详解
Java中的静态代理与动态代理详解
|
2月前
|
设计模式 Java C++
Java中的静态代理与动态代理详解
Java中的静态代理与动态代理详解