Java中23种设计模式之代理模式
一、什么是代理模式?
代理模式(Proxy Pattern)是一种结构型设计模式,其核心思想是:
通过引入一个代理对象作为中间层,控制对目标对象(真实对象)的访问,并在访问前后添加额外的处理逻辑。
代理模式的本质是 “接力调用”,代理对象和目标对象实现相同的接口,对外提供一致的访问方式,但代理对象会在调用目标对象的方法前后 “插队” 执行自己的逻辑。
二、代理模式的核心角色
代理模式涉及三个核心角色:
- 抽象主题(Subject):定义代理和目标对象的公共接口,确保两者具有相同的行为。
- 真实主题(Real Subject):实际处理业务逻辑的目标对象,如 “明星”。
- 代理主题(Proxy Subject):持有真实主题的引用,在调用真实主题的方法前后添加额外逻辑,如 “经纪人”。
三、案例场景:明星与经纪人
我们以 “明星表演” 为例:
- 真实主题:明星(ConcreteStar)负责核心表演逻辑。
- 代理主题:经纪人(StarAgentProxy)作为代理,在明星表演前后处理事务(如安排场地、结算费用)。
- 抽象主题:定义 “表演” 接口(Star),约束明星和经纪人的共同行为。
四、代码实现(Java 静态代理)
1. 抽象主题接口(Star)
// 定义明星的公共行为:表演
interface Star {void perform(); // 表演方法
}
2. 真实主题:具体明星(ConcreteStar)
// 真实明星类,实现核心表演逻辑
class ConcreteStar implements Star {@Overridepublic void perform() {System.out.println("明星开始精彩的表演!"); // 核心业务逻辑}
}
3. 代理主题:经纪人(StarAgentProxy)
// 经纪人代理类,实现Star接口并持有明星引用
class StarAgentProxy implements Star {private final Star realStar; // 让经纪人代理类持有目标对象(真实明星)的引用//这样在下面就可以调用真实明星的方法// 构造方法:接收真实明星对象public StarAgentProxy(Star realStar) {this.realStar = realStar;}@Overridepublic void perform() {// 前置处理:代理在调用真实对象前的逻辑System.out.println("经纪人:安排场地、宣传推广、对接流程...");// 调用真实明星的表演方法realStar.perform(); // 核心逻辑由真实对象处理// 后置处理:代理在调用真实对象后的逻辑System.out.println("经纪人:处理费用结算、安排下一场行程...");}
}
4. 客户端测试(StaticProxyTest)
public class StaticProxyTest {public static void main(String[] args) {// 创建真实明星对象Star realStar = new ConcreteStar();// 创建经纪人代理对象,传入真实明星Star agentProxy = new StarAgentProxy(realStar);// 客户端通过代理对象调用方法(仿佛直接调用真实对象)agentProxy.perform();}
}
运行结果:
5.变量引用关系
在代码 Star agentProxy = new StarAgentProxy(realStar);
里:
realStar
是ConcreteStar
类的一个实例,也就是目标对象。agentProxy
是StarAgentProxy
类的一个实例,即代理对象。
在 StarAgentProxy
类的构造方法 public StarAgentProxy(Star star) { this.star = star; }
中,只是把 realStar
对象的引用传递给了 StarAgentProxy
类的成员变量 star
,这意味着 StarAgentProxy
类的实例 agentProxy
持有了 realStar
对象的引用,而并非 agentProxy
本身就是 realStar
对象。
当执行 agentProxy.perform();
时,由于 agentProxy
是 StarAgentProxy
类的实例,所以调用的是 StarAgentProxy
类中重写的 perform()
方法。
五、代理模式的核心原理
- 接口约束:代理和真实对象实现相同接口,确保客户端可以像使用真实对象一样使用代理对象(透明性)。
- 组合关系:代理对象通过构造方法接收真实对象(
has-a
关系),而非继承(is-a
),降低耦合度。- 责任分离:
- 真实对象专注核心业务(如 “表演”),
- 代理对象专注辅助逻辑(如 “预处理” 和 “后处理”)。
六、继承实现代理的三大致命缺点
// 明星接口(抽象主题)
interface Star {void perform(); // 表演方法
}// 真实明星(目标对象)
class ConcreteStar implements Star {@Override public void perform() {System.out.println("明星开始精彩的表演!");}
}// 经纪人代理类(继承真实明星)
class StarAgentProxy extends ConcreteStar { // 注意:这里用了继承@Override public void perform() {System.out.println("经纪人:安排场地、对接流程...");super.perform(); // 调用父类(明星)的方法System.out.println("经纪人:处理费用结算、安排下一场...");}
}// 测试
public class InheritanceProxyTest {public static void main(String[] args) {Star agent = new StarAgentProxy(); // 代理对象是真实明星的子类agent.perform();}
}
1. 强耦合:代理类绑定具体实现,而非抽象
- 问题:
StarAgentProxy
直接继承ConcreteStar
(具体类),而非Star
(接口)。
如果明星类需要更换实现(比如新增一个AdvancedStar
类),代理类必须跟着修改继承关系,违背了 “依赖抽象而非具体” 的设计原则。// 假设新增一个更高级的明星类 class AdvancedStar implements Star { /* ... */ } // 此时代理类若想代理AdvancedStar,必须修改继承关系,违反开闭原则 class StarAgentProxy extends AdvancedStar { /* ... */ }
2. 单继承限制:无法代理多个对象或扩展其他功能
- Java 特性:单继承机制导致代理类只能继承一个具体类,无法同时代理多个不同的目标对象(如同时代理 “歌手” 和 “演员” 两个不同类)。
- 扩展性差:如果代理类需要额外功能(如日志记录),只能通过多层继承实现,导致类层次结构复杂,违背 “组合优于继承” 原则。
3. 违背接口隔离:代理类依赖目标类的具体实现
- 本质问题:代理模式的核心是 “控制对目标对象的访问”,而继承方式让代理类直接拥有目标类的所有方法和属性(包括不需要的)。
例如:如果ConcreteStar
新增一个sing()
方法,代理类会自动继承该方法,而代理类可能并不需要这个方法,导致接口污染。
七、代理模式的正确打开方式:组合(Has-A)而非继承(Is-A)
核心思想
代理类与目标对象实现相同接口(基于抽象),通过构造方法持有目标对象的引用(组合关系),而非继承具体类。这样:
- 代理类只依赖接口(低耦合),可代理任何实现该接口的目标对象
- 遵循 “开闭原则”,新增目标类时无需修改代理类
- 灵活扩展,可在运行时动态替换目标对象
八、两种实现方式对比表
特性 | 继承式代理 | 组合式代理(正确方式) |
---|---|---|
耦合度 | 依赖具体类(强耦合) | 依赖接口(低耦合) |
开闭原则 | 新增目标类需修改代理类(违反) | 可直接代理新目标类(符合) |
扩展性 | 单继承限制,功能扩展困难 | 可自由组合其他功能(如日志) |
适用场景 | 仅能代理特定类 | 可代理所有实现接口的类 |
设计原则 | 违背 “依赖抽象” 原则 | 严格遵循接口隔离原则 |
九、静态代理的优缺点
优点:
- 符合开闭原则:不修改真实对象代码,通过代理类扩展功能。
- 职责清晰:代理与真实对象分工明确,代码易维护。
- 保护真实对象:代理可控制对真实对象的访问(如权限校验、日志记录)。
缺点:
- 类爆炸问题:每个真实对象需对应一个代理类,若接口方法多或真实对象多,会导致代理类数量膨胀。
- 灵活性有限:代理类的逻辑固定,若需通用增强逻辑(如所有方法都加日志),静态代理难以复用。
大家思考一下:如果系统中业务接口很多,一个接口对应一个代理类,显然也是不合理的,会导致类爆炸。怎么解决这个问题?
动态代理可以解决。因为在动态代理中可以在内存中动态的为我们生成代理类的字节码。代理类不需要我们写了。类爆炸解决了,而且代码只需要写一次,代码也会得到复用。
十、动态代理
1.动态代理概述
静态代理虽然能在不修改目标对象的基础上增强其功能,但存在类爆炸的问题。即每有一个目标对象,就需要为其创建一个对应的代理类。而动态代理则很好地解决了这个问题,它可以在运行时动态地生成代理类,无需为每个目标对象都手动编写代理类。
在 Java 中,实现动态代理主要有两种方式:基于 java.lang.reflect.Proxy
和 InvocationHandler
的 JDK 动态代理,以及基于 CGLIB 的动态代理。下面将分别详细介绍这两种方式。
2.JDK 动态代理
⑴.原理
JDK 动态代理是基于接口的代理方式。它通过 java.lang.reflect.Proxy
类和 java.lang.reflect.InvocationHandler
接口来实现。Proxy
类负责生成代理类的实例,而 InvocationHandler
接口则用于定义代理对象方法调用时的处理逻辑。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;// 定义一个公共接口,代表明星的表演行为
interface Star {// 表演方法void perform();
}// 具体的明星类,实现 Star 接口
class ConcreteStar implements Star {@Overridepublic void perform() {System.out.println("明星开始精彩的表演!");}
}// 动态代理的调用处理器
class StarInvocationHandler implements InvocationHandler {// 持有目标对象的引用private final Object target;// 构造方法,接收目标对象作为参数public StarInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 在调用目标对象的方法之前可以添加额外操作System.out.println("经纪人安排场地、宣传等准备工作。");// 调用目标对象的方法Object result = method.invoke(target, args);// 在调用目标对象的方法之后可以添加额外操作System.out.println("经纪人处理后续事务,如结算费用等。");return result;}
}// 测试类
public class JdkDynamicProxyTest {public static void main(String[] args) {// 创建具体的明星对象Star realStar = new ConcreteStar();// 创建动态代理的调用处理器,并传入目标对象StarInvocationHandler handler = new StarInvocationHandler(realStar);// 使用 Proxy 类的 newProxyInstance 方法创建动态代理对象Star proxy = (Star) Proxy.newProxyInstance(Star.class.getClassLoader(),new Class<?>[]{Star.class},handler);// 调用动态代理对象的方法proxy.perform();}
}
代码解释
Star
接口:定义了明星的核心行为perform()
,这是代理对象和目标对象都要实现的公共接口。ConcreteStar
类:具体的明星类,也就是目标对象,实现了perform()
方法。StarInvocationHandler
类:实现了InvocationHandler
接口,其中的invoke
方法是核心,它会在代理对象的方法被调用时自动执行。在invoke
方法中,可以在调用目标对象的方法前后添加额外的操作。
⑵.方法解释
Proxy.newProxyInstance
方法定义在 java.lang.reflect.Proxy
类中,其签名如下:
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException
参数解释
1.
ClassLoader loader
- 作用:指定用于加载代理类的类加载器。类加载器负责将代理类的字节码加载到 Java 虚拟机(JVM)中。通常使用目标对象所实现接口的类加载器,因为代理类需要与目标对象实现相同的接口,使用相同的类加载器可以确保代理类和接口在同一个类加载环境中。
- 代码示例中的情况:
Star.class.getClassLoader()
获取了Star
接口的类加载器。由于Star
接口是目标对象ConcreteStar
所实现的接口,使用这个类加载器可以保证代理类能够正确加载并实现Star
接口。2.
Class<?>[] interfaces
- 作用:指定代理类要实现的接口数组。代理类会实现这些接口中定义的所有方法,这样代理对象就可以像目标对象一样被使用,因为它们实现了相同的接口。
- 代码示例中的情况:
new Class<?>[]{Star.class}
表示代理类要实现Star
接口。这里使用数组是因为代理类可以同时实现多个接口,例如new Class<?>[]{Interface1.class, Interface2.class}
表示代理类将同时实现Interface1
和Interface2
两个接口。3.
InvocationHandler h
- 作用:指定代理对象方法调用的处理程序。当调用代理对象的方法时,实际上会调用
InvocationHandler
中的invoke
方法。在invoke
方法中,可以添加额外的逻辑,如在调用目标对象的方法前后进行一些操作,然后再调用目标对象的实际方法。- 代码示例中的情况:
handler
是StarInvocationHandler
类的实例。StarInvocationHandler
实现了InvocationHandler
接口,并重写了invoke
方法,在该方法中添加了经纪人在明星表演前后的操作逻辑。返回值
- 作用:返回一个实现了指定接口的代理对象。这个代理对象可以像目标对象一样调用接口中定义的方法,但在调用方法时会经过
InvocationHandler
的处理。- 代码示例中的情况:
(Star) Proxy.newProxyInstance(...)
将返回的代理对象强制转换为Star
类型,因为代理类实现了Star
接口。这样就可以通过proxy
变量调用Star
接口中定义的perform
方法。
⑶.通过 Star 类型的引用变量调用 perform 方法时,为什么是调用了代理对象实现的 perform 方法。
在Java的动态代理机制中,当通过Star类型的引用变量调用perform()方法时,实际上调用的是代理对象的方法,而代理对象内部通过InvocationHandler将调用转发给真实对象,并允许在调用前后插入额外逻辑。以下是详细的步骤解析:
① 动态代理的核心机制
动态代理对象是通过Proxy.newProxyInstance动态生成的类实例,它实现了指定的接口(如Star)。
代理对象的所有方法调用都会被重定向到InvocationHandler的invoke方法。
②代理对象的创建过程
在JdkDynamicProxyTest类中:
// 创建代理对象
Star proxy = (Star) Proxy.newProxyInstance(Star.class.getClassLoader(),new Class<?>[]{Star.class},handler
);
-
类加载器:
Star.class.getClassLoader()
用于加载代理类。 -
接口列表:
new Class<?>[]{Star.class}
表明代理类会实现Star
接口。 -
调用处理器:
handler
是StarInvocationHandler
实例,负责处理实际的方法调用。
③ 代理对象的方法调用流程
当调用proxy.perform()时:
代理对象拦截调用
动态生成的代理类覆盖了Star接口的所有方法(如perform())。当调用perform()时,代理类的方法实现会触发InvocationHandler.invoke()。
转发到InvocationHandler的invoke方法
代理类的方法实现类似以下伪代码:
public void perform() {handler.invoke(this, // 代理对象自身methodPerform, // 通过反射获取的Method对象(对应perform()方法)new Object[0] // 方法参数(本例中无参数));
}
在invoke
方法中处理逻辑
StarInvocationHandler
的invoke
方法执行以下操作:
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;
}
method.invoke(target, args)会调用真实对象(ConcreteStar实例)的perform()方法。 通过这种方式,代理对象在调用真实对象方法前后添加了额外逻辑。
④关键:代理对象与接口的关系
代理对象是接口的实现类
代理对象动态实现了Star接口,因此可以赋值给Star类型的变量。
方法调用的本质是动态分发
当通过Star接口调用perform()时,实际调用的是代理对象的实现方法,而代理对象的实现方法会委托给InvocationHandler。
⑤ 为什么不是直接调用真实对象的方法?
代理对象的目的是增强行为
动态代理的核心思想是“控制访问”——通过代理对象间接调用真实对象,从而在调用前后插入逻辑(如日志、事务管理等)。
透明性
调用者(如JdkDynamicProxyTest类)无需知道实际调用的是代理对象还是真实对象,只需面向接口编程。
总结
当调用proxy.perform()时,代理对象(动态生成的类)拦截了方法调用,并通过InvocationHandler将调用转发给真实对象。
InvocationHandler的invoke方法负责协调调用过程,既调用了真实对象的方法,又添加了额外逻辑。
动态代理通过接口实现透明性,使得调用者无需关心底层是直接调用还是代理增强。
⑷.invoke
方法
invoke
方法是 java.lang.reflect.InvocationHandler
接口中定义的唯一方法,当调用动态代理对象的方法时,实际上会执行 InvocationHandler
实现类中的 invoke
方法。下面详细解释 invoke
方法的各个参数以及方法体的逻辑。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
参数解释
1.
Object proxy
- 作用:表示生成的动态代理对象本身。这个对象是通过
Proxy.newProxyInstance
方法创建出来的代理对象。不过在invoke
方法中,通常不会直接使用proxy
对象,因为如果在invoke
方法中再次调用proxy
对象的方法,会导致无限递归调用,因为每次调用代理对象的方法都会触发invoke
方法。- 示例代码中的情况:在当前的
StarInvocationHandler
类的invoke
方法中,并没有使用proxy
参数。2.
Method method
- 作用:表示被调用的代理对象的方法。
Method
类是 Java 反射机制中的一个重要类,它包含了方法的各种信息,如方法名、参数类型、返回值类型等。通过method
对象,可以使用反射机制调用目标对象的对应方法。- 示例代码中的情况:在
invoke
方法中,method.invoke(target, args)
这行代码使用method
对象调用了目标对象target
的对应方法。3.
Object[] args
- 作用:表示调用代理对象方法时传递的参数数组。如果调用的方法没有参数,
args
数组将为null
;如果有参数,args
数组中的元素将按照方法参数的顺序依次排列。- 示例代码中的情况:在
Star
接口的perform
方法没有参数,所以在调用proxy.perform()
时,args
数组为null
。但如果Star
接口的方法有参数,这些参数会被封装在args
数组中传递给invoke
方法。
@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;
}
调用目标对象的方法
Object result = method.invoke(target, args);
使用 Method
对象的 invoke
方法调用目标对象 target
的对应方法,并将调用结果存储在 result
变量中。invoke
方法的第一个参数是要调用方法的对象,这里是目标对象 target
;第二个参数是调用方法时传递的参数数组,即 args
。
返回结果
return result;
将目标对象方法的调用结果返回,这样调用代理对象方法的代码就可以获取到目标对象方法的返回值。
CGLIB 动态代理:无接口场景下的代理方案
一、为什么需要 CGLIB 动态代理?
前文介绍的 JDK 动态代理基于接口实现,要求目标对象必须实现至少一个接口。但现实中存在大量没有接口的类(如遗留代码、第三方工具类),此时 JDK 代理失效。
CGLIB 动态代理应运而生:它通过生成目标类的子类实现代理,无需目标类实现接口,弥补了 JDK 代理的局限性。
二、CGLIB 核心原理
1. 核心思想
- 继承代理:CGLIB 在运行时动态生成目标类的子类(代理类),代理类重写目标类的方法。
- 方法拦截:通过回调函数(
MethodInterceptor
)拦截方法调用,在子类方法中添加增强逻辑(如前置 / 后置处理)。
2. 核心类
Enhancer
:CGLIB 的核心类,用于设置代理类的父类(目标类)和回调函数,生成代理对象。MethodInterceptor
:核心接口,定义方法拦截逻辑,相当于 JDK 代理中的InvocationHandler
。MethodProxy
:CGLIB 的方法代理类,用于高效调用目标类的方法(比反射更快)。
三、代码实战:用 CGLIB 代理 “无接口明星”
场景说明
假设明星类ConcreteStar
没有实现任何接口(现实中可能存在的遗留类),需要经纪人代理其表演流程。
1. 添加 CGLIB 依赖
Maven 项目需在pom.xml
中添加:
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version>
</dependency>
2. 目标类(无接口)
// 没有实现任何接口的明星类
class ConcreteStar {public void perform() { // 核心业务方法System.out.println("明星开始精彩的表演!");}// 假设这是一个不需要代理的final方法(CGLIB无法代理final方法)public final void sing() { System.out.println("明星演唱一首歌曲"); }
}
3. CGLIB 代理类(方法拦截器)
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;// 实现MethodInterceptor接口定义拦截逻辑
class StarCglibProxy implements MethodInterceptor {// 目标对象(被代理的真实对象)private final Object target;public StarCglibProxy(Object target) {this.target = target;}@Overridepublic Object intercept(Object proxy, // 代理对象(子类实例)Method method, // 被调用的目标方法Object[] args, // 方法参数MethodProxy proxyMethod // CGLIB的方法代理,用于调用目标方法) throws Throwable {// 前置处理:经纪人准备工作System.out.println("经纪人:安排场地、调试设备、对接流程...");// 方式1:通过反射调用目标方法(与JDK代理类似)// Object result = method.invoke(target, args);// 方式2:通过CGLIB的MethodProxy调用(更高效)Object result = proxyMethod.invokeSuper(proxy, args); // 调用父类(目标类)的方法// 后置处理:经纪人收尾工作System.out.println("经纪人:结算费用、安排下一场行程...");return result; // 返回目标方法的执行结果}
}
4. 客户端测试
import net.sf.cglib.proxy.Enhancer;public class CglibProxyTest {public static void main(String[] args) {// 1. 创建目标对象ConcreteStar realStar = new ConcreteStar();// 2. 创建CGLIB增强器Enhancer enhancer = new Enhancer();// 3. 设置代理类的父类(即目标类)enhancer.setSuperclass(realStar.getClass());// 4. 设置回调函数(方法拦截器)enhancer.setCallback(new StarCglibProxy(realStar));// 5. 生成代理对象(目标类的子类)ConcreteStar proxyStar = (ConcreteStar) enhancer.create();// 6. 调用代理对象的方法proxyStar.perform(); // 正常代理// 测试final方法:CGLIB无法代理,会直接调用目标类的方法proxyStar.sing(); // 输出:明星演唱一首歌曲(无代理逻辑)}
}
对于高版本的JDK,如果使用CGLIB,需要在启动项中添加两个启动参数:
-
在左侧列表中选择你的运行配置(如
Client
)。 -
在右侧的 VM options 输入框中,将两个参数合并为一行输入:
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/sun.net.util=ALL-UNNAMED
四、CGLIB 关键细节解析
1. intercept
方法参数对比
参数名 | 含义 | 与 JDK 代理的区别 |
---|---|---|
proxy | 代理对象(目标类的子类实例) | JDK 代理中是接口类型,CGLIB 中是子类实例 |
method | 被调用的目标方法(反射对象) | 与 JDK 代理的Method 参数含义相同 |
args | 方法参数数组 | 与 JDK 代理相同 |
proxyMethod | CGLIB 的方法代理对象,用于高效调用目标方法 | JDK 代理中无此参数,需通过method.invoke |
2. 调用目标方法的两种方式
- 反射调用(
method.invoke
):与 JDK 代理一致,兼容性强但性能稍低。 - CGLIB 专用调用(
proxyMethod.invokeSuper
):- 内部通过字节码生成技术直接调用父类方法,性能比反射快5-10 倍。
- 注意:
proxyMethod.invoke(proxy, args)
会递归调用代理类的方法(导致死循环),需用invokeSuper
调用父类(目标类)方法。
3. 代理范围限制
- 无法代理
final
类:CGLIB 通过继承实现代理,final
类不能被继承。 - 无法代理
final
方法:final
方法不能被重写,会直接调用目标类的方法(如示例中的sing()
方法)。 - 构造方法无法代理:代理类继承目标类,构造方法由目标类初始化。
五、CGLIB vs JDK 动态代理:核心对比
特性 | JDK 动态代理 | CGLIB 动态代理 |
---|---|---|
代理基础 | 接口(必须实现至少一个接口) | 类(生成目标类的子类) |
性能 | 反射调用,稍慢 | 字节码生成,调用效率更高 |
适用场景 | 有接口的目标对象 | 无接口、或需要代理类的场景 |
局限性 | 依赖接口,无法代理无接口类 | 无法代理 final 类 / 方法,继承可能破坏封装 |
Spring 中的应用 | 代理接口(如@Transactional 接口方法) | 代理类(如无接口的组件,或@Configuration 类) |
六、CGLIB 适用场景
- 无接口类代理:处理遗留代码、第三方工具类(如
HashMap
、ArrayList
等没有接口的类)。 - 高性能需求:
MethodProxy
比反射调用更快,适合对性能敏感的场景(如高频调用的工具类)。 - AOP 底层实现:Spring 框架中,当 Bean 没有实现接口时,默认使用 CGLIB 代理(可通过
proxy-target-class
配置强制使用)。 - 字节码增强:除了方法拦截,CGLIB 还可用于动态生成类、修改类结构(如添加字段、方法)。
七、CGLIB 代理的优缺点
优点:
- 无接口依赖:弥补 JDK 代理的局限性,适用范围更广。
- 高性能:通过字节码生成和
MethodProxy
优化调用效率。 - 灵活扩展:可代理类的所有非 final 方法,支持更细粒度的拦截。
缺点:
- 继承限制:代理类是目标类的子类,可能破坏类的封装性(如覆盖父类方法)。
- final 方法限制:无法代理
final
修饰的类或方法。 - 类加载问题:生成的代理类可能导致类加载器相关问题(如多模块环境)。
八、最佳实践
- 优先接口代理:如果目标类有接口,优先使用 JDK 动态代理(更符合面向接口设计)。
- 合理处理 final 成员:避免在目标类中对关键方法使用
final
修饰,确保 CGLIB 可代理。 - 结合 Spring 配置:在 Spring 中通过
@EnableAspectJAutoProxy(proxyTargetClass = true)
强制使用 CGLIB 代理类。 - 性能敏感场景:对高频调用的方法,优先使用 CGLIB 的
MethodProxy.invokeSuper
而非反射调用。
九、总结:CGLIB 的核心价值
CGLIB 动态代理是 JDK 代理的重要补充,解决了无接口类的代理问题,在框架设计(如 Spring AOP)和遗留代码处理中不可或缺。其核心思想是通过继承 + 方法拦截实现代理,相比 JDK 代理更灵活但也有更多限制。
理解 CGLIB 与 JDK 代理的区别,能帮助我们在不同场景下选择合适的代理方案:
- 有接口 → JDK 动态代理(优雅、符合设计原则)
- 无接口 → CGLIB 代理(强制代理,牺牲部分封装性)
通过动态代理技术,我们实现了 “在不修改目标对象的前提下扩展功能”,这正是 AOP(面向切面编程)的核心思想。掌握 CGLIB,能让我们在面对复杂代码结构时更游刃有余,写出更具扩展性的代码。
相关文章:
Java中23种设计模式之代理模式
一、什么是代理模式? 代理模式(Proxy Pattern)是一种结构型设计模式,其核心思想是: 通过引入一个代理对象作为中间层,控制对目标对象(真实对象)的访问,并在访问前后添加…...
git单独跟踪远程分支及处理合并异常情况
在 Git 中,自动跟踪远程分支(Tracking Remote Branch)是指: 当你创建一个本地分支时,让它直接关联(绑定)到对应的远程分支。这样,后续的 git pull、git push 等操作可以省略参数&…...
口腔助手|口腔挂号预约小程序|基于微信小程序的口腔门诊预约系统的设计与实现(源码+数据库+文档)
口腔小程序 目录 基于微信小程序的口腔门诊预约系统的设计与实现 一、前言 二、系统功能设计 三、系统实现 1、小程序前台界面实现 2、后台管理员模块实现 四、数据库设计 1、实体ER图 2、具体的表设计如下所示: 五、核心代码 六、论文参考 七、最新计算…...
Windows本地账户后门被关,微软强制使用在线账户
初次接触Windows10或者Windows11的同学应该都被微软一开始激活注册的在线账户坑过吧。 一切都按照微软的正向指引激活步骤,但到了账户注册的步骤时,不明所以的小白会按照微软的步骤进行新注册账户。 但坑就在这里,由于微软的账户服务器在国外…...
分类算法的介绍和应用场景
分类算法 1.算法介绍 和聚类是有区别的聚类是没有标签的 数据集中必须包含明确的类别标签,即已知每个样本所属的类别。这些标签作为学习的目标,指导模型的训练过程。 2.应用场景 广泛应用于需要对数据进行明确分类和预测的场景,如医疗诊断…...
将 CrewAI 与 Elasticsearch 结合使用
作者:来自 Elastic Jeffrey Rengifo 学习如何使用 CrewAI 为你的代理团队创建一个 Elasticsearch 代理,并执行市场调研任务。 CrewAI 是一个用于编排代理的框架,它通过角色扮演的方式让多个代理协同完成复杂任务。 如果你想了解更多关于代理…...
n8n自动化之添加jenkins
n8n自动化之添加jenkins Jenkins添加Api Token 点击“账户”点击“设置” 添加API Token 找到API Token,点击“添加新 Token”输入用户名点击“生成” 复制并保存秘钥 用生成token的用户名和密码填充下面的用户名和密码Jenkins instance URL是Jenkins文件夹的…...
DFS--
数字的全排列 #include <bits/stdc.h> using namespace std;//最大的排列数目 const int N10; int n; //存储排列的路径 int path[N]; //标记数字是否已经被使用 bool st[N];void dfs(int u){//到达递归边界,输出一个排列if(un){//输出循环for(int i0; i<…...
Vue:路由切换表格塌陷
目录 一、 出现场景二、 解决方案 一、 出现场景 当路由切换时,表格操作栏会出现行错乱、塌陷的问题 二、 解决方案 在组件重新被激活的时候刷新表格 <el-table ref"table"></el-table>activated(){this.$nextTick(() > {this.$refs[t…...
Ubuntu进入Recovery模式遇到问题
Ubuntu进入Recovery模式需要按ESC,但是没人告诉你进入后并不显示Advanced option.... 这种菜单,而是下面这个界面: 我分别测试了Ubuntu18和24的版本,都存在这个问题,就是不管你按一次ESC还是一直按着ESC都会进入到这个模式里。 非…...
淘宝API驱动跨境选品:多语言详情页自动翻译与本地化定价
淘宝 API 驱动跨境选品实现多语言详情页自动翻译与本地化定价,为跨境电商业务带来诸多便利与优势,以下是详细介绍: 一、多语言详情页自动翻译 技术原理 借助淘宝的 API 接口,获取商品详情页的各类文本信息,包括标题、描…...
IDEA 2024 Maven 设置为全局本地仓库,避免新建项目重新配置maven
使用idea创建Java项目时每次都要重新配置Maven,非常麻烦。其实IDEA可以配置全局Maven。方法如下: 1.关闭所有项目进入初始页面 2.选择所有配置 3.设置为自己的路径...
C++类成员内存分布详解
本文将探讨C类中成员变量的内存分布情况,包括普通成员、静态成员、虚函数等不同情况下的内存布局。 一、基本成员内存布局 1. 普通成员变量 普通成员变量按照声明顺序在内存中连续排列(受访问修饰符和内存对齐影响): class Nor…...
【PVR】《Palm Vein Recognition and Large-scale Research based on Deep Learning》
邬晓毅. 基于深度学习的掌静脉识别及规模化研究[D]. 四川:电子科技大学,2024. 文章目录 1、背景2、相关工作3、创新点和贡献4、方法和实验4.1、知识介绍4.2、基于自适应损失函数的掌静脉识别算法研究4.3、退化图像的掌静脉识别鲁棒性提升研究4.4、掌静脉识别系统规模化 5、总结…...
【码农日常】vscode编码clang-format格式化简易教程
文章目录 0 前言1 工具准备1.1 插件准备1.2 添加.clang-format1.3 添加配置 2 快速上手 0 前言 各路大神都说clangd好,我也来试试。这篇主要讲格式化部分。 1 工具准备 1.1 插件准备 照图安装。 1.2 添加.clang-format 右键添加文件,跟添加个.h或者.c…...
CExercise_08_字符串_2统计该字符串中每个字符出现的次数,统计过程中忽略大小写的差异,并打印最终每个字符出现的次数。
题目:CExercise_ 给定一个字符串,要求它可能包含数字和字母。 请编写函数,统计该字符串中每个字符出现的次数,统计过程中忽略大小写的差异,并打印最终每个字符出现的次数。 提示: 用一个int数组存储字符出现…...
LabVIEW 中 JSON 数据与簇的转换
在 LabVIEW 编程中,数据格式的处理与转换是极为关键的环节。其中,将数据在 JSON 格式与 LabVIEW 的簇结构之间进行转换是一项常见且重要的操作。这里展示的程序片段就涉及到这一关键功能,以下将详细介绍。 一、JSON 数据与簇的转换功能 &am…...
分布式文件存储系统FastDFS
文章目录 1 分布式文件存储1_分布式文件存储的由来2_常见的分布式存储框架 2 FastDFS介绍3 FastDFS安装1_拉取镜像文件2_构建Tracker服务3_构建Storage服务4_测试图片上传 4 客户端操作1_Fastdfs-java-client2_文件上传3_文件下载4_获取文件信息5_问题 5 SpringBoot整合 1 分布…...
DNS域名解析(以实操为主)
目录 一.正向解析(在Linux下) 1.1什么是正向解析 1.2具体操作 1编辑主配置文件 /etc/named.conf 2编辑域名文件 /etc/named.rfc1912.zones 3根据域名文件中定义的名称,来建立数据库文件 4重启服务 5验证 二.正向解析(在…...
Spark运行架构 RDD相关概念Spark-Core编程
以下是今天学习的知识点: 第三节 Spark运行架构 运行架构 Spark 框架的核心是一个计算引擎,整体来说,它采用了标准 master-slave 的结构。 核心组件 对于 Spark 框架有两个核心组件: Driver Spark 驱动器节点,用…...
校园智能硬件国产化的现状与意义
以下是校园智能硬件国产化的现状与意义: 现状 政策支持力度大:近年来,国家出台了一系列政策推动教育数字化和国产化发展。如2022年教育部等六部门印发《关于推进教育新型基础设施建设构建高质量教育支撑体系的指导意见》,明确提出…...
Android里面如何优化xml布局
在 Android 开发中,以下是系统化的优化方案,从基础到高级分层解析: 一、基础优化策略 1. 减少布局层级 问题:每增加一层布局,测量/布局时间增加 1-2ms 解决方案: <!-- 避免嵌套 --> <LinearLayo…...
Android10.0 framework第三方无源码APP读写断电后数据丢失问题解决
1.前言 在10.0中rom定制化开发中,在某些产品开发中,在某些情况下在App用FileOutputStream读写完毕后,突然断电 会出现写完的数据丢失的问题,接下来就需要分析下关于使用FileOutputStream读写数据的相关流程,来实现相关 功能 2.framework第三方无源码APP读写断电后数据丢…...
LabVIEW驱动开发的解决思路
在科研项目中,常面临将其他语言开发的定制采集设备驱动转换为 LabVIEW 适用形式的难题。特别是当原驱动支持匮乏、开发人员技术支持不足时,如何抉择解决路径成为关键。以下提供具体解决思路,助力高效解决问题。 一、评估现有驱动死磕的可…...
【C++】 —— 笔试刷题day_13
一、牛牛冲钻五 题目描述 题目说,牛牛在玩炉石传说,现在牛牛进行了n场游戏,让我们判断牛牛上了几颗星。 首先输入一个T,表示T组数据; 然后输入n和k,表示一个进行了n场数据,k表示连胜三场及以上…...
【ROS】分布式通信架构
【ROS】分布式通信架构 前言环境要求主机设置(Master)从机设置(Slave)主机与从机通信测试本文示例启动ROS智能车激光雷达节点本地计算机配置与订阅 前言 在使用 ROS 时,我们常常会遇到某些设备计算能力不足的情况。例…...
【第39节】windows编程:打造MFC版本任务管理器
目录 一、项目概述 二、项目开发的各种功能关键 2.1 进程信息的获取 2.2 线程信息的获取 2.3 进程模块信息的获取 2.3.1 模块快照 2.3.2 枚举模块 2.4 进程堆信息的获取 2.5 窗口信息的获取 2.6 文件信息的获取 2.7 内存信息和CPU占用率的获取 2.7.1 内存信息相关结…...
1.认识C语言
上层:应用软件 下层:操作系统、硬件 C语言擅长于下层方面 计算机语言的发展:低级 ——> 高级 用计算机的二进制指令写代码(低级语言) —— > 汇编指令(低级语言,用到了助记符ÿ…...
MySQL下200GB大表备份,利用传输表空间解决停服发版表备份问题
MySQL下200GB大表备份,利用传输表空间解决停服发版表备份问题 问题背景 在停服发版更新时,需对 200GB 大表(约 200 亿行数据)进行快速备份以预防操作失误。 因为曾经出现过有开发写的发版语句里,UPDATE语句的WHERE条…...
《Sqoop 快速上手:安装 + 测试实战》
推荐原文 见:http://docs.xupengboo.top/bigdata/di/sqoop.html Sqoop(SQL-to-Hadoop) 是 Apache 开源的工具,专门用于在 Hadoop 生态系统(如 HDFS、Hive、HBase) 和 关系型数据库(如 MySQL、O…...
MySQL体系架构(二)
MySQL中的目录和文件 2.2.1.bin目录 在MysQL的安装目录下有一个特别特别重要的bin目录,这个目录下存放着许多可执行文件。 其他系统中的可执行文件与此的类似。这些可执行文件都是与服务器程序和客户端程序相关的。 2.2.1.1.启动MySQL服务器程序 在UNIX系统中用来启动MySO…...
为什么反激采用峰值电流控制模式而非电压模式
电压模式控制是传统的控制方法,通过检测输出电压,与参考电压比较,然后调整PWM的占空比。这种方法的优点是简单,只需要一个电压反馈环路。但缺点可能包括对输入电压变化的响应较慢,动态性能不足,尤其是在负载…...
JavaScript逆向工程中的插桩技术完全指南
一、什么是插桩技术? 插桩(Instrumentation)是逆向工程中的核心技术之一,指的是在不改变程序原有逻辑的前提下,向目标程序中插入额外的代码或监控点,用于收集运行时信息、修改程序行为或进行调试分析。 插…...
LLM应用实战1-基本概念
文章目录 基本概念1. 提示词工程(Prompt Engineering)2. AI Agent(智能代理)3. Model Context Protocol (MCP)4. Function Calling(函数调用)5. Retrieval-Augmented Generation (RAG)6. FineTuning&#x…...
数据结构--堆
一、堆的定义 堆是一棵完全二叉树,树中的每个结点的值都不小于(或不大于)其左右孩子结点的值。其中,如果父亲结点的值始终大于或等于孩子结点的值,那么称这样的堆为大顶堆,这时每个结点的值都是以它为根节…...
第37次CCF计算机软件能力认证 / T4 / 集体锻炼
题目 代码 #include <bits/stdc.h> using namespace std; using LL long long;const int N 1e6 10; const int mod 998244353; int a[N]; int st[N][22];int get(int l, int r) {int x r - l 1;int k log2(x);return __gcd(st[l][k], st[r - (1 << k) 1][…...
ES6规范新特性总结
ES6新特性 var、let和const不存在变量提升暂时性死区不允许重复声明 解构赋值用途:交换变量的值从函数返回多个值提取JSON数据遍历map结构输入模块的制定方法 字符串的扩展codePointAt()String.fromCharCode()at()includes(),startsWith(),endsWith()repeat()padSta…...
AI模型多阶段调用进度追踪系统设计文档
AI模型多阶段调用进度追踪系统设计文档 一、系统概述 为解决AI模型处理大型文件时响应时间长的问题,我们设计并实现了一套异步进度追踪系统。该系统采用Server-Sent Events (SSE) 技术,建立从服务器到客户端的单向实时通信通道,使前端能够实…...
[MSPM0开发]最新版ccs20.0安装、配置及导入第一个项目
一、ccs20.0 下载与安装 Code Composer Studio™ 集成式开发环境 (IDE),适用于 TI 微控制器和处理器的集成开发环境 (IDE)。它包含一整套丰富的工具,用于构建、调试、分析和优化嵌入式应用。 ccs下载地址 链接 安装比较简单,在次略过。 二、…...
Win10怎么关闭远程控制?
对于Windows 10用户来说,Win10关闭远程桌面可以有效防止不必要的远程连接,从而保护个人数据和系统安全。那么,Win10怎么关闭远程控制功能呢?接下来,我们将详细介绍Win10关闭远程控制的具体操作步骤。 步骤1.双击桌面上…...
AI重构知识生态:大模型时代的学习、创作与决策革新
📝个人主页🌹:慌ZHANG-CSDN博客 🌹🌹期待您的关注 🌹🌹 一、引言:从知识的获取到知识的共生 过去,我们对“知识”的理解,大多依赖书籍、老师、经验和专业的培训体系。而在大语言模型(Large Language Models, LLM)崛起之后,AI成为了一种新的“知识界面”:…...
牛客 小红杀怪
通过枚举所有使用y技能的次数来枚举出所有方案,选出最合适的 #include<iostream> #include<cmath> #include<algorithm> using namespace std;int a, b, x, y; int ans500;int main() {ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>&…...
Spring入门概念 以及入门案例
Spring入门案例 Springspring是什么spring的狭义与广义spring的两个核心模块IoCAOP Spring framework特点spring入门案例不用new方法,如何使用返回创建的对象 容器:IoC控制反转依赖注入 Spring spring是什么 spring是一款主流的Java EE轻量级开源框架 …...
SpringAI调用硅基流动免费模型
一、引入Spring AI 新建一个Spring Boot的工程,在工程中引入Spring AI的依赖,Spring AI支持Ollma、类OpenAI的接口,这两个引入的pom不一样,这里示例中是使用的硅基流动的模型 <!-- Spring Boot版本要 2.x 或者 3.x以上-->…...
Java 开发中主流安全框架的详细对比,涵盖 认证、授权、加密、安全策略 等核心功能,帮助开发者根据需求选择合适的方案
以下是 Java 开发中主流安全框架的详细对比,涵盖 认证、授权、加密、安全策略 等核心功能,帮助开发者根据需求选择合适的方案: 1. 主流安全框架对比表 框架名称类型核心功能适用场景优点缺点官网/文档Spring Security企业级安全框架认证、授…...
【TVM教程】在支持 CMSIS-NN 的 Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 裸机上运行 TVM
Apache TVM是一个深度的深度学习编译框架,适用于 CPU、GPU 和各种机器学习加速芯片。更多 TVM 中文文档可访问 →https://tvm.hyper.ai/ 作者:Grant Watson 本节使用示例说明如何使用 TVM 在带有 CMSIS-NN 的 Arm Cortex-M55 CPU 和 Ethos™-U55 NPU 的…...
【Ai/Agent】Windows11中安装CrewAI过程中的错误解决记录
CrewAi是什么,可以看之下之前写的 《初识CrewAI多智能代理团队协框架》 (注:这篇是基于linux系统下安装实践的) 基于以下记录解决问题后,可以再回到之前的文章继续进行CrewAI的安装 遇到问题 在windows系统中安装 CrewAi 不管是使用 pip 或者…...
洛谷 P11962:[GESP202503 六级] 树上漫步 ← dfs + 邻接表
【题目来源】 https://www.luogu.com.cn/problem/P11962 【题目描述】 小 A 有一棵 n 个结点的树,这些结点依次以 1,2,⋯,n 标号。 小 A 想在这棵树上漫步。具体来说,小 A 会从树上的某个结点出发,每⼀步可以移动到与当前结点相邻的结点&…...
Linux shell脚本编程
什么是Shell程序设计? 也就是给计算机发命令,让它帮你做事,你通过shell 的小工具,用键盘输入指令,linux就会根据这些指令去执行任务,就像你法号一个指令一样。 shell的强大之处? 文件处理&a…...
嵌入式硬件篇---Uart和Zigbee
文章目录 前言一、UART(通用异步收发传输器)1. 基本概念2. 工作原理帧结构起始位数据位校验位停止位 异步通信波特率 3. 特点优点缺点 4. 典型应用 二、ZigBee1. 基本概念2. 技术细节工作频段2.4GHz868MHz 网络拓扑星型网络网状网络簇状网络 协议栈物理层…...