设计模式系列 - 代理模式及动态代理详解

本文介绍了代理模式的概念、结构和优缺点,提供了静态代理和动态代理(JDK及CGLIB实现)的代码示例,并对比了两者的差异。动态代理包括JDK动态代理和CGLIB动态代理,分别适用于不同的场景,代理模式常用于业务系统的非功能性需求如监控、统计、鉴权等。
摘要由CSDN通过智能技术生成

定义

为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

结构

抽象角色:通过接口或抽象类声明真实角色实现的业务方法。

代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。

真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用

优缺点

优点

  • 代理模式能将代理对象与真实被调用的目标对象分离。
  • 一定程度上降低了系统的耦合度,扩展性好。
  • 可以起到保护目标对象的作用。
  • 可以对目标对象的功能增强。

缺点

  • 代理模式会造成系统设计中类的数量增加。
  • 在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢。

应用实例

静态代理

业务描述:

有个哥们 想和他对象求婚,但是自己没有经验,又想浪漫一点,于是就找了一个“婚姻策划”

img

代码实现
package com.beauty.designpatterns.structure;

/**
 * description 代理
 *
 * @author yufengwen
 * @date 2023/2/28 17:45
 */
public class ProxyPattern {


    public static void main(String[] args) {
        Marriage lover = new Lover();
        Marriage scheme = new Scheme(lover);
        scheme.propose();
    }

}

// 求婚策划

/**
 * 接口
 */
interface Marriage{

    /**
     * 求婚
     */
     void propose();


}

/**
 * 具体的对象
 */
class Lover implements Marriage{

    /**
     * 求婚
     */
    @Override
    public void propose() {
        System.out.println("嫁给我吧");
    }
}

/**
 * 代理对象
 */
class Scheme implements Marriage{

    private Marriage marriage;

    public Scheme(Marriage marriage) {
        this.marriage = marriage;
    }

    /**
     * 求婚
     */
    @Override
    public void propose() {

        System.out.println("前期策划方案");
        System.out.println("===================================");
        marriage.propose();
        System.out.println("===================================");
        System.out.println("策划后期");

    }
}

动态代理

JDK动态代理
JDK动态代理步骤

JDK动态代理分为以下几步:

  1. 拿到被代理对象的引用,并且通过反射获取到它的所有的接口。
  2. 通过JDK Proxy类重新生成一个新的类,同时新的类要实现被代理类所实现的所有的接口。
  3. 动态生成 Java 代码,把新加的业务逻辑方法由一定的逻辑代码去调用。
  4. 编译新生成的 Java 代码.class。
  5. 将新生成的Class文件重新加载到 JVM 中运行。

所以说JDK动态代理的核心是通过重写被代理对象所实现的接口中的方法来重新生成代理类来实现的,那么假如被代理对象没有实现接口呢?那么这时候就需要CGLIB动态代理了。

/**
 * description
 * 主体方法 Proxy.newProxyInstance(ClassLoader loader,
 *                                           Class<?>[] interfaces,
 *                                           InvocationHandler h)
 *                                           InvocationHandler的实现为 代理的实现
 *
 * @author yufengwen
 * @date 2021/12/6 2:34 下午
 */
public class JdkProxy  {

    public static void main(String[] args) {
		//第一步:创建被代理对象
        Person xm = new Xm();
        // 第二步:创建handler,传入真实对象
        InvocationHandler invocationHandler = new MyInvocationHandler(xm);
        //第三步:创建代理对象,传入类加载器、接口、handler
        final Person person = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[]{Person.class}, invocationHandler);
        //第四步:调用方法
        person.eat();
        person.say();
    }
}

/**
 * 被代理类
 */
class Xm implements Person{

    @Override
    public void eat() {
        System.out.println("我是小明,我在吃东西");
    }

    @Override
    public void say() {
        System.out.println("我是小明,我在说话");
    }
}

/**
*  代理接口
*/
interface Person{

    void eat();

    void say();
}

/**
* 实现InvocationHandler;编写具体的代理逻辑
*/
class MyInvocationHandler implements InvocationHandler{

    private Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("jdk 代理操作前逻辑");
        Object result = method.invoke(target, args);
        System.out.println("jdk 代理操作后逻辑");
        return result;
    }
}

测试结果

jdk 代理操作前逻辑
我是小明,我在吃东西
jdk 代理操作后逻辑
jdk 代理操作前逻辑
我是小明,我在说话
jdk 代理操作后逻辑

Process finished with exit code 0
cglib动态代理

JDK动态代理是通过重写被代理对象实现的接口中的方法来实现,而CGLIB是通过继承被代理对象来实现,和JDK动态代理需要实现指定接口一样,CGLIB也要求代理对象必须要实现MethodInterceptor接口,并重写其唯一的方法intercept。

CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。(利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理)

注意:因为CGLIB是通过继承目标类来重写其方法来实现的,故而如果是final和private方法则无法被重写,也就是无法被代理。

/**
 * description
 *
 * @author yufengwen
 * @date 2021/12/6 3:00 下午
 */
public class CglibProxy {

    public static void main(String[] args) {
        final CgProxy<XiaoMing> callback = new CgProxy<>();
        final XiaoMing proxy = callback.getProxy(XiaoMing.class);
        proxy.eat();
    }

}

/**
 * 实现 MethodInterceptor
 * 重写 intercept 方法 实现具体的代理逻辑
 * @param <T>
 */
class CgProxy<T> implements MethodInterceptor{

    public T getProxy(Class<T> clazz){
        Enhancer enhancer = new Enhancer();
        //设置父类
        enhancer.setSuperclass(clazz);
        //设置方法拦截处理器
        enhancer.setCallback(this);
        return (T) enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

        System.out.println("cglib 代理 前逻辑");
        System.out.println(method.getName());
        methodProxy.invokeSuper(o,objects);
        System.out.println("cglib 代理 后逻辑");

        return null;
    }
}

测试结果:

cglib 代理 前逻辑
eat
我是小明,我在吃东西
cglib 代理 后逻辑

Process finished with exit code 0
CGLIB动态代理实现分析

CGLib动态代理采用了FastClass机制,其分别为代理类和被代理类各生成一个FastClass,这个FastClass类会为代理类或被代理类的方法分配一个 index(int类型)。这个index当做一个入参,FastClass 就可以直接定位要调用的方法直接进行调用,这样省去了反射调用,所以调用效率比 JDK 动态代理通过反射调用更高。

但是我们看上面的源码也可以明显看到,JDK动态代理只生成一个文件,而CGLIB生成了三个文件,所以生成代理对象的过程会更复杂

JDK和CGLib动态代理对比

JDK 动态代理是实现了被代理对象所实现的接口,CGLib是继承了被代理对象。 JDK和CGLib 都是在运行期生成字节码,JDK是直接写Class字节码,CGLib 使用 ASM 框架写Class字节码,Cglib代理实现更复杂,生成代理类的效率比JDK代理低。

JDK 调用代理方法,是通过反射机制调用,CGLib 是通过FastClass机制直接调用方法,CGLib 执行效率更高。

原理区别:

java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。核心是实现InvocationHandler接口,使用invoke()方法进行面向切面的处理,调用相应的通知。

而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。核心是实现MethodInterceptor接口,使用intercept()方法进行面向切面的处理,调用相应的通知。

1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP

2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP

3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

性能区别:

1、CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在jdk6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的方法进行代理,因为CGLib原理是动态生成被代理类的子类。

2、在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理。

各自局限:

1、JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理。

2、cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。

类型机制回调方式适用场景效率
JDK动态代理委托机制,代理类和目标类都实现了同样的接口,InvocationHandler持有目标类,代理类委托InvocationHandler去调用目标类的原始方法反射目标类是接口类效率瓶颈在反射调用稍慢
CGLIB动态代理继承机制,代理类继承了目标类并重写了目标方法,通过回调函数MethodInterceptor调用父类方法执行原始逻辑通过FastClass方法索引调用非接口类、非final类,非final方法第一次调用因为要生成多个Class对象,比JDK方式慢。多次调用因为有方法索引比反射快,如果方法过多,switch case过多其效率还需测试

静态代理和动态的本质区别

  • 静态代理只能通过手动完成代理操作,如果被代理类增加新的方法,代理类需要同步新增,违背开闭原则。
  • 动态代理采用在运行时动态生成代码的方式,取消了对被代理类的扩展限制,遵循开闭原则。
  • 若动态代理要对目标类的增强逻辑扩展,结合策略模式,只需要新增策略类便可完成,无需修改代理类的代码。

代理模式和装饰器模式的区别

代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

个人感觉两者最主要的区别在于:代理模式增加的代理逻辑 不是为了增强本身的功能;****装饰器模式主要就是为了增强原始类本身的逻辑

主要应用场景

代理模式常用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类统一处理,让程序员只需要关注业务方面的开发。除此之外,代理模式还可以用在 RPC、缓存等应用场景中。

参考文章:

设计模式之美
https://juejin.cn/post/7011357346018361375

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值