滴答滴:设计模式我是边学边记录,有问题的欢迎大家指出。学习的过程中会借助AI工具,我想说的是我们要把AI工具当作一个学识渊博的学者或者一个便捷的工具,同时要敢于质疑它,不能盲目的觉得对方说的一定是正确的,因为有时它的回答不见得是正确的,我们要带着自己的思考去使用AI工具,不断的和它对话和探讨,最终得出我们想要的答案。
一.什么是代理模式
代理模式是一种常用的结构型设计模式(关注类或对象的组合),允许通过代理对象来控制对另一个对象的访问,代理对象充当客户端与实际对象之间的中介,用于在客户端和实际对象之间添加一层间接层。
代理模式的核心思想是在客户端和目标对象之间引入一个代理对象,这个代理对象可以在调用目标对象的方法前后执行一些额外的操作,如日志记录、安全检查、事务管理等。通过这种方式,可以在不修改目标对象代码的情况下,增加额外的功能或控制。
大白话:代理模式就像是我们生活中找中介帮忙办事一样,代理模式中的“代理”就是那个中介,它替我们(客户端)去和目标对象打交道,并在这个过程中可以添加一些额外的操作,比如找房源、做质量检测等。想象一下,你想要买一套二手房,但自己去找房源、做质量检测、办理过户等手续既费时又费力。这时,你找到了一个中介公司,他们负责帮你找房源、检查房屋质量、协助你办理过户等手续,而你只需要选择心仪的房子并支付费用。在这个过程中,中介公司就是你的代理,它替你完成了原本需要你自己去做的繁琐工作。
二.代理模式分类
1.静态代理
静态代理是在代码编译阶段就已经生成了代理类,代理类需要实现与目标对象相同的接口。它可以在不修改目标对象的前提下对目标对象的方法进行增强。
代理模式中有抽象目标接口,目标对象,代理对象,
代码示例
// 目标接口
interface Subject {
void request();
}
// 目标对象
class RealSubject implements Subject {
@Override
public void request() {
System.out.println("处理真实的请求");
}
}
// 代理类
class ProxySubject implements Subject {
private RealSubject realSubject;
public ProxySubject(RealSubject realSubject) {
this.realSubject = realSubject;
}
@Override
public void request() {
// 在调用目标对象的方法之前,可以添加一些自定义的操作
preRequest();
// 调用目标对象的方法
realSubject.request();
// 在调用目标对象的方法之后,可以添加一些自定义的操作
postRequest();
}
private void preRequest() {
System.out.println("请求前的预处理");
}
private void postRequest() {
System.out.println("请求后的处理");
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
ProxySubject proxySubject = new ProxySubject(realSubject);
proxySubject.request(); // 输出:请求前的预处理,处理真实的请求,请求后的处理
}
}
应用场景
- 当需要在不修改目标对象代码的情况下,增加一些额外的功能时,可以使用静态代理。
- 静态代理在编译时就已经确定了代理关系,因此它适用于代理关系较为固定且不需要频繁变动的场景。
优点
- 实现简单:静态代理的代码在编译时就已经确定,因此实现起来相对简单直接。程序员可以手动编写代理类的代码,无需复杂的配置或运行时生成代码的过程。
- 易于理解和维护:由于代理类的代码是静态的,易于阅读和理解,这有助于后续的代码维护和问题排查。
- 稳定性高:静态代理通常不涉及复杂的运行时机制,因此相对更加稳定可靠。
缺点
- 无法动态地改变代理类的行为:由于静态代理的代码在编译时就确定,因此无法在运行时动态地改变代理类的行为。这限制了其灵活性和扩展性。
- 扩展性较差:如果要添加新的功能或者修改原有的功能,需要修改和重新编译代理类的代码,这可能会增加维护成本和时间。
2.动态代理
动态代理允许开发者在运行时动态地创建代理对象,这些代理对象在逻辑上代表其他对象(目标对象)。动态代理的实现主要依赖于JDK的动态代理API或第三方库如CGLIB。与静态代理不同,动态代理不需要为每个目标对象手动编写代理类,而是通过反射机制或者其他方式在运行时动态生成代理类。
2.1JDK动态代理
实现原理
在Java中,动态代理主要通过java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口来实现。
在动态代理的上下文中,当调用Proxy.newProxyInstance
方法时,该方法会动态地生成一个实现了指定接口的代理类(这个类是动态生成的,无法直接看到其源代码),并返回这个代理类的实例,即代理对象。这个代理对象确实是接口的一个实现,但它并不是传统意义上的“手动实现”接口,而是通过Java的反射和动态代理机制自动生成的,这个实现是自动生成的,并且其中的方法调用被重定向到了InvocationHandler
的invoke
方法,而不是直接在代理对象中实现这些方法的具体逻辑。简而言之,代理对象是一个特殊的实现,它依赖于InvocationHandler
来提供接口方法的具体实现逻辑,而不是在代理对象本身中直接实现这些方法。
理解Proxy.newProxyInstance
方法,Proxy.newProxyInstance
方法接收三个参数:
- 类加载器(ClassLoader):用于定义代理类的类加载器。这通常与目标对象具有相同的类加载器,以确保兼容性。
- 接口数组(Class<?>[] interfaces):代理类将实现的接口列表。代理类将是一个实现了这些接口的类。
- 调用处理器(InvocationHandler h):当代理对象的方法被调用时,这个处理器的
invoke
方法将被执行。invoke
方法的参数包括代理实例本身、被调用的方法以及传递给该方法的参数。
当通过代理对象调用方法时,Java运行时会自动将调用转发到InvocationHandler
的invoke
方法。这允许你在不修改目标对象代码的情况下,添加自定义的逻辑,如权限检查、日志记录、事务管理等。
扩展:
代理对象的方法并不包含实际的业务逻辑:在动态代理中,代理对象本身(即
Proxy.newProxyInstance
方法返回的实例)并不包含任何具体的业务逻辑代码。它仅仅是一个“壳”,一个遵循了特定接口的占位符。当你尝试调用这个代理对象的任何接口方法时,这些方法调用都会被拦截。invoke方法是方法的实际执行点:
InvocationHandler
的invoke
方法是实际执行点,它包含了所有被代理方法的调用逻辑。当你通过代理对象调用一个方法时,Java反射机制会查找对应的InvocationHandler
,并调用其invoke
方法,将代理实例本身、被调用的方法(以Method
对象的形式)以及调用该方法时传递的参数作为参数传递给invoke
方法。invoke方法中的业务逻辑:在
invoke
方法中,你可以执行任何自定义的逻辑,比如日志记录、安全检查、事务管理等。之后,你通常会通过反射调用目标对象(即被代理对象)的相应方法,来执行实际的业务逻辑。但请注意,这一步是可选的,取决于你的具体需求。代理对象与方法调用的关系:当代理对象的方法被调用时,实际上是指尝试通过代理对象访问某个接口方法。但这个调用并不会直接执行代理对象中的方法体(因为代理对象中没有具体的方法体),而是被转发到了
invoke
方法。在invoke
方法中,你可以决定是否以及如何调用目标对象的方法。处理器的invoke方法并不包含代理对象的方法:
invoke
方法本身并不包含代理对象的方法,而是提供了一个框架,让你可以在这个框架内决定如何处理对代理对象方法的调用。你可以在这个方法内部通过反射调用目标对象的方法,也可以完全忽略目标对象的方法调用,执行其他逻辑。
代码示例
// 目标接口
interface Subject {
void request();
}
// 目标对象实现
class RealSubject implements Subject {
@Override
public void request() {
System.out.println("处理真实的请求");
}
}
// 调用处理器
class InvocationHandlerImpl implements InvocationHandler {
private Object target;
public InvocationHandlerImpl(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在调用目标对象的方法之前,可以添加一些自定义的操作
preRequest();
// 调用目标对象的方法
Object result = method.invoke(target, args);
// 在调用目标对象的方法之后,可以添加一些自定义的操作
postRequest();
return result;
}
private void preRequest() {
System.out.println("请求前的预处理");
}
private void postRequest() {
System.out.println("请求后的处理");
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
InvocationHandler handler = new InvocationHandlerImpl(realSubject);
// 创建代理对象
Subject proxySubject = (Subject) Proxy.newProxyInstance(
RealSubject.class.getClassLoader(), // 类加载器
new Class[]{Subject.class}, // 目标对象实现的接口
handler // 调用处理器
);
proxySubject.request(); // 输出:请求前的预处理,处理真实的请求,请求后的处理
}
}
应用场景
- 动态代理适用于代理关系在运行时才能确定或代理类需要频繁变动的场景。
- 在Spring AOP中,就大量使用了动态代理来实现面向切面编程。
- 如果被代理的类已经实现了接口,并且你希望利用Java标准库来实现动态代理,那么基于JDK的动态代理是一个很好的选择。
优点
- 简单易用:只需要被代理类实现接口,无需修改被代理类的代码,即可实现代理功能。
- 标准支持:作为Java标准库的一部分,JDK动态代理得到了广泛的支持和稳定的维护。
- 性能较好:在大多数情况下,JDK动态代理的性能表现良好,能够满足一般的应用需求。
缺点
- 接口限制:JDK动态代理要求被代理类必须实现接口,这在一定程度上限制了它的使用范围。
- 无法代理final类和方法:由于JDK动态代理是通过接口实现的,因此无法代理final类或final方法。
- 实现逻辑需要编程实现:动态代理的实现逻辑需要程序员进行编程实现,包括代理类的创建、方法调用处理等方面,相对来说较为繁琐。
2.2CGLIB动态代理
实现原理
CGLIB(Code Generation Library)是一个基于ASM(一个Java字节码操作和分析框架)的代码生成库,它可以在运行时动态生成代理类。CGLIB动态代理通过继承被代理类来创建代理对象,因此被代理类不需要实现任何接口。这个代理类会重写目标类的所有非final方法(final方法不能被重写,因此无法被代理)。在代理类的方法中,CGLIB会使用方法拦截技术来拦截所有父类(即目标类)方法的调用,并在调用前后插入自定义的横切逻辑(如前置增强、后置增强等)。
-
初始化Enhancer:
首先,开发者会创建一个Enhancer
的实例。Enhancer
是CGLIB提供的一个类,用于在运行时动态生成代理类。 -
配置Enhancer:
接着,开发者会配置Enhancer
实例,包括设置目标类(即需要被代理的类)和回调(即实现了MethodInterceptor
接口的类的实例)。在这个配置过程中,虽然还没有直接生成字节码,但Enhancer
已经准备好了生成字节码所需的所有信息。 -
生成空的字节码对象:
当调用Enhancer
的create
方法时,CGLIB会开始生成代理类的字节码。这个过程中,首先会生成一个空的字节码对象,作为代理类的基类。这个空的字节码对象将包含代理类的基础结构,但还没有包含目标类的任何方法实现或自定义的代理逻辑。 -
填充字节码对象:
接下来,CGLIB会根据配置(如目标类、回调等)来填充这个空的字节码对象。这包括重写目标类的所有非final方法,并在这些方法内部插入调用MethodInterceptor
的intercept
方法的逻辑。这样,当代理类的方法被调用时,就会执行自定义的代理逻辑。 -
加载和实例化代理类:
最后,CGLIB会使用Java的类加载机制来加载这个动态生成的代理类,并创建其实例。这个实例就是最终的代理对象,开发者可以通过这个对象来间接调用目标类的方法,并在调用过程中执行自定义的代理逻辑。 - 方法调用:当通过代理对象调用目标类的方法时,CGLIB会拦截这个调用,并调用
MethodInterceptor
接口的intercept
方法。在intercept
方法中,可以执行自定义的前置逻辑,然后通过MethodProxy
的invokeSuper
方法调用目标类的原始方法,最后执行自定义的后置逻辑。
代码示例
public class TargetClass {
public void doSomething() {
System.out.println("目标方法");
}
}
public class CustomMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 前置处理
System.out.println("执行目标方法之前");
// 调用目标方法
Object result = proxy.invokeSuper(obj, args);
// 后置处理
System.out.println("执行目标方法之后");
return result;
}
}public class CglibDynamicProxyDemo {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TargetClass.class); // 设置父类
enhancer.setCallback(new CustomMethodInterceptor()); // 设置回调
TargetClass proxy = (TargetClass) enhancer.create(); // 创建代理对象
proxy.doSomething(); // 通过代理对象调用目标方法
}
}
应用场景
- 如果被代理的类没有实现接口,或者你需要更灵活的代理方式(如代理构造函数等),那么可以考虑使用基于CGLIB的动态代理。
扩展:
在Java中,
final
类不能被继承,因此CGLIB无法通过继承final
类来创建代理。同样,final
方法也不能被重写,所以CGLIB无法在代理类中重写final
方法以插入额外的逻辑。然而,CGLIB在处理对
final
方法的调用时,可以采用一些间接的策略,比如使用FastClass
机制。FastClass
是CGLIB提供的一种机制,它使用Java反射API来生成一个代理类,这个代理类包含了与被代理类方法相对应的快速访问方法。当调用被代理类的final
方法时,CGLIB可以通过这个FastClass
代理类来间接调用原始方法,而不是直接重写它。但请注意,这并不意味着CGLIB“代理”了final
方法本身;它只是提供了一种快速访问原始final
方法的方式,而没有在代理类中重写这些方法。因此,严格来说,CGLIB不能直接代理
final
类和final
方法。但是,通过一些间接的手段,它可以在不直接修改或重写这些final
方法的情况下,实现对它们的调用过程的控制或监控。然而,这种控制或监控的能力通常比直接代理非final
方法要有限得多,并且可能涉及到更多的性能开销。
优点
- 无接口限制:CGLIB动态代理通过继承被代理类来创建代理对象,因此无需被代理类实现接口。
- 更强大的代理能力:由于CGLIB可以代理类,因此它可以处理那些没有接口的类。
- 高度定制:CGLIB提供了更多的定制选项,可以更加灵活地控制代理对象的行为。
缺点
- 相对复杂:CGLIB动态代理的实现相对复杂,需要借助ASM等字节码操作框架,对于初学者来说可能较难上手。
- 性能开销较大:由于CGLIB需要生成新的类来实现代理,因此在性能上可能存在一定的开销,尤其是在大量创建和销毁代理对象时。
- 可能破坏封装性:由于CGLIB是通过继承被代理类来创建代理对象的,这可能会破坏被代理类的封装性,导致一些非预期的行为。