Java 代理模式

441 阅读19分钟

一、引言

在 Java 编程的广阔天地里,设计模式宛如璀璨星辰,照亮我们构建高效、可维护代码的道路,而代理模式则是其中一颗耀眼的明星🌟。

想象一下,你心仪一款限量版运动鞋,却无奈官方店铺缺货。此时,代购就如同代理,代替你去寻觅货源,不仅帮你拿到心仪之物,还可能提供诸如真伪鉴定、物流跟踪等额外服务,让购物体验更加完美。在 Java 世界中,代理模式发挥着异曲同工之妙,它作为一种结构型设计模式,为对象提供了替身,让我们能更灵活、高效地操控对象的访问与功能。

从日常的 Web 开发,到大型企业级项目,代理模式的身影随处可见。它既能助力我们优化系统性能,又能巧妙地解耦模块间的复杂关系,使得代码架构更加清晰、稳固。接下来,让我们一同深入 Java 代理模式的奇妙世界,揭开它神秘的面纱,探寻其中的奥秘与魅力。

二、代理模式是什么

代理模式,作为一种设计模式,其核心要义在于为其他对象提供一种代理,以此来控制对该对象的访问。用更通俗的话来讲,就是当我们不方便或者不应该直接与某个对象打交道时,引入一个 “中间人”—— 代理对象,由它来帮我们处理与目标对象的交互。 想象一下娱乐圈的场景,明星们事务繁忙,日程满满。当有商业演出邀约时,明星本人通常不会直接与主办方洽谈所有细节。这时,经纪人就闪亮登场,作为明星的代理,经纪人先与主办方沟通演出时间、地点、报酬等诸多事宜,筛选出合适的机会后,再向明星汇报。明星只需根据经纪人提供的信息,做出是否参演的决定,后续的合同签订、行程安排等琐碎但重要的事务,依旧由经纪人负责操办。在这个过程中,主办方与经纪人互动,间接影响明星的决策与行动,经纪人就是明星在商业演出事务上的代理,有效屏蔽了外界对明星的直接干扰,让明星能专注于表演本身。 再把目光投向房产交易市场,租客或购房者想要寻找心仪的房子,往往不会一家家地去联系房东。房产中介作为代理,手握大量房源信息,租客告知中介自己的预算、位置偏好等需求后,中介依据经验筛选出匹配房源,带租客看房、与房东议价,协调双方诉求,促成交易。租客与房东之间的沟通、协商,基本通过中介这个代理来完成,既提高了找房效率,又避免了双方因信息不对称或沟通不畅产生的问题。 通过这些生动的例子,我们能直观地感受到代理模式的关键所在:在不改变目标对象原有功能的基础上,通过代理对象来增强、管控对目标对象的访问,巧妙地实现功能拓展、访问限制等诸多实用目的,让整个系统更加灵活、稳健地运行。

三、静态代理

(一)定义与特点

静态代理,如其名所示,是在代码编译阶段就已然确定的代理模式。在这个模式下,开发人员需手动创建代理类,让它与目标对象实现相同的接口,并在代理类中持有目标对象的引用。如此一来,在运行时,代理对象便能代替目标对象接受客户端的调用,并且可以在调用目标对象方法前后,灵活地添加诸如日志记录、权限校验等额外功能。

打个比方,倘若有一个接口定义了 “工作” 这一抽象行为,其中包含 “编写代码” 的方法。目标对象是一位程序员,他专注于实现 “编写代码” 的核心业务逻辑。而代理对象就像是程序员的助手,同样实现了该接口。当外界调用 “编写代码” 方法时,实际操作会先经过助手这一代理,助手可以在程序员动手编码之前,做好代码规范文档的准备工作,代码编写完成后,助手再整理代码提交记录。

静态代理的优点显而易见,它直观易懂,逻辑清晰,代理类与目标类之间的关系一目了然,易于初学者上手理解设计模式的精妙之处。同时,在一些简单且固定的业务场景下,能够快速实现功能增强,无需复杂的配置或机制。

不过,它的缺点也不容小觑。每一个目标对象都需要对应一个手动编写的代理类,这无疑会导致代码量急剧膨胀,产生大量重复代码。一旦接口发生变更,无论是新增还是修改方法,与之关联的所有代理类和目标类都得跟着修改,维护成本颇高,灵活性较差,难以应对复杂多变的业务需求,犹如为每一个小任务都定制一套专属模具,模具一旦定型,修改起来费时费力。

(二)代码示例

下面我们通过一个简单而具体的示例来深入理解静态代理。假设我们正在开发一个票务系统,有一个卖票的接口 SellTickets,定义如下:

public interface SellTickets {
    void sell();
}

接着,有一个实现了该接口的目标类 TrainStation,代表火车站售票点,它的代码如下:

public class TrainStation implements SellTickets {
    @Override
    public void sell() {
        System.out.println("火车站售票点售出一张票");
    }
}

现在,为了增加一些额外功能,比如提供购票咨询服务,我们创建一个静态代理类 ProxyTicketOffice:

public class ProxyTicketOffice implements SellTickets {
    private TrainStation trainStation;

    public ProxyTicketOffice() {
        this.trainStation = new TrainStation();
    }

    @Override
    public void sell() {
        System.out.println("欢迎咨询购票信息");
        trainStation.sell();
        System.out.println("感谢您的光临,祝您旅途愉快");
    }
}

在上述代码中,ProxyTicketOffice 就是 TrainStation 的静态代理类。它内部持有 TrainStation 的实例,在 sell 方法中,先输出欢迎咨询的信息,然后调用目标对象 TrainStation 的 sell 方法完成售票操作,最后输出感谢信息。客户端调用代码如下:

public class Main {
    public static void main(String[] args) {
        SellTickets proxy = new ProxyTicketOffice();
        proxy.sell();
    }
}

四、动态代理

(一)JDK 动态代理

1. 核心组件

在 Java 的动态代理世界里,JDK 动态代理可谓是一把利器,其核心在于 InvocationHandler 接口和 Proxy 类。

InvocationHandler 接口,宛如一位幕后指挥官,它定义了代理实例的调用处理逻辑。当我们通过代理对象调用方法时,这个调用并不会直接奔向目标对象,而是被转交给 InvocationHandler 接口的 invoke 方法。这个 invoke 方法就像是一个智能调度中心,它接收三个关键参数:proxy,代表代理对象本身,它让你在方法内有机会获取代理对象的一些元信息,不过要小心使用,直接在 invoke 方法内调用 proxy 的其他方法,可能会陷入递归调用的陷阱;method,它是对被调用方法的一种反射抽象,通过它,我们能知晓被调用方法的名称、参数类型等详细信息,从而精准地决定后续处理流程;args,则是实实在在传递给被调用方法的参数值数组,拿着这些参数,我们可以按需进行预处理或传递给目标对象。

而 Proxy 类,无疑是创建代理对象的 “魔法工厂”,它提供的 newProxyInstance 方法更是核心中的核心。这个方法好似拥有神奇魔力,只需传入三个关键要素:类加载器 loader,它负责将生成的代理类加载进 JVM,通常我们会选用目标类的类加载器,确保兼容性与一致性;接口数组 interfaces,明确告知代理类需要实现哪些接口,这些接口就是代理类与外界沟通的桥梁,客户端代码通过这些接口调用方法,进而触发代理逻辑;还有至关重要的 InvocationHandler 对象 h,它将 Proxy 类与 InvocationHandler 接口紧密相连,使得代理对象在方法调用时,能准确无误地找到对应的调用处理逻辑,让整个动态代理流程顺畅运行。

2. 代码实现

接下来,让我们通过代码深入了解。依旧以票务系统为例,假设我们有一个通用的卖票接口 SellTickets:

public interface SellTickets {
    void sell();
}

以及实现该接口的火车站类 TrainStation:

public class TrainStation implements SellTickets {
    @Override
    public void sell() {
        System.out.println("火车站售票点售出一张票");
    }
}

现在,我们创建一个 JDKTicketProxy 类来实现 InvocationHandler 接口,构建动态代理逻辑:

public class JDKTicketProxy implements InvocationHandler {
    private Object target;

    public JDKTicketProxy(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;
    }

    public Object getProxy() {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
}

在上述代码中,JDKTicketProxy 类持有目标对象 target,在 invoke 方法里,我们在调用目标对象方法前后分别添加了友好的提示信息,getProxy 方法则利用 Proxy 类的 newProxyInstance 方法创建并返回代理对象。

客户端调用代码如下:

public class Main {
    public static void main(String[] args) {
        SellTickets trainStation = new TrainStation();
        SellTickets proxy = (SellTickets) new JDKTicketProxy(trainStation).getProxy();
        proxy.sell();
    }
}

运行这段代码,就能看到通过 JDK 动态代理,在不修改 TrainStation 类原有逻辑的基础上,巧妙地为卖票操作增添了额外的信息展示,让整个购票流程更加人性化,也充分展现了 JDK 动态代理在运行时灵活增强对象功能的强大魅力。

(二)CGLIB 动态代理

1. 原理

与 JDK 动态代理基于接口实现不同,CGLIB 动态代理独辟蹊径,采用基于继承的机制来施展它的 “魔法”。它依托强大的字节码技术,在运行时为目标类动态生成一个子类,这个子类可不简单,它就像是目标类的影子武士,不仅继承了目标类的所有方法,还能在关键时刻 “拦截” 父类方法的调用,进而织入我们精心设计的横切逻辑。

想象一下,我们有一个普通的类,没有实现任何接口,它就像一座孤立的小岛,无法直接通过 JDK 动态代理接入更复杂的功能体系。这时,CGLIB 动态代理登场,它深入字节码层面,如同一位神奇的建筑师,依据目标类的蓝图,快速构建出一个功能增强的子类。在这个子类中,通过方法拦截技术,当外部调用父类中被代理的方法时,会先进入到我们自定义的拦截逻辑中,在这里,我们可以像一位精明的管家,对方法调用进行全方位把控,添加诸如权限校验、日志记录、性能统计等诸多额外功能,之后再决定是否放行,调用父类的原始方法完成业务核心逻辑,真正做到悄无声息地为目标类赋能。

2. 适用场景

CGLIB 动态代理的优势在特定场景下展露无遗。当我们面对一些没有接口定义的遗留代码,或者由于架构设计限制无法轻易引入接口时,CGLIB 就成了 “救星”。比如说,我们需要对一个第三方库中的类进行功能扩展,而这个类并没有提供接口供我们使用 JDK 动态代理,此时 CGLIB 就能大显身手,轻松为其创建代理子类,无缝嵌入我们所需的额外逻辑。

不过,需要注意的是,CGLIB 动态代理虽然功能强大,但由于它是基于继承机制,在创建代理对象时,相较于 JDK 动态代理,会消耗更多的系统资源与时间成本。所以,在那些对创建对象性能要求极高、频繁创建代理对象的场景下,需要谨慎权衡使用。但如果是针对单例模式的对象,因其只需在初始化时创建一次代理,后续重复利用,CGLIB 动态代理便能充分发挥其无需接口、灵活扩展的优势,成为优化系统功能的得力助手。

五、应用场景

(一)权限控制

在企业级应用开发中,权限管理至关重要。以用户服务为例,假设有一个 UserService 接口,定义了诸如获取用户信息、修改用户密码等操作方法。

public interface UserService {
    User getUserById(int id);
    void updateUserPassword(int id, String newPassword);
}

而真实的 UserServiceImpl 类实现了这些方法,从数据库查询或更新用户数据。现在,为了保障系统安全,防止非法操作,我们创建一个代理类 SecureUserServiceProxy。

public class SecureUserServiceProxy implements UserService {
    private UserService userService;
    private User currentUser;

    public SecureUserServiceProxy(UserService userService, User currentUser) {
        this.userService = userService;
        this.currentUser = currentUser;
    }

    @Override
    public User getUserById(int id) {
        if (hasReadPermission(id)) {
            return userService.getUserById(id);
        }
        throw new SecurityException("您没有权限获取该用户信息");
    }

    @Override
    public void updateUserPassword(int id, String newPassword) {
        if (hasUpdatePermission(id)) {
            userService.updateUserPassword(id, newPassword);
        } else {
            throw new SecurityException("您没有权限修改该用户密码");
        }
    }

    private boolean hasReadPermission(int id) {
        // 假设这里有复杂的权限校验逻辑,比如当前用户是管理员或者查询的是自己的信息
        return currentUser.getRole().equals("admin") || currentUser.getId() == id;
    }

    private boolean hasUpdatePermission(int id) {
        return currentUser.getRole().equals("admin") || currentUser.getId() == id;
    }
}

在这个代理类中,它持有真实的 UserService 实例和当前登录用户的信息。当客户端调用 getUserById 或 updateUserPassword 方法时,代理类先根据自定义的权限校验规则,判断当前用户是否有权限执行操作。如果有权限,才调用真实服务类的方法,否则抛出异常,有效阻止非法访问,保障系统数据安全,让不同角色的用户只能在权限范围内操作,提升系统的安全性与稳定性。

(二)远程调用

在分布式系统架构日益普及的当下,远程调用已然成为家常便饭。当客户端需要调用远程服务时,代理模式就能大展身手,让整个过程变得轻松便捷。

假设我们有一个电商系统,其中商品服务部署在远程服务器上,本地客户端想要获取商品详情。我们先定义一个 ProductService 远程接口,声明诸如 getProductDetails 等方法。

public interface ProductService extends Remote {
    Product getProductDetails(int productId) throws RemoteException;
}

服务端实现 ProductServiceImpl 类,实现该接口并处理业务逻辑,与数据库交互获取商品数据。

在客户端,我们创建一个本地代理类 RemoteProductServiceProxy,它实现 ProductService 接口。

public class RemoteProductServiceProxy implements ProductService {
    private Registry registry;
    private ProductService remoteService;

    public RemoteProductServiceProxy() throws RemoteException, NotBoundException {
        registry = LocateRegistry.getRegistry("远程服务器IP", 远程服务端口);
        remoteService = (ProductService) registry.lookup("ProductService");
    }

    @Override
    public Product getProductDetails(int productId) throws RemoteException {
        return remoteService.getProductDetails(productId);
    }
}

客户端代码只需与这个本地代理类交互,调用 getProductDetails 方法时,代理类内部负责与远程服务建立连接、发送请求、接收响应,将结果返回给客户端。如此一来,客户端无需了解复杂的网络通信细节,如 Socket 编程、数据序列化与反序列化等,只需像调用本地方法一样调用代理方法,就能轻松获取远程服务的数据,极大提升了开发效率,让分布式系统的开发与维护更加得心应手。

(三)缓存

缓存是优化系统性能的一大利器,代理模式在缓存场景下有着巧妙的应用。

以文件读取为例,假设我们有一个 FileReaderService 接口,定义了 readFile 方法用于读取文件内容。

public interface FileReaderService {
    String readFile(String filePath);
}

真实的 FileReaderServiceImpl 类实现该接口,通过 BufferedReader 等方式从文件系统读取文件内容。

现在创建一个缓存代理类 CachedFileReaderProxy。

public class CachedFileReaderProxy implements FileReaderService {
    private FileReaderService fileReaderService;
    private Map<String, String> cache = new HashMap<>();

    public CachedFileReaderProxy(FileReaderService fileReaderService) {
        this.fileReaderService = fileReaderService;
    }

    @Override
    public String readFile(String filePath) {
        if (cache.containsKey(filePath)) {
            return cache.get(filePath);
        }
        String content = fileReaderService.readFile(filePath);
        cache.put(filePath, content);
        return content;
    }
}

在这个代理类中,维护了一个 cache 映射表,用于存储已读取文件的路径与内容。当客户端调用 readFile 方法时,代理类先检查缓存中是否已存在该文件的内容,如果命中缓存,直接返回缓存数据,避免重复的文件读取操作,大大提高了读取效率,尤其是在频繁读取相同文件的场景下,能显著减少系统开销,提升整体性能。

(四)日志记录

在软件开发过程中,日志记录对于调试、监控系统运行状态起着举足轻重的作用。代理模式能轻松实现日志记录功能,且无需对原有业务类大动干戈。

假设我们有一个业务接口 BusinessService,包含 executeBusinessLogic 方法。

public interface BusinessService {
    void executeBusinessLogic();
}

真实的 BusinessServiceImpl 类专注于实现业务逻辑。

现在创建一个日志记录代理类 LoggingBusinessProxy。

public class LoggingBusinessProxy implements BusinessService {
    private BusinessService businessService;

    public LoggingBusinessProxy(BusinessService businessService) {
        this.businessService = businessService;
    }

    @Override
    public void executeBusinessLogic() {
        Date startTime = new Date();
        System.out.println("业务方法于 " + startTime + " 被调用");
        businessService.executeBusinessLogic();
        Date endTime = new Date();
        System.out.println("业务方法执行完毕,耗时 " + (endTime.getTime() - startTime.getTime()) + " 毫秒");
    }
}

在代理类的 executeBusinessLogic 方法中,在调用真实业务方法前后,分别记录方法开始执行的时间和结束的时间,并输出相应日志信息。这样,开发人员在调试时,能清晰看到方法的调用时机与执行时长,便于排查问题;运维人员在监控系统时,也能依据日志了解业务方法的运行情况,及时发现潜在风险,保障系统稳定运行。

六、代理模式优缺点

(一)优点

代理模式的优点显著,犹如为代码注入了强大的活力与韧性。

首先,它能极大地降低耦合度。在复杂的系统架构中,客户端只需与代理对象交互,对目标对象的具体实现细节全然不知,如同两个紧密咬合的齿轮间加入了一层润滑剂,让彼此的运转更加顺滑。当目标对象的内部逻辑需要调整时,只要代理与目标对象之间的接口保持稳定,客户端代码便无需改动,大大提升了系统的可维护性与扩展性。

扩展性强是其另一大亮点。代理类可以灵活地添加各种功能,无论是日志记录、权限校验,还是性能监控,都能在不触及目标对象核心代码的前提下轻松实现。就像为一座房子添砖加瓦,无需推倒重建,只需在外部搭建附属设施,便能让房屋功能更加完备。

再者,代理模式为访问控制提供了便利。在涉及敏感信息或关键操作的场景下,代理对象充当守门人,严格把控访问权限,确保只有合法的请求才能触及目标对象,为系统安全保驾护航。

最后,从功能复用角度看,代理类一旦构建完成,可在多个相似场景下重复使用,避免了重复开发,提高了代码的复用率,符合软件开发中高效、精简的原则,让开发人员能将更多精力投入到核心业务逻辑的雕琢上。

(二)缺点

然而,代理模式也并非完美无瑕,存在一些需要谨慎对待的缺点。

一方面,它不可避免地增加了类的数量。尤其是静态代理,每一个目标对象都可能催生一个对应的代理类,使得项目结构愈发复杂,代码管理成本直线上升,宛如一个原本简洁的花园里,因过多的装饰小品而略显杂乱,增加了开发者梳理代码逻辑的难度。

另一方面,性能问题在某些情况下不容忽视。动态代理在运行时通过反射等机制实现功能增强,而反射操作相较于直接的方法调用,往往消耗更多的时间与资源,犹如在高速公路上设置了一道收费站,车辆通行速度必然会有所减缓。在对性能要求苛刻、方法调用频繁的场景下,这一劣势可能会被放大,影响系统的整体效率。

此外,代理模式的学习曲线相对陡峭,对于初学者而言,理解代理、目标、客户端之间的复杂交互以及动态代理背后的原理(如 JDK 动态代理的反射机制、CGLIB 动态代理的字节码生成技术)并非易事,需要投入一定的时间与精力去钻研,才能娴熟运用,避免陷入代码调试的泥沼。

七、总结

至此,我们已全方位领略了 Java 代理模式的风采。从静态代理到动态代理,JDK 动态代理凭借接口与反射机制,在运行时灵活增强功能;CGLIB 动态代理则以继承为基,突破接口限制,为无接口类注入活力。

在应用场景中,它大显身手:权限控制时,化身安全卫士,严守数据访问关卡;远程调用里,作为通信桥梁,让本地与远程无缝对接;缓存场景下,变身效率神器,减少重复操作;日志记录中,成为忠实史官,记录系统运行轨迹。

当然,代理模式并非完美。它虽降低耦合、便于扩展、助力访问管控与功能复用,却也存在类数增多、性能损耗、学习成本较高等问题。但只要我们权衡利弊,在合适处精准运用,便能化缺点为亮点。

Java 代理模式是编程路上的得力伙伴,望大家多实践、多探索,让代码架构更优、功能更强,开启高效编程新篇章。