【Java】代理模式(静态代理&动态代理)

2023-05-16

CONTENT

    • 代理模式
    • 静态代理
    • 动态代理
      • JDK 动态代理(基于接口)
      • CGLIB 动态代理(基于类继承)
      • JDK 动态代理 v.s. CGLIB 动态代理
      • JDK 动态代理为什么必须基于接口
    • Reference

代理模式

定义: 代理模式(Proxy Pattern),设计模式的一种,为其他对象提供一种代理以控制对这个对象的访问。

用途:

  1. 在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

  2. 使用代理对象可以在不修改目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。比如说在目标对象的某个方法执行前后可以增加一些自定义的操作。

比喻: 类似与明星(目标对象)和经纪人(代理对象)的关系,客户想谈合作不直接找明星(目标对象),而是找他的经纪人(代理对象)谈合作,然后明星(目标对象)来做客户要求的工作。

优点: 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。符合代码设计的开闭原则(对扩展开放,对修改关闭)。远程代理使得客户端可以访问在远程机器上的对象,远程机器可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。

缺点: 由于在客户端和目标对象之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

分类:

  1. 远程代理:为不同地理的对象提供局域网代表对象。

  2. 虚拟代理:根据需要将资源消耗很大的对象进行延迟,真正需要的时候再创建。

  3. 安全代理:控制用户的访问权限。

  4. 智能代理:提供对目标对象额外的服务「使用最多的」。(静态代理和动态代理)

静态代理

静态代理中的 静态 指的是 代理类 在编译期间就创建好了,是手动创建的类,不是运行时动态生成的。也就是说 在编译时就已经将 接口,目标类,代理类等确定下来。

其实 实现某一接口或继承某一类,都可以实现静态代理,但是平时说的静态代理指的是使用接口的静态代理,因为它更灵活。

为什么需要接口?
代理类与目标类之间通常会存在关联关系,一个代理类的对象与一个目标类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用目标类的对象的相关方法,来提供特定的服务。使用接口,当换一个实现相同接口的目标类时,不需要写新的代理类,静态代理的情况下,用接口更灵活。
from:CSDN:夢_殤:JAVA静态代理为什么用聚合用接口

优点:

  1. 在符合开闭原则的情况下,对目标对象功能进行扩展和拦截。
  2. 访问无法访问的资源,职责清晰。

缺点:

  1. 每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。一个 代理类 只能服务于 一种 目标类(实现某一接口)。也就是说 需要对实现不同接口的目标类都单独写一个代理类,多个代理类代码可能冗余。
  2. 代理类和目标类必须实现相同的接口。假如接口中新增了一个方法,那么目标类和代理类都要实现新方法,同时代理类可能还要对新方法写和之前一样的操作。这会很繁琐,代码也需要经常修改,维护代价高。
  3. 代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展。

以 明星(目标对象)和 经纪人(代理对象)举例:

// 工作者的接口
interface Workable {
    void work();
}

// 目标类 明星
class Star implements Workable {

    @Override
    public void work() {
        System.out.println("Star sing and dance.");
    }
}

// 代理类 经纪人
class Broker implements Workable {

    private final Workable worker;

    public Broker(Workable worker) {
        this.worker = worker;
    }

    @Override
    public void work() {
        System.out.println("Broker do some prep work.");
        worker.work();
        System.out.println("Broker do some finishing work.");
    }
}


public class StaticProxy {
    public static void main(String[] args) {
        // 目标对象 明星
        Star star = new Star();
        // 代理对象 经纪人
        Broker broker = new Broker(star);
        // 通过 代理对象(经纪人) 调用 目标对象(明星) 完成客户的工作要求
        broker.work();
    }
}

OutPut 程序输出

Broker do some prep work.
Star sing and dance.
Broker do some finishing work.

动态代理

那么有没有这样的代理,不管接口是否新增了方法,代理类都不需要做任何修改呢?对于相同的代理类代码只写一次呢?有,那就是 JDK 动态代理。

那么有没有这样的代理,不管是否实现了接口,都能实现代理呢?有,那就是 CGLIB 动态代理。

相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理目标类。

动态代理是指客户通过代理类来调用其它目标对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。动态代理是在运行时动态生成类字节码,并加载到 JVM 中。

JDK 动态代理(基于接口)

※ JDK 是通过接口实现的动态代理,目标类必须实现至少一个接口 ※

JDK 中生成代理对象主要涉及的类是:java.lang.reflect.Proxy,接口是 java.lang.reflect.InvocationHandler。

Proxy 类中用到的方法,方法返回一个 代理类的对象

public static Object newProxyInstance(ClassLoader loader,     // 指定当前目标对象使用的类加载器
                                      Class<?>[] interfaces,  // 目标对象实现的一些接口的类型
                                      InvocationHandler h)    // 事件处理器,就是实现了下面接口的对象

InvocationHandler 接口中要实现的方法,实际 代理部分 的处理逻辑,当动态代理对象调用一个方法时,这个方法的实际调用就会被转发到实现 InvocationHandler 接口类中的 invoke 方法。

public Object invoke(Object proxy, Method method, Object[] args)
// proxy : 动态生成的 代理对象
// method : 与代理类对象调用的方法相对应
// args : 当前 method 方法的参数

以 明星(目标对象)和 经纪人(代理对象)举例:

这里如果 Workable 接口新增了方法 workFast(),如果代理不用 workFast(),完全不用改,如果代理用 workFast(),但在 workFast() 方法前后做的事情和 work() 方法是一样的,那也不用改。或者说如果以后不代理 Star 类了,比如说 代理 Child,Child 实现 Playable 接口,如果在 play() 方法前后做的事情是一样的,BrokerInvocationHandler 和 JdkProxyFactory 都不用改。

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


// 工作者的接口
interface Workable {
    void work();
}

// 目标类 明星
class Star implements Workable {

    @Override
    public void work() {
        System.out.println("Star sing and dance.");
    }
}

// 经纪人 实际工作,如果通过 经纪人代理对象 调用 明星目标对象,实际会转到执行这里的 invoke 方法
class BrokerInvocationHandler implements InvocationHandler {

    // target :目标对象
    private final Object target;

    public BrokerInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        // 为了证明代理类是动态生成的
        System.out.println("proxy.getClass().getName() : " + proxy.getClass().getName());
        System.out.println("Broker do some prep work.");
        result = method.invoke(target, args);
        System.out.println("Broker do some finishing work.");
        return result;
    }
}

// 代理对象 的工厂类,生产代理类的代理对象,这里只生产 BrokerInvocationHandler 经纪人
class JdkProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 目标类的类加载器
                target.getClass().getInterfaces(),  // 代理需要实现的接口(可指定多个)
                new BrokerInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler
        );
    }
}


public class DynamicProxyJDK {
    public static void main(String[] args) {
        // 目标对象 明星
        Star star = new Star();
        // 代理对象 经纪人
        Workable broker = (Workable) JdkProxyFactory.getProxy(star);
        // 通过 代理对象(经纪人) 调用 目标对象(明星) 完成客户的工作要求
        broker.work();
    }
}

OutPut 程序输出

proxy.getClass().getName() : $Proxy0    // 为了证明代理类是动态生成的
Broker do some prep work.
Star sing and dance.
Broker do some finishing work.

CGLIB 动态代理(基于类继承)

※ CGLIB 是通过继承实现的动态代理,目标类不能声明为 final,调用的方法不能声明为 final ※

CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。

很多知名的开源框架都使用到了CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

CGLIB(Code Generation Library) 实际是属于一个开源项目,使用 cglib 需要引入 cglib 的 jar 包。

pom.xml

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

CGLIB 中生成代理对象主要涉及的类是:net.sf.cglib.proxy.Enhancer,接口是:net.sf.cglib.proxy.MethodInterceptor。

Enhancer 类中用到的方法,方法返回一个 代理类的对象

public Object create()

在调用 create() 创建代理对象之前,需要设置几个量,调用的是 Enhancer 类中 或者是 Enhancer 继承的父类 AbstractClassGenerator 中的 set 方法

public void setClassLoader(ClassLoader classLoader) // 设置当前目标对象使用的类加载器,父类中
public void setSuperclass(Class superclass)         // 设置目标类,Enhancer 类中
public void setCallback(Callback callback)          // 设置方法拦截器,父类中

MethodInterceptor 接口中要实现的方法,实际 代理部分 的处理逻辑,当动态代理对象调用一个方法时,这个方法的实际调用就会被拦截到实现 MethodInterceptor 接口类中的 intercept 方法。

public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy)
// o: 动态生成的 代理对象
// method : 与代理对象调用的方法相对应,目标类被拦截的方法
// args : 当前 method 方法的参数
// methodProxy :代理对象调用的方法,因为不需要目标对象,用它调用父类(目标类)的方法

以 明星(目标对象)和 经纪人(代理对象)举例:

可以看到 明星(目标类)不再需要实现一个 Workable 接口。

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

import java.lang.reflect.Method;


// 目标类 明星
class Star {
    public void work() {
        System.out.println("Star sing and dance.");
    }
}

// 经纪人 实际工作,如果通过 经纪人代理对象 调用 明星目标对象,实际会拦截到执行这里的 intercept 方法
class BrokerMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object result = null;
        // 为了证明代理类是动态生成的
        System.out.println("o.getClass().getName() : " + o.getClass().getName());
        System.out.println("Broker do some prep work.");
        result = methodProxy.invokeSuper(o, args);
        System.out.println("Broker do some finishing work.");
        return result;
    }
}

// 代理对象 的工厂类,生产代理类的代理对象,这里只生产 BrokerMethodInterceptor 经纪人
class CglibProxyFactory {
    public static Object getProxy(Class<?> clazz) {
        Enhancer enhancer = new Enhancer();                   // 创建动态代理字节码增强器
        enhancer.setClassLoader(clazz.getClassLoader());      // 设置目标类的类加载器
        enhancer.setSuperclass(clazz);                        // 设置目标类
        enhancer.setCallback(new BrokerMethodInterceptor());  // 设置方法拦截器,代理对象自定义的 MethodInterceptor
        return enhancer.create();                             // 创建代理类对象
    }
}


public class DynamicProxyCGLIB {
    public static void main(String[] args) {
        // 目标对象 明星
        // 不需要目标对象,因为是 通过继承 目标对象 实现的动态代理,代理类继承了 目标类中的方法
        // 代理对象 经纪人
        Star broker = (Star) CglibProxyFactory.getProxy(Star.class);
        // 通过 代理对象(经纪人) 调用 目标类(明星)的方法 完成客户的工作要求
        broker.work();
    }
}

OutPut 程序输出

o.getClass().getName() : Star$$EnhancerByCGLIB$$e55eba8a    // 为了证明代理类是动态生成的
Broker do some prep work.
Star sing and dance.
Broker do some finishing work.

JDK 动态代理 v.s. CGLIB 动态代理

  1. JDK 是通过接口实现的动态代理,只能代理实现了接口的类或者直接代理接口。CGLIB 是通过继承实现的动态代理,可以代理未实现任何接口的类,但不能代理声明为 final 类型的类,声明为 final、private、static 的方法(final,private 方法不能继承,static 方法不能重写)。
  2. 就二者的效率来说,JDK 动态代理生成类速度快,调用慢(因为不能直接调用被代理对象的方法,而是要通过反射),CGLIB 生成类速度慢,但后续调用快(因为可以通过计算索引,直接定位到具体的方法)。但是实际上 JDK 的速度随着版本的升级越来越优秀,而 CGLIB 却止步不前。在 JDK8 下 JDK 动态代理性能比 CGLIB 稍好一些。

运行 1 0 5 10^5 105

JDK8CGLIB
耗时990ms1041ms

JDK 动态代理为什么必须基于接口

  1. 直接原因:动态生成的代理类(eg:$Proxy0)会实现目标类实现的所有接口 并 继承 java.lang.reflect.Proxy 类,Java 是单继承多实现,所以 JDK 无法利用继承来为目标对象生产代理对象。

  2. 根本原因:(个人看法,思考很浅)

    1. 代理接口比代理类更好更灵活些,调接口可以灵活的代理实现接口的多个类。面向接口编程?
    2. 为了通用,所以牺牲了一些特性,继承目标类的话需要重写方法,就不能代理声明为 final 类型的类,声明为 final、private、static 的方法。
    3. 。。。

Reference

  1. 百度百科:代理模式
  2. 菜鸟教程:代理模式
  3. JavaGuide:Java 代理模式详解
  4. thinkkeep.github.io :静态代理模式
  5. CSDN:夢_殤:JAVA静态代理为什么用聚合用接口
  6. 简书:Android开发_Hua:静态代理与动态代理详解
  7. segmentfault:Soarkey:Java三种代理模式:静态代理、动态代理和cglib代理
  8. B站:尚硅谷Java入门视频教程
  9. CSDN:风云叶易:实战CGLib系列文章 MethodInterceptor和Enhancer
  10. 简书:myf008:JDK 动态代理和Cglib性能对比
  11. 简书:小可怜求放过:JDK动态代理为什么不能代理类–详解动态代理
  12. segmentfault:MinGRn:关于代理:为什么 JDK 动态代理只能为接口生成代理?
  13. 知乎:luoxn28:JDK动态代理为什么不能代理类:评论Jeromememory
  14. CSDN:大火yzs:【AOP系列】自己动手模仿一个可以代理普通类的Proxy类(四)
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Java】代理模式(静态代理&动态代理) 的相关文章

随机推荐