Java 代理模式

Java 代理模式

Intro

代理模式是一种设计模式,允许一个对象充当另一个对象的替代或中介。代理对象可以控制对目标对象的访问,在不修改原目标对象核心逻辑的情况下,实现额外的功能,如日志记录、权限控制或延迟加载,甚至监控等;

alt text

代理模式的应用场景

  • AOP: 通过代理类来拦截目标对象方法的调用,实现对目标对象方法的增强。
  • 权限校验:通过代理类来控制对某些方法或资源的访问。
  • 日志记录:在调用目标对象的方法时,记录相关日志信息;
  • 性能监控:在代理类中记录方法的执行时间,便于分析性能瓶颈。
  • 懒加载:通过代理对象延迟初始化某些资源,以节省内存或提高性能。
  • 远程代理:为远程调用提供一个本地代理,使得本地方法调用就像调用远程服务一样。

为什么引入代理

通过在代理中实现功能(如日志打印、性能监控),而不是直接修改源代码,能够有效地将功能与业务逻辑解耦

单一职责原则
将日志记录、性能监控等功能放在代理中,目标类专注于业务逻辑。这样,业务逻辑的实现和横切关注点(Cross-Cutting Concerns)之间的依赖关系被削弱,符合单一职责原则。未来如果日志记录或监控的需求变化,需要改变日志记录的格式、级别或存储方式等,那么只对代理类进行修改即可,而无需改动每个目标类的代码
代码复用与一致性:
代码复用:代理类可以被多个目标类共享,避免了在每个目标类中都实现相同的功能(如日志记录),减少冗余代码
确保一致性:在所有目标类中应用相同的日志或监控逻辑;所有日志都遵循相同的格式,更易于后期分析和监控
易于扩展和修改
功能扩展:如果需要添加新功能(如异常处理、输入验证等),可以在代理类中实现,而无需干扰目标类的业务逻辑。例如,可以在日志记录的基础上添加性能监控的功能,只需扩展 invoke 方法
灵活性:可以在运行时动态决定使用哪个代理,例如,可以使用不同的代理实现来添加不同的功能,避免修改目标类,这在团队合作中可以减少冲突。

在 Java 中,代理模式主要分为两种:静态代理动态代理。这两种类型的代理虽然都能够控制对目标对象的访问,但它们的实现方式有所不同。

Static Proxy

假设你有一个接口 BankAccount,它定义了 3 个方法。其中一个方法为withdraw()

假设有一个实现了这个接口的类 BankAccountImpl,它负责具体的取钱操作。现在,你可以创建一个代理类BankAccountProxy,它也实现了该接口,但在调用 BankAccountImplwithdraw方法前后,增加一些额外的操作,比如记录日志或进行权限检查。

Steps

  • 定义接口,BankAccount Interface
  • 定义 class 实现接口,BankAccountImpl implements BankAccount
  • 编写代理类,BankAccountProxy
  • 调用BankAccountProxy的构造器,创建静态代理对象实例
1
2
3
4
5
6
7
package proxydemo;
// 账户服务接口,定义金融操作
public interface BankAccount {
    void viewBalance(); // 查看余额
    void withdraw(); // 取款
    void deposit(); // 存款
}
1
2
3
4
5
6
7
8
9
10
11
12
13
Admin user:
BankAccountProxy initialized with user role: ADMIN
User with role ADMIN is viewing balance.
Displaying account balance.
Withdrawing funds from account.
Depositing funds into account.

Regular user:
BankAccountProxy initialized with user role: USER
User with role USER is viewing balance.
Displaying account balance.
Access denied for user with role USER: Only ADMIN can withdraw funds.
Access denied for user with role USER: Only ADMIN can deposit funds.

优点

  • 可以在不修改BankAccountImpl类的情况下,添加新的功能;代理类与目标类之间的耦合度低。

缺点

  • 如果目标类有很多方法,手动编写代理类会非常繁琐。
  • 代理类的代码量较多,且每次更改都需要手动维护。

Dynamic Proxy

静态代理是在编译期间就需要完成的,即代理类的字节码都已经确定,每个类都要创建一个代理类,导致代码冗余。动态代理不需要手动编写每个代理类,而是在运行时才会生成代理类,并写入 class 文件,相较于静态代理代码冗余更少。

Java 中的动态代理主要有两种方式:

  • JDK 动态代理:基于接口的代理,它只能代理实现了接口的类。
  • CGLIB 动态代理:基于继承的代理,它可以代理没有实现接口的类。

JDK 动态代理

Java JDK 动态代理是基于 java.lang.reflect.ProxyInvocationHandler 实现的。

  • 当调用代理类的方法时,会将其委托给InvocationHandler实例的invoke方法来运行。
  • Proxy 类基于目标类实现的接口和 InvocationHandler 接口,通过反射机制来创建代理类对象。

JDK 动态代理的原理在这里不赘述,感兴趣可参考彻底明白 JDK 动态代理的底层原理

Steps

  • 定义接口,BankAccount Interface
  • 定义 class 实现接口,BankAccountImpl implements BankAccount
  • 创建InvocationHandler接口的实现类
  • 使用java.lang.reflect.ProxynewProxyInstance方法创建动态代理对象的实例
1
2
3
4
5
6
7
package proxydemo;
// 账户服务接口,定义金融操作
public interface BankAccount {
    void deposit(double amount);
    void withdraw(double amount);
    double getBalance();
}
1
2
3
4
5
6
7
8
9
Method deposit is called with arguments: [100.0]
Deposited: 100.0
Method deposit executed in 20 ms
Method withdraw is called with arguments: [50.0]
Withdrew: 50.0
Method withdraw executed in 1 ms
Method getBalance is called with arguments: []
Method getBalance executed in 3 ms
Balance: 50.0

CGLIB 动态代理

CGLIB(Code Generation Library)依赖于 ASM 库,允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。

CGLIB 在 Spring 框架中有所应用
Spring AOP uses either JDK dynamic proxies or CGLIB to create the proxy for a given target object. JDK dynamic proxies are built into the JDK, whereas CGLIB is a common open-source class definition library (repackaged into spring-core).
If the target object to be proxied implements at least one interface, a JDK dynamic proxy is used. All of the interfaces implemented by the target type are proxied. If the target object does not implement any interfaces, a CGLIB proxy is created.

CGLIB 与 JDK 动态代理的区别

  • 代理对象类型:
    • JDK 动态代理:只能代理实现了接口的类。
    • CGLIB 动态代理:可以代理普通的类(没有实现接口的类)。
  • 实现方式:
    • JDK 动态代理:通过反射实现,代理类实现目标对象的接口。
    • CGLIB 动态代理:通过生成目标类的子类来实现。
  • 限制:
    • CGLIB 不能代理 final 类和 final 方法,因为它是通过生成子类来实现的,而 final 方法不能被重写。

Steps

  • 引入 CGLIB 依赖
  • 创建目标类,BankAccount
  • 创建 MethodInterceptor 实现类:处理方法拦截逻辑。
  • 生成代理对象:通过 CGLIB 创建代理对象的实例

InaccessibleObjectException

java.lang.reflect.InaccessibleObjectException-->Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @a67c67e

Your CGLIB proxy usage may face limitations with the JDK 9+ platform module system. As a typical case, you cannot create a CGLIB proxy for a class from the java.lang package when deploying on the module path. Such cases require a JVM bootstrap flag –add-opens=java.base/java.lang=ALL-UNNAMED which is not available for modules.
或者,可以换 jdk8

1
2
3
4
5
6
7
8
9
public class BankAccount {
    public void deposit(double amount) {
        System.out.printf("Depositing: " + amount);
    }

    public void withdraw(double amount) {
        System.out.printf("Withdrawing: " + amount);
    }
}
1
2
3
4
5
6
Method deposit is being called with arguments: [100.0]
Depositing: 100.0
Method deposit execution completed.
Method withdraw is being called with arguments: [50.0]
Withdrawing: 50.0
Method withdraw execution completed.

Ref

Proxy Java SE 21 & JDK 21

作者

Jiaxing Gao

发布于

2024-10-22

更新于

2024-11-23

许可协议

评论

}