设计模式:代理模式 - 控制访问与增强功能的艺术

一、为什么使用代理模式?

在开发中,你是否遇到过以下问题:

• 某些功能调用需要权限校验,但不希望修改核心逻辑?

• 某些对象的创建开销过高,希望延迟加载以优化性能?

• 在不改变原始类的情况下,如何在调用前后插入日志或监控代码?

这些场景中,代理模式提供了一种优雅的解决方案。

二、什么是代理模式?

代理模式(Proxy Pattern)属于结构型设计模式,其核心思想是提供一种代理以控制对目标对象的访问。代理对象在客户端和目标对象之间起到中介的作用,并可以在不改变客户端代码的情况下增强或控制对目标对象的访问。从而有效提升系统的扩展性与灵活性。

代理模式的模型结构

1.抽象主题接口(Subject):定义目标对象和代理对象的公共方法。

2.真实主题(RealSubject):实现抽象主题接口,包含实际的业务逻辑。

3.代理类(Proxy):实现抽象主题接口,包含对真实主题的引用,从而操作真实主题并可提供访问控制和功能增强。

三、代理模式的主要类型及示例

代理模式主要类型

1.静态代理

特点:需要显示创建代理类,在编译期间确定代理类和目标类的绑定关系。

适用场景:功能明确、需求稳定的场景,如日志记录或权限校验等。

2.动态代理

特点:无需显示生成代理类,运行时会动态生成代理类。

适用场景:需要更高灵活性的场景

动态代理的2种实现机制:

• JDK 动态代理:通过 Java 反射机制实现,要求目标类实现接口。

• CGLib 动态代理:通过生成目标类的子类实现,无需目标类实现接口。

3.虚拟代理

特点:通过虚拟代理延迟实例化目标对象,仅在真正需要时才加载资源。 从而提高资源利用率和性能。

适用场景:对象的创建需要较大开销的场景 ,如大图片、视频加载等。

1.静态代理示例:对支付功能增加日志记录功能

第一步:定义支付接口 (目标功能)

java 复制代码
// 定义公共接口
public interface Payment {
    void pay(double amount);
}

第二步:创建真实支付类

java 复制代码
// 实现真实对象
public class RealPayment implements Payment {
    @Override
    public void pay(double amount) {
        System.out.println("支付金额:" + amount + "元");
    }
}

第三步:创建代理支付类

实现支付接口,增加日志功能

java 复制代码
// 静态代理类
public class PaymentProxy implements Payment {
    private final Payment realPayment;
    public PaymentProxy(Payment realPayment) {
        this.realPayment = realPayment;
    }
    @Override
    public void pay(double amount) {
        // 在方法调用前后添加额外逻辑
        System.out.println("[代理日志] 支付前检查用户权限...");
        realPayment.pay(amount);
        System.out.println("[代理日志] 支付完成,记录支付日志。");
    }
}

第四步:客户端测试代码

通过代理对象调用真实对象的支付方法,隔离了客户端和真实对象。

java 复制代码
// 客户端测试
public class ProxyTest {
    public static void main(String[] args) {
        Payment payment = new PaymentProxy(new RealPayment());
        payment.pay(100.0);
    }
}

输出结果:

bash 复制代码
[代理日志] 支付前检查用户权限...
支付金额:100.0元
[代理日志] 支付完成,记录支付日志。

2. 虚拟代理示例:图片延迟加载

通过虚拟代理延迟图片的创建,从而优化资源使用和性能。

第一步:定义图片接口

bash 复制代码
// 定义图片接口
public interface Image {
    void display(); // 显示图片
}

第二步:构建真实图片类

实现图片的加载和显示功能

java 复制代码
// 真实图片类
public class RealImage implements Image {
    private String fileName;
    //构造函数
    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk(); 
    }
    //模拟从磁盘加载图片
    private void loadFromDisk() {
        System.out.println("Loading " + fileName);
    }
    @Override
    public void display() {
        System.out.println("Displaying " + fileName);
    }
}

第三步:构建图片代理类

实现图片接口,并对真实图片类的创建进行控制。

java 复制代码
// 图片代理类
public class ProxyImage implements Image {

    // 持有对真实图片对象的引用
    private RealImage realImage;

    // 保存图片文件名,用于真实图片对象的创建
    private String fileName;

    // 构造函数,初始化代理类时传入图片文件名
    public ProxyImage(String fileName) {
        this.fileName = fileName;
    }

    @Override
    // 实现 display 方法,增加延迟加载功能
    public void display() {
        // 检查 realImage 是否为 null
        if (realImage == null) {
            // 只有realImage未创建时,才实例化。
            realImage = new RealImage(fileName);
        }
        // 调用真实图片对象的 display 方法
        realImage.display();
    }
}

第四步:测试代码,客户端调用

java 复制代码
// 客户端调用
public class ProxyPatternDemo {
    public static void main(String[] args) {
        Image image1 = new ProxyImage("image1.jpg");
        Image image2 = new ProxyImage("image2.jpg");
        // 第一次调用 display,会触发图片加载
        image1.display();
        System.out.println("---");
        // 第二次调用 display,不会重复加载
        image1.display();
        System.out.println("---");
        // 另一张图片首次调用,会触发图片加载
        image2.display();
    }
}

运行结果:

bash 复制代码
Loading image1.jpg  
Displaying image1.jpg  
---  
Displaying image1.jpg  
---  
Loading image2.jpg  
Displaying image2.jpg 

代码说明

1.Image 接口:定义图片的通用行为display()。

2.RealImage 类:真实图片类,实现了图片的真实加载和显示逻辑。

3.ProxyImage 类:代理图片类,控制真实图片类的加载和显示。

4.客户端代码:通过代理图片类调用真实图片类的display()方法,无需关心加载细节。

延迟加载机制:

• realImage在代理对象ProxyImage初始化时并未创建。

• ProxyImage会在首次显示图片时创建实例RealImage。

• 后续再调用display()方法时,因为真实图片类已创建,不会再次实例化加载图片。从而降低不必要的资源消耗。

如上示例代码已详细展示了如何在不同场景中应用代理模式,增强功能同时确保代码结构清晰。

3、动态代理 !!!!

动态代理通过"运行时动态生成代理类",可帮助开发者快速解决功能增强和接口代理中的重复性问题,从而提升代码灵活性和开发效率。

注:

• AOP(面向切面编程):

Spring 的 AOP 框架,通过注解或配置实现功能增强,比动态代理更直观一些,尤其是在大型项目中。

• 模板模式:

通过定义模板方法,将重复逻辑抽象到父类中,也减少每个接口的重复实现。

动态代理模式是一种通过在运行时动态生成代理类来实现目标对象功能扩展的设计模式。它与静态代理的最大区别在于:动态代理无需手动编写代理类,代理类由框架或工具动态生成。

目前常见的动态代理实现方式有两种:

1.JDK 动态代理:基于 Java 反射机制,只能代理实现了接口的类。

2.CGLib 动态代理:基于字节码生成技术,可以代理没有实现接口的类。

JDK 动态代理是一种基于 Java 标准库java.lang.reflect包实现的代理方式,可以在运行时动态生成代理对象。代理对象实现和目标类同样的接口,并将方法调用转发给目标对象,同时可以在方法调用前后执行额外的增强处理。

生成流程:

1.创建代理类工厂

在调用Proxy.newProxyInstance方法时,动态生成代理类,该代理类会实现目标接口,并将方法调用委托给一个实现了InvocationHandler接口的实例。

2.定义 InvocationHandler 接口

InvocationHandler是 JDK 动态代理的核心接口。它只有一个invoke方法,当代理对象的方法被调用时,invoke方法会被自动触发。此方法接收以下三个参数:

• proxy:当前生成的代理对象;

• method:目标对象中被调用的方法;

• args:方法调用的参数数组。

3.调用代理对象的方法

当调用代理对象的方法时,InvocationHandler的invoke方法会被自动调用。开发者可以在invoke方法中添加增强逻辑(如日志记录、权限检查等),然后通过反射调用目标对象的方法。

4.通过反射执行目标方法

在invoke方法中,开发者可以使用反射机制调用目标对象的方法,并在调用前后插入额外的逻辑处理。最终,将目标方法的返回值返回给调用者。

JDK 动态代理通过这种机制实现了对目标方法调用的灵活控制,适合需要对实现了接口的类进行动态增强的场景。

JDK 动态代理实例

场景:在支付功能中动态添加日志。

步骤 1:定义接口

定义一个公共接口,供目标类和代理类实现。这是 JDK 动态代理的前提。

java 复制代码
public interface Payment {
    void pay(int amount);
}

步骤 2:实现目标类

目标类实现接口,提供具体业务逻辑。

java 复制代码
public class RealPayment implements Payment {
    @Override
    public void pay(int amount) {
        System.out.println("Processing payment of $" + amount);
    }
}

步骤 3:实现 InvocationHandler

定义一个代理处理器类,实现InvocationHandler接口,用于自定义增强逻辑。

java 复制代码
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PaymentInvocationHandler implements InvocationHandler {
    private final Payment realPayment;

    // 构造方法,接收目标对象
    public PaymentInvocationHandler(Payment realPayment) {
        this.realPayment = realPayment;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          /**
     * @param proxy  动态生成的代理对象实例,通常很少直接使用。
     * @param method 目标对象中被调用的方法。
     * @param args   方法的参数数组。
     */
  
        // 增强逻辑:方法调用前
        System.out.println("[Log] Starting payment...");

        // 调用目标方法
        Object result = method.invoke(realPayment, args);

        // 增强逻辑:方法调用后
        System.out.println("[Log] Payment finished!");

        return result;
    }
}

步骤 4:创建代理对象

通过Proxy.newProxyInstance方法生成代理对象。

java 复制代码
import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        // 创建目标对象
        Payment realPayment = new RealPayment();

        // 创建代理对象
        Payment proxyPayment = (Payment) Proxy.newProxyInstance(
            realPayment.getClass().getClassLoader(), // 类加载器
            realPayment.getClass().getInterfaces(),  // 目标对象实现的接口
            new PaymentInvocationHandler(realPayment) // 动态代理处理器
        );

        // 调用代理对象的方法
        proxyPayment.pay(100);
    }
}

以下是Proxy.newProxyInstance的三个参数及其含义:

1.realPayment.getClass().getClassLoader()

• 类加载器,传入目标对象的类加载器。

2.realPayment.getClass().getInterfaces()

• 目标对象实现的接口列表。

• 代理类会实现这些接口,从而与目标对象拥有相同的方法。

3.PaymentInvocationHandler(realPayment)

• 动态代理处理器,负责拦截代理对象的方法调用。

•InvocationHandler中定义了增强逻辑和目标方法的调用。

运行效果

运行上述代码,输出如下:

java 复制代码
[Log] Starting payment...
Processing payment of $100
[Log] Payment finished!

CGLib(Code Generation Library)是一个基于 ASM(Java 字节码操作框架)实现的动态代理库。它的工作原理是,在程序运行时动态生成目标类的子类,并在子类中拦截目标方法的调用,从而实现代理功能。与 JDK 动态代理不同,CGLib 不需要目标类实现接口,而是直接通过继承目标类来创建代理类。所以可以代理没有实现接口的类。

生成流程:

1.创建Enhancer对象

CGLib 使用Enhancer类来生成代理类。通过创建Enhancer对象并设置目标类和拦截器等参数,最终生成代理类。

2.设置代理逻辑(回调拦截器)

实现MethodInterceptor接口,定义拦截逻辑。该接口的intercept方法会在每次调用代理类的方法时触发,开发者可以在intercept方法中添加增强功能,如日志记录、权限验证等。

3.动态生成代理类

CGLib 在运行时动态生成目标类的子类作为代理类。生成的代理类会继承目标类的所有方法,并在每个方法执行时触发intercept方法。

4.调用代理对象的方法

当调用代理对象的方法时,MethodInterceptor中的intercept方法会被触发,即增强逻辑(如日志记录、权限验证等)执行,目标类的实际方法通过MethodProxy.invokeSuper调用。

CGLib 动态代理实例

场景:动态代理一个没有实现接口的类,为其添加日志功能。

代码实现

步骤 1:定义目标类

目标类不需要实现接口,可以直接定义具体方法。

java 复制代码
// 一个没有实现接口的普通类
public class UserService {
    // 模拟添加用户的操作
    public void addUser(String username) {
        System.out.println("添加用户:" + username);
    }

    // 模拟删除用户的操作
    public void deleteUser(String username) {
        System.out.println("删除用户:" + username);
    }
}

步骤 2:创建 CGLib 动态代理拦截器

创建一个类实现MethodInterceptor接口,用于定义增强逻辑。

java 复制代码
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

// 定义拦截器,为目标类的方法添加日志功能
public class LoggingInterceptor implements MethodInterceptor {

    /**
     * @param obj    代理对象本身(动态生成的代理类实例)
     * @param method 目标类的方法
     * @param args   目标方法的参数数组,按顺序包含每个参数值。
     * @param proxy  方法代理,用于调用目标类(父类)方法的代理工具
     * @return 方法执行结果
     * @throws Throwable 可能抛出的异常
     */ 
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 在方法调用前添加日志
        System.out.println("[日志] 调用方法:" + method.getName() + ",参数:" + java.util.Arrays.toString(args));

        // 调用目标类的原始方法
        Object result = proxy.invokeSuper(obj, args);

        // 在方法调用后添加日志
        System.out.println("[日志] 方法执行完成:" + method.getName());
        return result;
    }
}

代码解析

1.proxy.invokeSuper(obj, args):调用目标类的原始方法。

2.intercept方法参数:

步骤3:代理工厂

java 复制代码
import net.sf.cglib.proxy.Enhancer;

// 创建代理对象的工厂类
public class CGLibProxyFactory {
    /**
     * 获取代理对象
     *
     * @param clazz 需要代理的目标类
     * @return 代理对象
     */
    public <T> T getProxy(Class<T> clazz) {
        // 创建 Enhancer 对象,用于生成代理类
        Enhancer enhancer = new Enhancer();

        // 设置目标类为代理类的父类
        enhancer.setSuperclass(clazz);

        // 设置回调拦截器,用于增强目标类的方法
        enhancer.setCallback(new LoggingInterceptor());

        // 创建代理对象
        return (T) enhancer.create();
    }
}

代码解析:

代理工厂 (CGLibProxyFactory):

• 使用 CGLib 的Enhancer创建动态代理对象。

• 设置目标类为代理类的父类,并将拦截器传递给Enhancer。

步骤4:测试代码

创建代理对象,调用目标方法时,会自动触发拦截器中的intercept方法。

java 复制代码
public class Client {
    public static void main(String[] args) {
        // 使用代理工厂生成代理对象
        CGLibProxyFactory proxyFactory = new CGLibProxyFactory();
        UserService userServiceProxy = proxyFactory.getProxy(UserService.class);

        // 调用代理对象的方法
        userServiceProxy.addUser("Alice");
        System.out.println();
        userServiceProxy.deleteUser("Alice");
    }
}

CGLib实现的思路总结

1.定义目标类

目标类无需实现接口,直接定义业务逻辑。

2.创建拦截器

实现MethodInterceptor接口,编写增强逻辑。

3.生成代理对象

使用Enhancer设置目标类和拦截器,生成代理对象。

4.调用代理方法

代理对象的方法调用会触发拦截器,实现动态代理逻辑。

四、代理模式的价值

对开发的价值

1.单一职责:代理模式将对象的核心功能与附加功能(如延迟加载)解耦,优化代码结构。

2.扩展性强:通过代理类新增功能,无需修改核心功能类,符合开闭原则

代理模式有如下缺点:

1.增加复杂度:引入代理类,可能导致维护难度增加。

2.性能损耗:代理访问会增加额外开销。

动态代理:
动态代理的价值

1.提高代码复用性

动态代理可以统一处理多个类的公共逻辑,减少重复代码。

2.增强系统扩展性

在不修改目标类的情况下动态扩展功能,符合开闭原则。

3.简化代理类生成

通过运行时生成代理类,避免手动编写静态代理类。

4.便于模拟与隔离

动态代理可以轻松模拟接口行为,方便单元测试和集成测试。

动态代理的缺点

1.性能开销较大,依赖反射机制动态代理依赖反射机制来生成代理类、执行方法,尽管反射提供了强大的灵活性,但其性能开销较大。在高频次调用的场景下,相较于直接方法调用,动态代理的性能会有所下降,并可能引发类型安全问题。

2.增加代码复杂性,调试较困难动态代理虽然简化了代码编写,但也带来了额外的复杂性。在调试和追踪时,尤其是使用第三方库(如 Spring AOP 或 CGLib)时,代理的行为不容易直观理解,调试代理方法比直接调试目标方法更具挑战性。

3.可能导致内存泄漏如果代理对象未被正确释放,或者引用关系处理不当,可能导致内存泄漏。代理对象和目标对象之间的引用可能无法被垃圾回收器识别,导致内存无法及时释放。

五、代理模式的适用场景

1.远程代理:为远程对象提供本地代理,简化分布式系统中远程方法的调用。

2.虚拟代理:延迟加载高开销资源,优化性能。

3.安全代理:控制敏感资源的访问权限。

4.缓存代理:缓存高频访问的数据,减少重复计算或查询。

5.日志代理:自动记录操作日志。

6.增强功能代理:动态扩展事务、监控等功能。

动态代理

1.权限校验如用户角色验证、操作权限控制。

2.日志记录为方法调用添加日志,便于监控和调试。

3.事务管理在方法调用前后添加事务处理逻辑。

4.性能监控动态收集方法调用的性能数据。

5.接口适配动态创建接口实现,减少手动实现的工作量。

六、总结

代理模式的核心价值在于:通过引入代理对象,解耦目标对象与客户端,在保留系统灵活性的同时,为目标对象添加功能增强或行为控制,最终提升系统的可扩展性与稳定性。

动态代理是实现灵活功能扩展的有力工具,通过在运行时生成代理类,解决了静态代理代码冗余的问题。无论是JDK 动态代理还是CGLib 动态代理,它们都能够有效解耦客户端和目标对象,为系统提供更强的扩展性和灵活性。然而,在使用动态代理时,也需要权衡其可能带来的性能开销、代码复杂性和内存管理问题。因此,在设计时要根据具体场景谨慎选择代理方式,充分考虑这些潜在缺点。

实践建议:

• 避免代理类过多导致系统复杂度增加。

• 简单场景谨慎使用,避免过度设计。

相关推荐
大鹏dapeng1 小时前
使用gone v2 的 Provider 机制升级改造 goner/xorm 的过程记录
后端·设计模式·go
快乐源泉2 小时前
【设计模式】参数校验逻辑复杂,代码长?用责任链
后端·设计模式·go
天堂的恶魔9462 小时前
C++项目 —— 基于多设计模式下的同步&异步日志系统(2)(工厂模式)
开发语言·c++·设计模式
小猪乔治爱打球3 小时前
[Golang修仙之路] 策略模式
设计模式
chushiyunen3 小时前
设计模式-观察者模式和发布订阅模式区别
观察者模式·设计模式
浅陌sss4 小时前
设计模式 --- 观察者模式
设计模式
程序员JerrySUN4 小时前
设计模式 Day 9:命令模式(Command Pattern)完整讲解与实战应用
设计模式·命令模式
小马爱打代码14 小时前
设计模式:依赖倒转原则 - 依赖抽象,解耦具体实现
设计模式
Koma-forever14 小时前
java设计模式-适配器模式
java·设计模式·适配器模式
自在如风。16 小时前
Java 设计模式:原型模式详解
java·设计模式·原型模式