从青铜到王者:Java设计模式——代理模式

从青铜到王者:Java代理模式全解析

一、什么是代理模式

1.1 代理模式的定义

代理模式,从名字上看就像是找了一个 "代理人" 来帮你做事。在 Java 开发中,代理模式是一种结构型设计模式,它为其他对象提供一种代理以控制对这个对象的访问。也就是说,当你想要访问某个对象(我们称之为目标对象)时,不直接去访问它,而是通过一个代理对象来访问 。代理对象就像是目标对象的 "代言人",所有对目标对象的访问请求都先经过代理对象,代理对象可以在这个过程中做一些额外的操作,比如权限检查、日志记录等,然后再将请求转发给目标对象,或者直接根据情况处理请求而不转发 。

用代码来简单表示,假设有一个目标对象Target,它实现了某个接口Subject

typescript 复制代码
// 抽象主题接口
interface Subject {
    void request();
}

// 真实主题类,实现了Subject接口
class Target implements Subject {
    @Override
    public void request() {
        System.out.println("目标对象执行请求");
    }
}

然后有一个代理对象Proxy,同样实现了Subject接口,并且持有目标对象的引用:

typescript 复制代码
// 代理类,实现了Subject接口
class Proxy implements Subject {
    private Target target;

    public Proxy(Target target) {
        this.target = target;
    }

    @Override
    public void request() {
        // 可以在调用目标对象方法之前做一些操作,比如权限检查
        System.out.println("代理对象进行权限检查");
        // 调用目标对象的方法
        target.request();
        // 可以在调用目标对象方法之后做一些操作,比如日志记录
        System.out.println("代理对象记录日志");
    }
}

在客户端使用时,通过代理对象来访问目标对象的方法:

typescript 复制代码
public class Client {
    public static void main(String[] args) {
        Target target = new Target();
        Proxy proxy = new Proxy(target);
        proxy.request();
    }
}

这样,当调用proxy.request()时,实际上是代理对象先进行了权限检查,然后调用目标对象的request方法,最后又记录了日志 。这就是代理模式的基本工作方式,通过代理对象来控制对目标对象的访问,并且可以在访问前后添加额外的功能 。

1.2 生活中的代理模式

为了更好地理解代理模式,我们来看看生活中的一些例子 。

房产中介:当你想买房子的时候,你可以自己去寻找房源,和房东沟通、谈判、办理各种手续 。但是这样做往往很麻烦,需要花费大量的时间和精力 。所以很多人会选择找房产中介 。房产中介就是一个代理,他掌握了大量的房源信息,了解市场行情 。你只需要告诉中介你的需求,比如预算、地段、房型等,中介就会帮你筛选合适的房源,安排看房,并且在你和房东之间进行沟通和协调,帮助你完成购房交易 。在这里,房产中介就是你(购房者)和房东(房源提供者)之间的代理,你通过中介来间接访问房源,中介在这个过程中提供了信息筛选、沟通协调等额外的服务 。

明星经纪人:明星通常都有自己的经纪人 。明星专注于自己的演艺事业,比如拍戏、唱歌、参加活动等 。而经纪人则负责处理明星的各种事务,比如接广告代言、安排演出活动、与媒体沟通等 。当有品牌商想要找明星代言时,不会直接联系明星,而是先联系经纪人 。经纪人会根据品牌的知名度、形象、代言费用等因素进行评估,决定是否接受代言 。如果接受,再和品牌商进行谈判,确定代言的细节 。在这个过程中,经纪人就是明星的代理,控制着对明星的访问,并且为明星提供了业务筛选、商务谈判等服务 。

代购:现在很多人喜欢购买国外的商品,但是由于各种原因,比如没有时间出国、不了解国外的购物渠道等,无法直接购买 。这时候就可以找代购 。代购就是代理你去国外购买商品,他们了解国外的市场和购物流程,能够帮你找到你需要的商品,并且帮你办理运输、清关等手续,把商品送到你手中 。代购在这个过程中就是你和国外商家之间的代理,让你能够间接购买到国外的商品 。

通过这些生活中的例子,我们可以看到代理模式的核心思想就是通过一个代理来控制对某个对象的访问,并且代理可以提供一些额外的服务,让访问更加方便、高效、安全 。这种思想在软件开发中同样非常有用,它可以帮助我们更好地管理代码,提高代码的可维护性和可扩展性 。

1.3 代理模式的作用

代理模式在 Java 开发中有着非常重要的作用,主要体现在以下几个方面 :

控制访问:通过代理对象,我们可以对目标对象的访问进行控制 。比如在前面的例子中,代理对象可以进行权限检查,只有具有相应权限的用户才能访问目标对象的方法 。这在企业级开发中非常常见,比如对于一些敏感的业务操作,需要进行用户身份验证和权限控制,以确保系统的安全性 。

typescript 复制代码
class Proxy implements Subject {
    private Target target;
    private User currentUser;

    public Proxy(Target target, User currentUser) {
        this.target = target;
        this.currentUser = currentUser;
    }

    @Override
    public void request() {
        // 进行权限检查
        if (currentUser.hasPermission("access_target_method")) {
            target.request();
        } else {
            System.out.println("用户没有权限访问");
        }
    }
}

增强功能:代理对象可以在调用目标对象的方法前后添加额外的功能,比如日志记录、性能监控、事务处理等 。这些功能可以帮助我们更好地了解系统的运行情况,提高系统的性能和可靠性 。

typescript 复制代码
class Proxy implements Subject {
    private Target target;

    public Proxy(Target target) {
        this.target = target;
    }

    @Override
    public void request() {
        long startTime = System.currentTimeMillis();
        System.out.println("开始记录日志");
        target.request();
        System.out.println("结束记录日志");
        long endTime = System.currentTimeMillis();
        System.out.println("方法执行时间:" + (endTime - startTime) + "ms");
    }
}

解耦:代理模式可以将客户端和目标对象解耦,客户端只需要和代理对象交互,而不需要了解目标对象的具体实现细节 。这样可以提高代码的可维护性和可扩展性,当目标对象的实现发生变化时,只需要修改代理对象的代码,而不需要修改客户端的代码 。

延迟加载:对于一些创建开销较大的对象,我们可以使用代理模式来实现延迟加载 。代理对象在一开始并不会创建目标对象,而是在真正需要使用目标对象时才创建它 。这样可以提高系统的性能,减少资源的浪费 。

typescript 复制代码
class Proxy implements Subject {
    private Target target;

    @Override
    public void request() {
        if (target == null) {
            target = new Target();
        }
        target.request();
    }
}

代理模式通过控制访问、增强功能、解耦和延迟加载等作用,使得我们的代码更加灵活、可维护和高效,是 Java 开发中非常重要的一种设计模式 。

二、静态代理

2.1 静态代理的实现步骤

静态代理是代理模式的一种实现方式,它在编译期就已经确定了代理类的代码。下面我们来详细看看静态代理的实现步骤:

  1. 定义接口 :首先,我们需要定义一个接口,这个接口定义了目标对象和代理对象共同的行为。目标对象和代理对象都必须实现这个接口,这样才能保证它们在行为上的一致性 。比如,我们定义一个发送短信的接口SmsService
typescript 复制代码
public interface SmsService {
    String send(String message);
}

这个接口只有一个send方法,用于发送短信,参数message是要发送的短信内容,返回值是发送结果 。

  1. 实现类 :接下来,我们要创建一个实现上述接口的目标类,这个类是实际执行核心业务逻辑的类 。例如,创建一个SmsServiceImpl类来实现SmsService接口:
typescript 复制代码
public class SmsServiceImpl implements SmsService {
    @Override
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

SmsServiceImpl类中,send方法实现了发送短信的具体逻辑,它会打印出要发送的短信内容,并返回该内容,表示短信发送成功 。

  1. 代理类 :然后,我们创建一个代理类,这个代理类也实现了与目标类相同的接口 。在代理类中,我们会持有一个目标对象的引用,并在代理类的方法中调用目标对象的相应方法 。同时,我们可以在调用目标对象方法的前后添加一些额外的逻辑,比如日志记录、权限检查等 。下面是一个SmsProxy代理类的示例:
typescript 复制代码
public class SmsProxy implements SmsService {
    private final SmsService smsService;

    public SmsProxy(SmsService smsService) {
        this.smsService = smsService;
    }

    @Override
    public String send(String message) {
        // 调用方法之前,添加自己的操作,比如日志记录
        System.out.println("before method send()");
        // 调用目标对象的方法
        String result = smsService.send(message);
        // 调用方法之后,添加自己的操作,比如日志记录
        System.out.println("after method send()");
        return result;
    }
}

SmsProxy类中,通过构造函数传入目标对象smsService 。在send方法中,先打印出 "before method send ()",表示在调用目标对象的send方法之前执行的操作;然后调用目标对象的send方法发送短信,并将返回结果存储在result变量中;最后打印出 "after method send ()",表示在调用目标对象的send方法之后执行的操作 。最后返回短信发送结果 。

通过以上三个步骤,我们就完成了静态代理的实现 。在客户端代码中,我们可以使用代理对象来调用方法,从而实现对目标对象的访问控制和功能增强 。

2.2 代码示例

为了更直观地理解静态代理的工作原理,我们来看一个完整的代码示例 。假设我们正在开发一个短信通知系统,需要在发送短信前后记录日志 。我们可以使用静态代理来实现这个功能 。

首先,定义SmsService接口,它包含一个send方法用于发送短信:

typescript 复制代码
public interface SmsService {
    String send(String message);
}

然后,实现SmsServiceImpl类,它是真正发送短信的类,实现了SmsService接口:

typescript 复制代码
public class SmsServiceImpl implements SmsService {
    @Override
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

接着,创建SmsProxy代理类,它也实现了SmsService接口,并且持有SmsServiceImpl的实例,在send方法中添加了日志记录的功能:

typescript 复制代码
public class SmsProxy implements SmsService {
    private final SmsService smsService;

    public SmsProxy(SmsService smsService) {
        this.smsService = smsService;
    }

    @Override
    public String send(String message) {
        System.out.println("before method send()");
        String result = smsService.send(message);
        System.out.println("after method send()");
        return result;
    }
}

最后,在客户端代码中使用代理类:

typescript 复制代码
public class Client {
    public static void main(String[] args) {
        // 创建目标对象
        SmsService smsService = new SmsServiceImpl();
        // 创建代理对象,并将目标对象传入
        SmsService proxy = new SmsProxy(smsService);
        // 通过代理对象调用方法
        proxy.send("Hello, this is a test message.");
    }
}

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

typescript 复制代码
before method send()
send message:Hello, this is a test message.
after method send()

从输出结果可以看出,当通过代理对象调用send方法时,代理对象先执行了自己添加的日志记录逻辑(打印 "before method send ()"),然后调用目标对象的send方法发送短信,最后又执行了自己添加的日志记录逻辑(打印 "after method send ()") 。这就是静态代理的工作过程,通过代理对象,我们可以在不修改目标对象代码的情况下,对目标对象的方法进行增强 。

2.3 静态代理的优缺点

静态代理作为代理模式的一种实现方式,有其自身的优点和缺点 。了解这些优缺点,有助于我们在实际开发中根据具体需求选择合适的技术方案 。

优点

  • 实现简单:静态代理的实现过程相对直观和简单 。只需要按照定义接口、实现目标类和代理类的步骤,编写相应的代码即可 。不需要复杂的反射机制或动态生成类的技术,对于初学者来说容易理解和掌握 。就像我们前面的短信发送示例,通过几个简单的类和方法,就实现了对短信发送功能的代理和增强 。

  • 性能较高:由于静态代理类在编译时就已经确定,其字节码文件在运行前就已经存在 。在运行时,不需要像动态代理那样动态生成代理类,减少了类加载和字节码生成的开销 。因此,静态代理在性能方面表现较好,适用于对性能要求较高且代理逻辑相对固定的场景 。

  • 易于调试:因为静态代理的代码是固定编写的,在调试过程中,我们可以直接查看和跟踪代理类和目标类的代码执行路径 。相比于动态代理,调试过程更加直接和方便,能够快速定位和解决问题 。

缺点

  • 代码冗余:如果有多个目标类,并且每个目标类都需要实现相同的代理逻辑,那么就需要为每个目标类创建一个对应的代理类 。这会导致大量重复的代理代码,增加了代码的维护成本 。例如,除了短信发送功能,还有邮件发送、推送通知等功能都需要类似的日志记录代理逻辑,就需要分别为每个功能创建代理类,代码会变得冗长和繁琐 。

  • 不易维护:当接口发生变化时,比如增加或删除方法,不仅目标类需要修改,所有的代理类也都需要相应地修改 。这违反了软件开发中的开闭原则(对扩展开放,对修改关闭),使得代码的维护变得困难 。如果一个项目中有很多代理类,一旦接口变动,逐一修改代理类的工作量将非常大 。

  • 灵活性差:静态代理在编译时就确定了代理类和目标类之间的关系,缺乏动态性 。在运行时,无法根据不同的条件或需求动态地切换代理对象或代理逻辑 。这在一些需要灵活处理代理关系的场景中,显得力不从心 。比如,根据用户的权限不同,需要动态地选择不同的代理逻辑,静态代理就很难实现 。

静态代理虽然实现简单、性能较高,但由于存在代码冗余、不易维护和灵活性差等缺点,在实际应用中,通常适用于接口和实现类较少、代码变动不频繁且对性能要求较高的场景 。对于更复杂和灵活的需求,动态代理可能是更好的选择 。

三、动态代理

在 Java 开发中,动态代理是一种非常强大的技术,它允许我们在运行时动态地创建代理对象,而不需要像静态代理那样在编译期就确定代理类的代码 。动态代理为我们提供了更高的灵活性和扩展性,使得我们可以在不修改目标对象代码的前提下,对其行为进行增强和控制 。接下来,我们将深入探讨动态代理的原理、JDK 动态代理和 CGLIB 动态代理的实现及它们之间的对比 。

3.1 动态代理的原理

动态代理的核心原理基于 Java 的反射机制 。在 Java 中,反射允许我们在运行时获取类的信息,包括类的方法、属性等,并且可以动态地调用对象的方法 。动态代理正是利用了反射机制,在运行时动态生成代理类的字节码,并将其加载到 JVM 中,从而创建出代理对象 。

动态代理主要涉及两个关键的类和接口:InvocationHandlerProxy

  • InvocationHandler :这是一个接口,它定义了一个invoke方法 。当我们通过代理对象调用方法时,实际上会调用InvocationHandlerinvoke方法 。invoke方法接收三个参数:代理对象本身(proxy)、被调用的方法(method)以及方法的参数(args) 。在invoke方法中,我们可以实现自己的逻辑,比如在调用目标方法之前进行权限检查,调用之后进行日志记录等 。然后通过反射调用目标对象的方法,并返回结果 。
typescript 复制代码
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public 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("进行权限检查");
        // 通过反射调用目标对象的方法
        Object result = method.invoke(target, args);
        // 调用方法之后的操作,比如日志记录
        System.out.println("记录日志");
        return result;
    }
}
  • Proxy :这是一个类,它提供了静态方法newProxyInstance用于创建动态代理对象 。newProxyInstance方法接收三个参数:类加载器(ClassLoader),用于加载代理类的字节码;接口数组(Class<?>[] interfaces),代理类需要实现的接口,通过这些接口,代理对象可以调用目标对象的方法;InvocationHandler对象,用于处理代理对象的方法调用 。
typescript 复制代码
import java.lang.reflect.Proxy;

public class ProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new MyInvocationHandler(target));
    }
}

当我们调用Proxy.newProxyInstance方法时,JVM 会动态生成一个代理类,这个代理类实现了我们指定的接口,并且在代理类的方法中,会将方法调用转发给InvocationHandlerinvoke方法 。这样,我们就可以在invoke方法中对目标对象的方法进行增强和控制 。

3.2 JDK 动态代理

JDK 动态代理是 Java 原生提供的动态代理实现方式,它基于接口实现代理功能 。下面我们来详细了解 JDK 动态代理的实现步骤、代码示例以及其特点 。

3.2.1 JDK 动态代理的实现步骤
  1. 定义接口 :首先,我们需要定义一个接口,这个接口定义了目标对象和代理对象共同的行为 。目标对象和代理对象都必须实现这个接口,这样才能保证它们在行为上的一致性 。例如,我们定义一个发送短信的接口SmsService
typescript 复制代码
public interface SmsService {
    String send(String message);
}
  1. 实现类 :创建一个实现上述接口的目标类,这个类是实际执行核心业务逻辑的类 。比如,创建一个SmsServiceImpl类来实现SmsService接口:
typescript 复制代码
public class SmsServiceImpl implements SmsService {
    @Override
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}
  1. 自定义 InvocationHandler :实现InvocationHandler接口,并重写invoke方法 。在invoke方法中,我们可以在调用目标方法之前和之后添加自己的逻辑 。例如:
typescript 复制代码
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class SmsInvocationHandler implements InvocationHandler {
    private Object target;

    public SmsInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 调用方法之前的操作,比如日志记录
        System.out.println("before method " + method.getName());
        // 通过反射调用目标对象的方法
        Object result = method.invoke(target, args);
        // 调用方法之后的操作,比如日志记录
        System.out.println("after method " + method.getName());
        return result;
    }
}
  1. 创建代理对象 :使用Proxy类的newProxyInstance方法创建代理对象 。传入目标对象的类加载器、目标对象实现的接口数组以及自定义的InvocationHandler对象 。例如:
typescript 复制代码
import java.lang.reflect.Proxy;

public class ProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new SmsInvocationHandler(target));
    }
}
  1. 使用代理对象 :在客户端代码中,通过代理对象调用方法,实际执行的是InvocationHandlerinvoke方法的逻辑 。例如:
typescript 复制代码
public class Client {
    public static void main(String[] args) {
        SmsService smsService = new SmsServiceImpl();
        SmsService proxy = (SmsService) ProxyFactory.getProxy(smsService);
        proxy.send("Hello, dynamic proxy!");
    }
}
3.2.2 代码示例

为了更直观地展示 JDK 动态代理的工作原理,我们将上述步骤整合为一个完整的代码示例 。

首先,定义SmsService接口:

typescript 复制代码
public interface SmsService {
    String send(String message);
}

然后,实现SmsServiceImpl类:

typescript 复制代码
public class SmsServiceImpl implements SmsService {
    @Override
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

接着,实现SmsInvocationHandler类:

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

public class SmsInvocationHandler implements InvocationHandler {
    private Object target;

    public SmsInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before method " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("after method " + method.getName());
        return result;
    }
}

再创建ProxyFactory类:

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

public class ProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new SmsInvocationHandler(target));
    }
}

最后,在Client类中使用代理对象:

typescript 复制代码
public class Client {
    public static void main(String[] args) {
        SmsService smsService = new SmsServiceImpl();
        SmsService proxy = (SmsService) ProxyFactory.getProxy(smsService);
        proxy.send("Hello, dynamic proxy!");
    }
}

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

typescript 复制代码
before method send
send message:Hello, dynamic proxy!
after method send

从输出结果可以看出,当通过代理对象调用send方法时,先执行了SmsInvocationHandlerinvoke方法的前置逻辑(打印 "before method send"),然后调用了目标对象SmsServiceImplsend方法,最后执行了invoke方法的后置逻辑(打印 "after method send") 。这就是 JDK 动态代理的工作过程,通过代理对象,我们可以在不修改目标对象代码的情况下,对目标对象的方法进行增强 。

3.2.3 JDK 动态代理的特点
  • 基于接口:JDK 动态代理只能为实现了接口的类创建代理对象 。这是因为 JDK 动态代理生成的代理类是实现了目标接口的,通过接口来调用目标对象的方法 。如果目标类没有实现接口,就无法使用 JDK 动态代理 。

  • 运行时生成代理类:JDK 动态代理的代理类是在运行时通过反射机制动态生成的,而不是在编译期就确定好的 。这使得我们可以根据不同的需求,在运行时灵活地创建代理对象,并且可以在代理对象中添加不同的逻辑 。

  • 性能开销:由于 JDK 动态代理使用了反射机制,在运行时动态生成代理类和调用方法,相比于静态代理,会有一定的性能开销 。但是在大多数情况下,这种性能开销是可以接受的,特别是在对灵活性和扩展性要求较高的场景中 。

  • 代码简洁 :JDK 动态代理的实现相对简洁,只需要实现InvocationHandler接口和使用Proxy类的newProxyInstance方法,就可以创建代理对象并对目标对象的方法进行增强 。相比于静态代理,减少了大量重复的代理类代码 。

3.3 CGLIB 动态代理

CGLIB(Code Generation Library)是一个强大的高性能的代码生成库,它可以在运行时扩展 Java 类与实现 Java 接口 。CGLIB 动态代理是基于字节码增强技术实现的,它通过继承目标类来创建代理类,而不需要目标类实现接口 。下面我们来详细了解 CGLIB 动态代理的原理、代码示例以及其特点 。

3.3.1 CGLIB 动态代理的原理

CGLIB 动态代理的原理是通过继承目标类,生成一个子类作为代理类 。在代理类中,重写目标类的方法,在方法调用前后插入自定义的逻辑 。CGLIB 使用 ASM(一个 Java 字节码操纵框架)来动态生成字节码,实现对目标类的增强 。

CGLIB 动态代理主要涉及两个关键的类和接口:MethodInterceptorEnhancer

  • MethodInterceptor :这是一个接口,它定义了一个intercept方法 。当我们通过代理对象调用方法时,实际上会调用MethodInterceptorintercept方法 。intercept方法接收四个参数:被代理的对象(obj)、被拦截的方法(method)、方法的参数(args)以及用于调用原始方法的MethodProxy对象 。在intercept方法中,我们可以实现自己的逻辑,比如在调用目标方法之前进行权限检查,调用之后进行日志记录等 。然后通过MethodProxy对象调用目标对象的原始方法,并返回结果 。
typescript 复制代码
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 调用方法之前的操作,比如权限检查
        System.out.println("进行权限检查");
        // 通过MethodProxy调用目标对象的原始方法
        Object result = methodProxy.invokeSuper(obj, args);
        // 调用方法之后的操作,比如日志记录
        System.out.println("记录日志");
        return result;
    }
}
  • Enhancer :这是 CGLIB 提供的一个类,用于生成代理对象 。通过Enhancer类,我们可以设置代理类的父类(即目标类)、回调函数(即实现了MethodInterceptor接口的对象)等 。然后调用create方法,就可以生成代理对象 。
typescript 复制代码
import net.sf.cglib.proxy.Enhancer;

public class ProxyFactory {
    public static Object getProxy(Class<?> clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(clazz.getClassLoader());
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(new MyMethodInterceptor());
        return enhancer.create();
    }
}

当我们调用代理对象的方法时,代理类会拦截方法调用,然后调用MethodInterceptorintercept方法,在intercept方法中执行我们自定义的逻辑,并通过MethodProxy调用目标对象的原始方法 。这样,我们就实现了对目标对象方法的增强和控制 。

3.3.2 代码示例

为了更直观地展示 CGLIB 动态代理的工作原理,我们来看一个完整的代码示例 。假设我们有一个发送短信的类AliSmsService,它没有实现任何接口,我们使用 CGLIB 动态代理来对其发送短信的方法进行增强 。

首先,引入 CGLIB 的依赖 。如果使用 Maven,可以在pom.xml文件中添加以下依赖:

typescript 复制代码
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

然后,创建AliSmsService类:

typescript 复制代码
public class AliSmsService {
    public void send(String message) {
        System.out.println("send message:" + message);
    }
}

接着,实现MyMethodInterceptor类:

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

import java.lang.reflect.Method;

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("before method " + method.getName());
        Object result = methodProxy.invokeSuper(obj, args);
        System.out.println("after method " + method.getName());
        return result;
    }
}

再创建ProxyFactory类:

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

public class ProxyFactory {
    public static Object getProxy(Class<?> clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(clazz.getClassLoader());
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(new MyMethodInterceptor());
        return enhancer.create();
    }
}

最后,在Client类中使用代理对象:

typescript 复制代码
public class Client {
    public static void main(String[] args) {
        AliSmsService aliSmsService = (AliSmsService) ProxyFactory.getProxy(AliSmsService.class);
        aliSmsService.send("Hello, CGLIB proxy!");
    }
}

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

typescript 复制代码
before method send
send message:Hello, CGLIB proxy!
after method send

从输出结果可以看出,当通过代理对象调用send方法时,先执行了MyMethodInterceptorintercept方法的前置逻辑(打印 "before method send"),然后调用了目标对象AliSmsServicesend方法,最后执行了intercept方法的后置逻辑(打印 "after method send") 。这就是 CGLIB 动态代理的工作过程,通过代理对象,我们可以对没有实现接口的类进行方法增强 。

3.3.3 CGLIB 动态代理的特点
  • 无需接口:CGLIB 动态代理不需要目标类实现接口,它通过继承目标类来创建代理类 。这使得 CGLIB 动态代理可以应用于更多的场景,特别是当目标类无法实现接口时,CGLIB 动态代理就成为了一种很好的选择 。

  • 可代理普通类:CGLIB 动态代理可以代理普通的 Java 类,而不仅仅是实现了接口的类 。这为我们在开发中提供了更大的灵活性,能够对各种类型的类进行功能增强 。

  • 性能较高:CGLIB 动态代理使用字节码增强技术,直接在字节码层面进行操作,相比于 JDK 动态代理的反射机制,性能更高 。特别是在对性能要求较高的场景中,CGLIB 动态代理具有一定的优势 。

  • 代理类继承目标类 :由于 CGLIB 动态代理是通过继承目标类来创建代理类,所以代理类和目标类之间存在继承关系 。这也意味着代理类无法代理目标类中被声明为final的方法,因为final方法不能被重写 。

3.4 JDK 动态代理与 CGLIB 动态代理的对比

JDK 动态代理和 CGLIB 动态代理都是 Java 中常用的动态代理实现方式,它们各有特点,适用于不同的场景 。下面我们从多个方面对它们进行对比 。

对比项 JDK 动态代理 CGLIB 动态代理
代理方式 基于接口,代理类实现目标接口 基于继承,代理类继承目标类
目标类要求 目标类必须实现接口 目标类

四、代理模式的应用场景

4.1 远程代理

远程代理是代理模式的一种应用场景,它主要用于处理远程对象的访问 。在分布式系统中,当客户端需要访问位于不同地址空间(比如不同的服务器、不同的进程或者不同的网络)的对象时,直接访问会面临网络通信、序列化与反序列化等复杂问题 。这时就可以使用远程代理,它在客户端本地代表远程对象,客户端通过调用本地代理对象的方法,代理对象负责与远程对象进行通信,将方法调用和参数传递给远程对象,并接收远程对象返回的结果,再将结果返回给客户端 。这样,客户端就可以像调用本地对象一样调用远程对象的方法,而无需关心网络通信的细节 。

以 RPC(Remote Procedure Call,远程过程调用)框架为例,在一个微服务架构中,有多个服务模块,每个服务模块可能部署在不同的服务器上 。比如,一个电商系统中,订单服务和商品服务是两个独立的微服务 。当订单服务需要调用商品服务获取商品信息时,就可以使用远程代理 。订单服务中的代码通过调用本地的商品服务代理对象的方法,代理对象将请求进行序列化,通过网络发送到商品服务所在的服务器 。商品服务接收到请求后,进行反序列化,执行相应的方法,将结果序列化后通过网络返回给订单服务的代理对象 。代理对象再将结果反序列化,返回给订单服务的调用代码 。在这个过程中,远程代理隐藏了网络通信的复杂性,使得订单服务可以像调用本地方法一样调用商品服务的方法 。

4.2 虚拟代理

虚拟代理用于延迟创建开销较大的对象 。当一个对象的创建成本很高,比如需要消耗大量的内存、需要进行复杂的初始化操作或者需要从远程资源加载数据时,如果在程序启动或者对象被创建时就立即创建该对象,可能会导致系统性能下降或者启动时间过长 。虚拟代理模式可以解决这个问题,它在对象真正被使用时才创建目标对象,在对象未被使用时,使用代理对象来代表目标对象 。代理对象可以在创建目标对象之前提供一些简单的功能,比如返回一个占位符或者默认值,给用户一个响应 。

以加载大图片为例,在一个图像查看器应用中,如果直接加载一个大尺寸的高清图片,可能需要消耗大量的内存和时间,导致应用启动缓慢或者界面卡顿 。这时可以使用虚拟代理,在界面加载时,先创建一个虚拟代理对象来代表图片 。虚拟代理对象可以返回一个小尺寸的缩略图或者一个占位符,快速展示在界面上,给用户一个图片存在的提示 。当用户真正点击查看图片或者图片进入可视区域时,虚拟代理对象再去加载真正的大图片,并替换掉之前展示的缩略图或占位符 。这样,既提高了应用的响应速度,又减少了不必要的资源消耗 。

4.3 安全代理

安全代理主要用于控制对对象的访问权限 。在一些系统中,某些对象或者对象的某些方法可能具有敏感性,需要限制只有特定的用户或者具有特定权限的用户才能访问 。安全代理在客户端访问目标对象之前,会对客户端的身份和权限进行检查 。如果客户端具有相应的权限,代理对象将请求转发给目标对象;如果客户端没有权限,代理对象可以拒绝请求,并返回相应的错误信息 。

以权限控制为例,在一个企业级应用中,有一个用户管理模块,其中包含了修改用户密码、删除用户等敏感操作 。只有系统管理员或者具有特定角色的用户才能执行这些操作 。可以使用安全代理来保护这些敏感操作 。当普通用户调用修改用户密码的方法时,安全代理对象会检查用户的身份和权限,发现普通用户没有权限执行该操作,就会返回 "权限不足" 的错误提示 。只有当系统管理员调用该方法时,安全代理对象验证其权限通过后,才会将请求转发给真正的用户管理对象执行修改密码的操作 。通过安全代理,有效地保证了系统的安全性,防止未经授权的访问 。

4.4 智能指引

智能指引也称为智能代理,它可以在访问目标对象时,进行一些额外的操作,比如记录方法调用次数、记录方法调用的参数和返回值、进行性能监控等 。智能指引代理对象在客户端调用目标对象的方法时,会拦截方法调用,执行自己的逻辑,然后再将调用转发给目标对象 。

以记录方法调用次数为例,在一个统计系统中,需要统计某个方法被调用的次数 。可以使用智能指引代理,当客户端调用目标对象的方法时,智能指引代理对象会记录下方法的调用次数,然后再将调用转发给目标对象 。通过这种方式,可以方便地获取方法的调用频率,用于系统性能分析、资源分配等 。例如,在一个电商系统中,统计商品查询方法的调用次数,可以帮助我们了解用户对商品信息的关注度,从而优化商品展示策略或者调整服务器资源分配 。

4.5 延迟加载

延迟加载是代理模式的一个重要应用场景,它与虚拟代理有一定的相似性,但又不完全相同 。延迟加载主要关注的是在对象被访问时才加载其真实数据,而虚拟代理更侧重于用一个轻量级的代理对象来代表真实对象 。在延迟加载中,代理对象在一开始并不包含真实对象的数据,而是在第一次访问对象的某个属性或者方法时,才去加载真实数据 。

以 Hibernate 延迟加载为例,在一个数据库应用中,使用 Hibernate 框架进行对象关系映射 。假设我们有一个User对象,它关联了一个Order对象集合,表示用户的订单 。如果使用传统的加载方式,当加载User对象时,会同时加载其关联的所有Order对象,这可能会导致大量的数据加载,尤其是当Order对象数量很多时,会严重影响系统性能 。而使用 Hibernate 的延迟加载功能,在加载User对象时,其关联的Order对象集合并不会立即加载,而是返回一个代理对象 。当程序真正访问User对象的Order集合属性时,代理对象才会去数据库中查询并加载对应的Order对象数据 。这样,有效地减少了不必要的数据加载,提高了系统的性能和响应速度 。

五、代理模式在框架中的应用

代理模式在 Java 开发的众多框架中有着广泛的应用,它为框架提供了强大的功能和灵活性 。下面我们将深入探讨代理模式在 Spring AOP 和 MyBatis 这两个常用框架中的具体应用 。

5.1 Spring AOP 中的代理模式

Spring AOP(Aspect-Oriented Programming)是 Spring 框架的一个重要模块,它允许我们将横切关注点(如日志记录、事务管理、权限控制等)与业务逻辑分离,通过代理模式实现对业务方法的增强 。在 Spring AOP 中,代理模式是其实现切面编程的核心机制 。

Spring AOP 主要使用两种代理技术:JDK 动态代理和 CGLIB 动态代理 。当目标对象实现了接口时,Spring AOP 默认使用 JDK 动态代理;当目标对象没有实现接口时,Spring AOP 会使用 CGLIB 动态代理 。不过,我们也可以通过配置强制 Spring AOP 使用 CGLIB 动态代理 。

以日志记录为例,假设我们有一个业务服务类UserService,其中包含一个register方法用于用户注册 。现在我们希望在register方法执行前后记录日志,以跟踪方法的调用情况 。我们可以使用 Spring AOP 和代理模式来实现这个功能 。

首先,定义UserService接口和实现类:

typescript 复制代码
public interface UserService {
    void register(String username, String password);
}

public class UserServiceImpl implements UserService {
    @Override
    public void register(String username, String password) {
        System.out.println("用户 " + username + " 注册成功");
    }
}

然后,定义一个切面类LoggingAspect,用于记录日志 :

typescript 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    @Around("execution(* com.example.service.UserService.register(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("开始记录日志:进入 register 方法");
        try {
            // 调用目标方法
            return joinPoint.proceed();
        } finally {
            System.out.println("结束记录日志:离开 register 方法");
        }
    }
}

在上述代码中,@Aspect注解表示这是一个切面类,@Around注解定义了一个环绕通知,它会在目标方法执行前后执行 。execution(* com.example.service.UserService.register(..))是切点表达式,用于指定要增强的方法 。

最后,在 Spring 配置文件中启用 AspectJ 自动代理:

typescript 复制代码
<configuration>
    <component-scan base-package="com.example"/>
    <aop:aspectj-autoproxy/>
</configuration>

或者使用 Java 配置类:

typescript 复制代码
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
public class AppConfig {
}

当我们在客户端代码中调用UserServiceregister方法时,实际上调用的是代理对象的方法 。代理对象会拦截方法调用,并根据切面类的定义,在目标方法执行前后记录日志 。这样,我们就通过代理模式和 Spring AOP 实现了对业务方法的功能增强,而且没有修改UserService的原有代码,符合开闭原则 。

5.2 MyBatis 中的代理模式

MyBatis 是一个优秀的持久层框架,它简化了数据库操作,提高了开发效率 。在 MyBatis 中,代理模式主要应用于 Mapper 接口的实现 。开发者只需要定义 Mapper 接口,而无需编写接口的实现类,MyBatis 会在运行时为这些接口创建动态代理对象,通过代理对象来执行 SQL 语句 。

MyBatis 使用 JDK 动态代理为 Mapper 接口生成代理对象 。当我们调用 Mapper 接口的方法时,实际上是调用代理对象的方法 。代理对象会拦截方法调用,并根据方法名和参数等信息,查找对应的 SQL 语句,然后执行 SQL 语句并返回结果 。

以用户管理系统为例,假设我们有一个UserMapper接口,用于定义用户相关的数据库操作方法:

typescript 复制代码
import com.example.model.User;

import java.util.List;

public interface UserMapper {
    // 根据ID查询用户
    User getUserById(int id);
    // 查询所有用户
    List<User> getAllUsers();
    // 插入用户
    int insertUser(User user);
}

同时,我们需要编写对应的 Mapper XML 文件,用于配置 SQL 语句 。例如,UserMapper.xml文件内容如下:

typescript 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
    <select id="getUserById" parameterType="int" resultType="com.example.model.User">
        SELECT * FROM users WHERE id = #{id}
    </select>
    <select id="getAllUsers" resultType="com.example.model.User">
        SELECT * FROM users
    </select>
    <insert id="insertUser" parameterType="com.example.model.User">
        INSERT INTO users (username, password, email) VALUES (#{username}, #{password}, #{email})
    </insert>
</mapper>

在 MyBatis 的配置文件mybatis-config.xml中,需要注册 Mapper XML 文件:

typescript 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
                <property name="username" value="root"/>
                <property name="password" value="password"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/example/mapper/UserMapper.xml"/>
    </mappers>
</configuration>

在客户端代码中,我们通过SqlSession获取UserMapper的代理对象,并调用其方法:

typescript 复制代码
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import com.example.mapper.UserMapper;
import com.example.model.User;

import java.io.InputStream;
import java.util.List;

public class MyBatisExample {
    public static void main(String[] args) {
        String resource = "mybatis-config.xml";
        try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            try (SqlSession session = sqlSessionFactory.openSession()) {
                UserMapper userMapper = session.getMapper(UserMapper.class);
                // 根据ID查询用户
                User user = userMapper.getUserById(1);
                System.out.println(user);
                // 查询所有用户
                List<User> users = userMapper.getAllUsers();
                users.forEach(System.out::println);
                // 插入用户
                User newUser = new User("newUser", "password", "newUser@example.com");
                int result = userMapper.insertUser(newUser);
                session.commit();
                System.out.println("插入结果:" + result);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,SqlSession.getMapper(UserMapper.class)方法会返回UserMapper接口的代理对象 。当我们调用代理对象的方法时,MyBatis 会根据方法名在 Mapper XML 文件中查找对应的 SQL 语句,并执行 SQL 语句,将结果返回给客户端 。通过这种方式,MyBatis 利用代理模式简化了数据库操作,使开发者可以更专注于业务逻辑的实现 。

六、总结

代理模式作为一种强大的设计模式,在 Java 开发中有着广泛的应用和重要的地位 。无论是静态代理还是动态代理,都为我们提供了一种有效的方式来控制对对象的访问,并在不修改目标对象代码的前提下,为其添加额外的功能 。

静态代理实现简单,在编译期就确定了代理类,性能较高,适合对性能要求较高且代理逻辑相对固定的场景 。然而,它存在代码冗余、不易维护和灵活性差等缺点 。

动态代理则在运行时动态生成代理类,具有更高的灵活性和扩展性 。JDK 动态代理基于接口实现,代码简洁,但只能为实现了接口的类创建代理对象 。CGLIB 动态代理基于字节码增强技术,无需目标类实现接口,可代理普通类,性能也较高,但代理类继承目标类,无法代理目标类中被声明为final的方法 。

代理模式在远程代理、虚拟代理、安全代理、智能指引、延迟加载等场景中都有着出色的表现 。在 Spring AOP 和 MyBatis 等框架中,代理模式更是发挥了关键作用,为框架提供了强大的功能和灵活性 。

希望通过本文的介绍,你对代理模式有了更深入的理解和掌握 。在实际开发中,根据具体的需求和场景,选择合适的代理模式和实现方式,能够让你的代码更加优雅、高效和可维护 。不断学习和实践代理模式,将有助于你提升自己的编程能力,更好地应对各种复杂的开发任务 。

相关推荐
超级小忍6 分钟前
从零开始:JDK 在 Windows、macOS 和 Linux 上的下载、安装与环境变量配置
java·windows·macos
.鸣14 分钟前
Java学习笔记:IDEA简单使用技巧
java·学习
2501_9167665417 分钟前
【IDEA2017】使用设置+创建项目的不同方式
java·intellij-idea
kyle~18 分钟前
C++---多态(一个接口多种实现)
java·开发语言·c++
funfan051721 分钟前
IDEA基础配置优化指南(中英双版)
java·ide·intellij-idea
罗小爬EX22 分钟前
在IDEA中设置新建Java类时自动添加类注释
java·intellij-idea
vvilkim37 分钟前
深入理解 Spring Boot Starter:简化依赖管理与自动配置的利器
java·前端·spring boot
柯南二号44 分钟前
【Java后端】【可直接落地的 Redis 分布式锁实现】
java·redis·分布式
1点东西1 小时前
新来的同事问我当进程/机器突然停止时,finally 到底会不会执行?
java·后端·程序员
Aspartame~2 小时前
K8s的相关知识总结
java·容器·kubernetes