掘金 后端 ( ) • 2024-04-23 10:42

1. 代理模式介绍

代理模式是一种结构型设计模式,它允许一个对象(代理对象)控制另一个对象的访问。代理对象通常充当客户端和实际对象之间的中介,用于对实际对象的访问进行控制、监控或其他目的。在Java中,代理模式常用于以下情境:

  1. 虚拟代理(Virtual Proxy): 当对象的创建和初始化需要大量资源时,可以使用虚拟代理延迟对象的实例化,只有在真正需要时才创建和初始化对象。这有助于提高系统的性能。
  2. 保护代理(Protection Proxy): 用于控制对真实对象的访问权限。通过代理对象添加一些验证逻辑,确保客户端在访问真实对象之前满足一定条件。
  3. 远程代理(Remote Proxy): 用于在不同地址空间中代表对象。当对象存在于不同的地址空间时,可以使用远程代理在客户端和服务器之间进行通信。
  4. 缓存代理(Caching Proxy): 用于为频繁访问的对象提供缓存,以减少对真实对象的访问次数,提高系统性能。

2. 关键思想

代理模式的关键思想是引入一个代理对象来控制对另一个对象的访问。代理对象充当客户端和实际对象之间的中介,可以用于控制对实际对象的访问权限、执行附加操作,或者延迟实际对象的创建和初始化。

以下是代理模式的一些关键思想:

  1. 控制访问: 代理对象可以控制对实际对象的访问,限制或验证客户端的请求,确保只有符合特定条件的请求才能访问真实对象。
  2. 延迟加载(懒加载): 代理模式可以延迟实际对象的创建和初始化,只有在需要的时候才真正创建对象。这有助于提高系统性能,特别是当实际对象的创建和初始化成本较高时。
  3. 附加操作: 代理对象可以在调用真实对象之前或之后执行一些额外的操作,例如记录日志、性能监控、缓存数据等。这样,代理对象可以在不修改真实对象的情况下,为系统添加新的功能。
  4. 隔离复杂性: 代理模式可以将一些复杂的业务逻辑隔离到代理对象中,使真实对象保持相对简单和专注于核心功能。这有助于提高代码的可维护性和可读性。

总体而言,代理模式的关键思想是通过引入代理对象,为系统提供一层间接访问,从而实现对实际对象访问的控制和管理,同时增加了灵活性和可扩展性。

3. 实现方式:

代理模式可以通过接口或继承来实现。在Java中,常见的实现方式有静态代理和动态代理。

1. 静态代理:

静态代理是在编译时就已经确定代理关系的方式,代理类是在编译阶段手动创建的。

应用一:假设你有一个网络图片加载的需求,而加载图片可能是一个比较耗时的操作,你希望在加载图片时显示一个加载提示,而加载完成后显示图片。这个场景可以通过静态代理模式来实现。

示例代码一

// 图片接口
interface Image {
    // 显示图片的抽象方法
    void display();
}


// 具体的图片加载类
class RealImage implements Image {
    private String filename;

    // 构造方法,接收图片文件名并加载图片
    public RealImage(String filename) {
        this.filename = filename;
        loadImageFromDisk();
    }

    // 模拟从磁盘加载图片的私有方法
    private void loadImageFromDisk() {
        System.out.println("加载图片:" + filename);
        // 模拟加载图片的耗时操作
    }

    // 实现显示图片的方法
    @Override
    public void display() {
        System.out.println("显示图片:" + filename);
    }
}


// 图片加载代理类
class ProxyImage implements Image {
    private RealImage realImage;
    private String filename;

    public ProxyImage(String filename) {
        this.filename = filename;
    }

    @Override
    public void display() {
        // 在显示图片之前,可以添加一些额外的逻辑,比如显示加载提示
        System.out.println("加载图片:" + filename + " (通过代理)");

        // 创建真实对象(懒加载)
        if (realImage == null) {
            realImage = new RealImage(filename);
        }

        // 调用真实对象的显示方法
        realImage.display();
    }
}

public class ProxyExample {
    public static void main(String[] args) {
        // 使用代理对象
        Image image = new ProxyImage("sample.jpg");

        // 第一次显示,会触发代理对象加载图片
        image.display();

        // 第二次显示,不会再次加载图片,直接显示
        image.display();
    }
}

在这个例子中,ProxyImage充当了代理对象,它在显示图片之前可以添加加载提示的逻辑,并在需要时创建和初始化真实的RealImage对象。这样,使用代理对象来加载图片的过程中,可以灵活地控制和附加一些额外的操作。

应用二:假设你在设计一个权限管理系统,需要确保只有经过授权的用户才能访问敏感信息。这时可以使用静态代理模式来实现。

示例代码二

// 授权接口
interface AuthorizedUser {
    // 访问敏感信息的抽象方法
    void accessSensitiveInfo();
}

// 具体的用户类
class RealUser implements AuthorizedUser {
    private String username;

    // 构造方法,接收用户名
    public RealUser(String username) {
        this.username = username;
    }

    // 实现访问敏感信息的方法
    @Override
    public void accessSensitiveInfo() {
        System.out.println(username + "正在访问敏感信息。");
    }
}

// 授权代理类
class AuthorizedUserProxy implements AuthorizedUser {
     // 真实用户对象
    private RealUser realUser;
    // 用户名
    private String username;
    // 是否已经获得授权
    private boolean isAuthorized;

    // 构造方法,接收用户名
    public AuthorizedUserProxy(String username) {
        this.username = username;
    }

    // 模拟授权的过程
    public void grantAccess() {
        // 假设只有特定用户才能获得授权
        if ("admin".equals(username)) {
            isAuthorized = true;
            realUser = new RealUser(username);
            System.out.println(username + "已获得访问权限。");
        } else {
            isAuthorized = false;
            System.out.println(username + "无权访问敏感信息。");
        }
    }

    // 实现访问敏感信息的方法
    @Override
    public void accessSensitiveInfo() {
        if (isAuthorized) {
            // 在访问敏感信息之前可以添加一些额外的逻辑
            System.out.println("检查 " + username + " 的访问日志");

            // 调用真实对象的方法
            realUser.accessSensitiveInfo();
        } else {
            System.out.println(username + "无权访问敏感信息。");
        }
    }
}


public class ProxyExample {
    public static void main(String[] args) {
        // 创建代理对象
        AuthorizedUserProxy userProxy = new AuthorizedUserProxy("user1");

        // 尝试访问敏感信息,但未授权
        userProxy.accessSensitiveInfo();

        // 授权
        userProxy.grantAccess();

        // 再次尝试访问敏感信息,已授权
        userProxy.accessSensitiveInfo();
    }
}

在这个例子中,AuthorizedUserProxy充当了代理对象,控制用户是否被授权访问敏感信息。在授权的过程中,可以添加一些额外的逻辑,如记录访问日志。这样,代理对象在保护敏感信息的同时,还可以附加一些控制和监控的功能。

静态代理是在编译时就确定代理关系的方式,代理类是在编译阶段手动创建的。以下是静态代理的要点和注意事项:

要点:

  1. 代理接口: 定义一个代理接口,包含与被代理对象相同的方法,使得代理类和真实对象实现相同的接口。
  2. 真实对象: 创建一个真实对象的类,实现代理接口。
  3. 代理类: 创建一个代理类,也实现代理接口。在代理类中,持有一个真实对象的引用,并在代理方法中调用真实对象的方法,可以在调用前后执行额外的操作。
  4. 客户端: 在客户端通过实例化代理对象来使用。客户端只需要与代理类打交道,无需直接与真实对象交互。

注意事项:

  1. 接口一致性: 代理类和真实对象必须实现相同的接口,以保持一致性。
  2. 耦合度高: 静态代理的实现过程中,代理类需要显式地引用真实对象。这导致了代理类和真实对象的耦合度较高,一旦接口发生变化,代理类也需要相应修改。
  3. 每个接口方法都要代理: 如果接口中的方法很多,而只想代理其中的一部分方法,就需要在代理类中实现这些方法,即使不做额外的处理。
  4. 不适用于大规模应用: 当需要代理的类较多,而且需要对它们进行不同的代理操作时,会导致代码冗余。
  5. 维护困难: 如果多个代理类需要共享一些公共的逻辑,或者逻辑发生改变,需要修改所有相关的代理类。
  6. 无法实现横切关注点复用: 静态代理在代理类中硬编码了额外的逻辑,如果这些逻辑需要被多个类共享,就会导致代码的重复。

静态代理是一种简单直观的代理方式,但在某些情况下可能不够灵活和可维护。为了解决一些静态代理的缺点,可以考虑使用动态代理。

优点:

  1. 简单直观: 静态代理的实现相对简单,易于理解和掌握。
  2. 编译时检查: 代理类在编译时就已经存在,因此编译时就能检测到代码的错误,有助于提前发现问题。
  3. 可控性强: 由于代理类是手动创建的,开发人员可以更加精确地控制代理过程,灵活应对需求变化。

缺点:

  1. 代码冗余: 每个需要代理的类都需要创建一个代理类,导致代码冗余。当接口变化时,所有的代理类也需要相应修改。
  2. 维护困难: 如果有多个类需要代理,而这些类有一些公共逻辑,对公共逻辑的修改会导致所有代理类的修改,维护起来相对困难。
  3. 灵活性差: 静态代理在代理类中硬编码了额外的逻辑,对于不同的需求可能需要创建不同的代理类,缺乏灵活性。

适用场景:

  1. 小规模应用: 静态代理适用于较小规模的应用,当代理类的数量相对较少且代理逻辑不复杂时,能够简洁明了地实现代理。
  2. 简单业务逻辑: 当代理逻辑相对简单,不需要频繁变更时,静态代理能够满足需求。
  3. 编译时确定代理关系: 如果代理关系在编译时已经确定,而且不需要动态改变,静态代理是一种合适的选择。
  4. 需要在代理中加入特定逻辑: 当需要在代理中加入一些特定的逻辑,而这些逻辑不同于业务逻辑,静态代理可以提供一个清晰的切入点。

总体而言,静态代理适用于一些简单场景,但在大规模应用、复杂业务逻辑以及频繁变更代理关系的情况下,可能会显得不够灵活和可维护。在这种情况下,动态代理可能更为合适。

2. 动态代理:

动态代理是在运行时创建代理类的方式,通常使用Java的java.lang.reflect.Proxy类和InvocationHandler接口。

应用一:假设你有一个网络图片加载的需求,而加载图片可能是一个比较耗时的操作,你希望在加载图片时显示一个加载提示,而加载完成后显示图片。这个场景可以通过动态代理模式来实现。

示例代码一

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

// 图片接口
interface Image {
    // 显示图片的抽象方法
    void display();
}


// 具体的图片加载类
class RealImage implements Image {
    private String filename;

    // 构造方法,接收图片文件名并加载图片
    public RealImage(String filename) {
        this.filename = filename;
        loadImageFromDisk();
    }

    // 模拟从磁盘加载图片的私有方法
    private void loadImageFromDisk() {
        System.out.println("加载图片:" + filename);
        // 模拟加载图片的耗时操作
    }

    // 实现显示图片的方法
    @Override
    public void display() {
        System.out.println("显示图片:" + filename);
    }
}

// 图片加载的 InvocationHandler
class ImageLoadingHandler implements InvocationHandler {
    // 真实图片加载对象
    private Object realImage;

    // 构造方法,接收真实图片加载对象
    public ImageLoadingHandler(Object realImage) {
        this.realImage = realImage;
    }

    // invoke 方法,定义代理对象的行为
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 在显示图片之前,可以添加一些额外的逻辑,比如显示加载提示
        System.out.println("加载图片(通过代理)");

        // 调用真实对象的方法
        return method.invoke(realImage, args);
    }
}


public class ImageLoadingProxyExample {
    public static void main(String[] args) {
        // 创建真实对象
        Image realImage = new RealImage("sample.jpg");

        // 创建图片加载的 InvocationHandler
        ImageLoadingHandler handler = new ImageLoadingHandler(realImage);

        // 创建代理对象
        Image proxy = (Image) Proxy.newProxyInstance(
                Image.class.getClassLoader(),
                new Class[]{Image.class},
                handler
        );

        // 使用代理对象
        proxy.display();
    }
}

应用二:假设你在设计一个权限管理系统,需要确保只有经过授权的用户才能访问敏感信息。这时可以使用动态代理模式来实现。

示例代码二

// 授权接口
interface AuthorizedUser {
    // 访问敏感信息的抽象方法
    void accessSensitiveInfo();
}

// 具体的用户类
class RealUser implements AuthorizedUser {
    private String username;

    // 构造方法,接收用户名
    public RealUser(String username) {
        this.username = username;
    }

    // 实现访问敏感信息的方法
    @Override
    public void accessSensitiveInfo() {
        System.out.println(username + "正在访问敏感信息。");
    }
}

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

// 权限控制的 InvocationHandler
class AuthorizationHandler implements InvocationHandler {
    // 真实用户对象
    private Object realUser;

    // 构造方法,接收真实用户对象
    public AuthorizationHandler(Object realUser) {
        this.realUser = realUser;
    }

    // invoke 方法,定义代理对象的行为
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 在访问敏感信息之前可以添加一些额外的逻辑
        System.out.println("检查 " + method.getName() + " 的访问日志");

        // 调用真实对象的方法
        return method.invoke(realUser, args);
    }
}


import java.lang.reflect.Proxy;

public class AuthorizationProxyExample {
    public static void main(String[] args) {
        // 创建真实对象
        AuthorizedUser realUser = new RealUser("admin");

        // 创建权限控制的 InvocationHandler
        AuthorizationHandler handler = new AuthorizationHandler(realUser);

        // 创建代理对象
        AuthorizedUser proxy = (AuthorizedUser) Proxy.newProxyInstance(
                AuthorizedUser.class.getClassLoader(),
                new Class[]{AuthorizedUser.class},
                handler
        );

        // 使用代理对象
        proxy.accessSensitiveInfo();
    }
}

动态代理的基本原理:

动态代理的原理基于 Java 的反射机制。Java 提供了 java.lang.reflect 包,其中的 Proxy 类和 InvocationHandler 接口是实现动态代理的关键。

在这个过程中,Proxy.newProxyInstance 方法会动态生成一个代理类,这个代理类实现了指定的接口,并在调用接口方法时通过 InvocationHandlerinvoke 方法来处理方法调用。代理类的字节码是在运行时生成的,它实现了指定的接口并通过 InvocationHandler 的实现来调用真实对象的方法,并在调用前后添加了额外的逻辑。这种机制使得我们可以在运行时动态地创建代理类,而不需要预先定义代理类的源代码。

要点:

  1. 接口: 动态代理通常要求被代理的类实现一个接口,因为动态代理是基于接口的。
  2. InvocationHandler: 创建一个实现 InvocationHandler 接口的类,用于定义代理对象的行为。
  3. Proxy.newProxyInstance: 使用 Proxy.newProxyInstance 方法创建代理对象。这个方法接收三个参数:ClassLoaderClass[] (代理类需要实现的接口),和 InvocationHandler
  4. 动态生成代理类: 代理类是在运行时动态生成的,它实现了指定的接口并在方法调用时调用 InvocationHandlerinvoke 方法。

注意事项:

  1. 仅支持接口代理: Java的动态代理机制只能代理接口,不能代理类。这是因为一个类只能继承一个类,但是可以实现多个接口,因此动态代理基于接口实现更为灵活。
  2. 无法处理 final 类和方法: 动态代理无法代理 final 类和 final 方法,因为不能生成继承于 final 类或覆盖 final 方法的代理类。
  3. 性能开销: 动态代理相比于静态代理,由于运行时生成代理类,可能会带来一些性能开销,尤其是在大规模应用中。但在实际应用中,这种性能开销一般是可以接受的。
  4. 无法处理直接调用本类方法: 如果代理类的方法直接调用本类的其他方法,而不是通过代理对象调用,那么这些方法调用不会被代理,也就是说,代理对象无法拦截本类中的方法调用。
  5. equals 和 hashCode 方法: 如果代理类未覆盖 equalshashCode 方法,可能导致在使用代理对象时出现意外的行为。通常建议在 InvocationHandler 中处理这两个方法。
  6. ClassLoader 选择: 选择适当的 ClassLoader 对象是很重要的,因为代理类的字节码是由 ClassLoader 加载的。通常,使用被代理类的类加载器即可。

总体而言,动态代理是一种强大的工具,适用于需要在运行时动态生成代理类的场景,但在使用时需要注意其限制和潜在的性能开销。

优点:

  1. 灵活性: 动态代理使得在运行时能够动态生成代理类,相比静态代理更加灵活。不需要预先定义代理类的源代码,而是在运行时生成,更适应变化的需求。
  2. 代码复用: 由于代理逻辑集中在 InvocationHandler 中,可以复用同一个 InvocationHandler 实现,减少代码冗余。
  3. 透明性: 使用动态代理,可以使代理对象对真实对象的调用显得更加透明,客户端无需关心代理的存在。

缺点:

  1. 性能开销: 动态代理在运行时生成代理类的字节码,可能会引入一些性能开销。尤其是在频繁调用的场景下,可能对性能产生一定影响。
  2. 仅支持接口代理: Java动态代理机制只支持代理接口,无法直接代理类。这对于一些类而言可能是一个限制。
  3. 无法处理 final 类和方法: 由于动态代理是基于接口实现的,无法代理 final 类和 final 方法。

应用场景:

  1. AOP(面向切面编程): 动态代理常用于实现面向切面编程,通过代理对象对原有对象的方法进行增强,如日志记录、性能监控等。
  2. 事务处理: 在事务处理中,动态代理可以在方法执行前后进行事务的开启和提交,实现事务管理。
  3. 权限控制: 可以使用动态代理对方法进行权限控制,例如检查用户是否有执行某个方法的权限。
  4. 远程调用(RMI): 在远程方法调用中,动态代理可以用于生成远程对象的代理,隐藏网络通信的细节。
  5. 延迟加载: 在需要时才加载对象,可以使用动态代理实现延迟加载,减少系统启动时的资源消耗。

总体而言,动态代理在很多场景下都是非常有用的工具,特别是在需要在运行时动态生成代理类的情况下,提供了更大的灵活性和可维护性。然而,在一些对性能要求极高的场景,可能需要谨慎使用。