Java两种动态代理方式-JDK动态代理和Cglib动态代理

Java中两种常用动态代理方式

转载:java | 什么是动态代理? - 知乎 (zhihu.com)

什么是动态代理?两种常用的动态代理方式-CSDN博客

  基于接口的动态代理 基于类的动态代理
提供者 JDK 第三方CGLib
说明 使用JDK官方的Proxy类创建代理对象 使用CGLib的Enhancer类创建代理对象
注意 代理的目标对象必须实现接口 需要导入asm.jar包

代理模式在日常生活中很常见,例如:看演唱会很难抢票,可以找黄牛排队买票;出去 吃饭不方便,可以点外卖,让骑手配送。

1、基于接口的动态代理

动态代理是通过反射,动态获取抽象接口的类型,从而获取相关特性进行代理。因此代理可以为所有的委托方进行代理。

给代理类起个通用点的名字 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;
​
        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;
    }
}

客户端代码

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();
    }
}

使用动态代理的三个注意点

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

  • 重写InvocationHandler 接口invoke方法:

     public Object invoke(Object proxy, Method method, Object[] args) 
  • 获取代理类,需要使用 Proxy.newProxyInstance(Clas loader, Class[] interfaces, InvocationHandler h) 这个方法去获取Proxy对象(Proxy 类类型的实例)

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

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

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

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

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

注意:

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

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

  3. 在 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 包

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

  3. 方法拦截器对 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();
    }
}

结果:

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

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

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

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

源码下载:https://github.com/turoDog/review_java.git

posted @ 2023-11-02 11:03  该用户已注销!  阅读(59)  评论(0)    收藏  举报