您现在的位置是:首页 >技术杂谈 >肝一肝设计模式【七】-- 代理模式网站首页技术杂谈

肝一肝设计模式【七】-- 代理模式

老六说编程 2024-06-14 18:01:02
简介肝一肝设计模式【七】-- 代理模式

系列文章目录

肝一肝设计模式【一】-- 单例模式 传送门
肝一肝设计模式【二】-- 工厂模式 传送门
肝一肝设计模式【三】-- 原型模式 传送门
肝一肝设计模式【四】-- 建造者模式 传送门
肝一肝设计模式【五】-- 适配器模式 传送门
肝一肝设计模式【六】-- 装饰器模式 传送门



前言

本节我们继续分析设计模式中的结构型模式,前文中我们已经分析了适配器模式和装饰器模式,本节我们来学习一下——代理模式。


一、什么是代理模式

代理模式(Proxy Pattern),用于在对象之间提供间接访问,在代理模式中,代理对象充当了原始对象的代表,以控制对原始对象的访问。

代理模式的主要角色:

  • 抽象主题(Subject):定义了代理类和原始类之间的公共接口,以便代理类可以替代原始类。抽象主题通常是一个接口或抽象类,其中定义了原始对象和代理对象需要实现的方法。

  • 具体主题(Real Subject):是代理模式中的原始对象,代理类所代表的对象。具体主题实现了抽象主题中定义的接口,是真正执行业务逻辑的对象。

  • 代理(Proxy):代理是客户端访问具体主题的中介。代理对象与具体主题实现相同的接口,并保存具体主题的引用。当客户端向代理对象发送请求时,代理对象会将请求转发给具体主题,并可以在请求前后添加额外的逻辑。

概念了解了以后,其实不难理解,生活当中就有很多代理模式的样例,举个栗子,韩梅梅和李雷是同学,韩梅梅饿了但又不想自己出去吃饭,就想让李雷去买回来。
在这里插入图片描述

这里李雷就相当于是代理角色。

二、静态代理

代理模式又分为静态代理和动态代理,先说静态代理

写下代码:

先定义一个顶层接口,定义买饭这件事

public interface IPerson {
	void findFood();
}

韩梅梅饿了要吃饭

public class HanMeiMei implements IPerson {
	@Override
	public void findFood() {
		System.out.println("韩梅梅饿了想找点吃的");
	}
}

但韩梅梅有点懒,想让李雷帮忙买回来

public class LiLei implements IPerson {
	
	private HanMeiMei hanMeiMei;
	
	public LiLei(HanMeiMei hanMeiMei) {
		this.hanMeiMei = hanMeiMei;
	}

	@Override
	public void findFood() {
		System.out.println("李雷接到韩梅梅的电话");
		hanMeiMei.findFood();
		System.out.println("李雷帮韩梅梅买点吃的回来");
	}
}

测试一下:

public class Test {
    public static void main(String[] args) {
        LiLei liLei = new LiLei(new HanMeiMei());
        liLei.findFood();
    }
}

静态代理是在编译时就已经确定代理关系的代理模式。它需要为每个原始对象编写一个代理类,代理类与原始类实现相同的接口,以便可以通过代理类访问原始对象。在代理类中,可以通过调用原始对象的方法来实现对原始对象的访问,并可以在方法前后添加额外的逻辑。

三、动态代理

上述场景里,韩梅梅的主要诉求是饿了想吃饭,如果当时李雷没联系上,就还得联系别人,代码角度就还需要新增一个代理类,这样的话显然使用静态代理就不太适合了,目的是买到食物,具体谁来买其实并不重要,这就引申出来动态代理的概念。

在Java中,目前普遍使用的是JDK动态代理和CGLib动态代理

1. JDK动态代理

JDK动态代理是指使用Java内置的反射机制来动态生成代理对象的一种代理模式实现方式。

JDK动态代理需要满足以下两个条件:

  • 被代理类必须实现至少一个接口。

  • 代理类必须实现InvocationHandler接口。

InvocationHandler接口中只定义了一个方法invoke(),这个方法会在代理对象调用方法时被自动调用。在invoke()方法中,我们可以根据方法名和参数类型等信息,决定是否要将方法调用转发给被代理对象,或者在调用前后添加一些额外的逻辑。

我们来修改下代码:

首先先修改一下代理类

public class WaiMai implements InvocationHandler {

	private HanMeiMei hanMeiMei;

    public WaiMai(HanMeiMei hanMeiMei) {
        this.hanMeiMei = hanMeiMei;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("拿起手机");
        Object result = method.invoke(hanMeiMei, args);
        System.out.println("在外卖平台下单,小哥开始配送");
        return result;
    }
}

测试一下:

public class Test {
    public static void main(String[] args) {
        HanMeiMei hanMeiMei = new HanMeiMei();
        IPerson proxyPerson = (IPerson) Proxy.newProxyInstance(
            hanMeiMei.getClass().getClassLoader(),
            hanMeiMei.getClass().getInterfaces(),
            new WaiMai(hanMeiMei)
        );
        proxyPerson.findFood();
    }
}

newProxyInstance()方法会动态生成一个代理类,该方法有三个参数:

  • ClassLoader:生成一个类, 这个类也需要加载到方法区中, 因此需要指定ClassLoader来加载该类
  • Class[] interfaces:要实现的接口
  • InvocationHandler:调用处理器

2. CGLib动态代理

CGLib动态代理是指使用CGLib库来动态生成代理对象的一种代理模式实现方式。
相比于JDK动态代理,它可以代理没有实现任何接口的类,因为它是通过继承被代理类来生成代理对象的。

CGLib动态代理的实现过程是:通过ASM字节码框架直接将代理对象类的class文件加载到JVM中,修改其字节码生成子类,子类重写父类中的方法,并在重写的方法中增加了我们定义的逻辑。最后,生成一个新的代理对象子类的class文件,然后通过反射机制来创建代理对象。

修改下代码:

首先修改一下代理类,不在需要实现顶层接口

public class HanMeiMei {
	public void findFood() {
		System.out.println("韩梅梅饿了想找点吃的");
	}
}

先新增一个实现MethodInterceptor接口的实现类

public class WaiMai implements MethodInterceptor {
    @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;
    }
}

测试一下:

public class Test {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(HanMeiMei.class);
        enhancer.setCallback(new WaiMai());

        HanMeiMei proxyHanMeiMei = (HanMeiMei) enhancer.create();
        proxyHanMeiMei.findFood();
    }
}

CGLib动态代理,可以选择使用反射调用或者FastClass机制调用,默认情况下会使用反射调用,如果需要使用FastClass机制调用,则需要通过设置Enhancer类的useFastClass属性来开启。

使用FastClass机制:

public class Test {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(HanMeiMei.class);
        enhancer.setCallback(new WaiMai());

		enhancer.setUseFactory(false);  // 禁用缓存,强制使用FastClass机制
        enhancer.setUseFastClass(true); // 开启FastClass机制

        HanMeiMei proxyHanMeiMei = (HanMeiMei) enhancer.create();
        proxyHanMeiMei.findFood();
    }
}

使用FastClass机制调用时,CGLib会通过ASM字节码框架生成一个FastClass类,该类中包含被代理类中方法的索引和对应的方法调用代码,这样在调用代理类的方法时,就可以直接使用FastClass类中对应方法的调用代码,避免了反射调用的开销。
需要注意的是,使用FastClass机制调用虽然能够提高代理类的性能,但也会增加代理类生成的时间和内存开销。

上文所描述的是CGLib 3.0.0 版本之前,想开启 FastClass 机制,需要手动调用setUseFastClass()方法来设置。
CGLib 3.0.0 版本开始默认启用 FastClass 机制,无需调用setUseFastClass()方法。


写在最后

代理模式的基本思想是创建一个代理对象,该代理对象与原始对象具有相同的接口,以便可以替换原始对象。当客户端向代理对象发送请求时,代理对象会将请求转发给原始对象,同时可以在请求前后添加额外的逻辑。这种方式可以隐藏原始对象的复杂性,并提供更加简单和易用的接口。

代理模式的优点:

  • 解耦原有对象,代理对象与原有对象之间的耦合度降低,原有对象可以专注于自己的业务逻辑,而代理对象则负责其他方面的操作。
  • 对原有对象进行增强,代理对象可以在调用原有对象方法前后进行一些操作,例如日志记录、缓存处理等,从而增强了原有对象的功能。

静态代理的优点:

  • 在编译时进行类型检查,避免了运行时出现类型错误的风险
  • 提供更好的性能,因为代理类在编译时就已经生成,不需要在运行时动态生成代理对象

静态代理的缺点:

  • 需要为每个原始对象编写一个代理类,如果原始对象的接口发生变化,代理类也需要相应地进行更新

动态代理的优点:

  • 可以动态地生成代理对象,避免了静态代理中需要为每个原始对象编写代理类的麻烦
  • 可以支持对不同的原始对象进行代理,并可以在运行时动态地添加或删除代理对象

动态代理的缺点:

  • JDK动态代理和CGLib动态代理(反射调用)需要通过反射机制动态生成代理类,可能会降低一些性能,CGLib动态代理(FastClass调用)虽然避免了反射调用的开销,但会增加代理类生成的时间和内存开销,同样会影响性能
  • 只能代理公共方法,不能代理私有方法和final方法
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。