前言
依赖注入其实只有两种:构造器注入和setter方法注入,@AutoWire自动注入其实是他们的扩展:no没有自动注入,需要手动指定注入,byName、byType是setter注入,constructor是构造器注入。想过没有为什么没有getter注入呢?其时是有的 Getter注入是指使用 getter 方法进行硬编码的“依赖注入”。之所以使用引号,是因为它是硬编码的,并非真正的依赖注入。“注入”依赖项的方式是通过子类化并重写 getter 方法,或者使用反射(通常使用模拟库)替换 getter 来更改 getter
public class ClassUnderTest{
public void saySomething(){
System.out.println(getString());
}
protected String getString(){
return "Hello World";
}
}
public class GetFrench extends ClassUnderTest{
@Override
protected String getString(){
return "Bonjour le monde";
}
}
确定依赖项是普通对象还是工厂,如果老的getter方法每次都生成一个新对象,并且它需要,那么依赖项应该是工厂。其实子类重写就是"工厂方法",它返回了不一样的对象,只是略显"笨拙"。通过子类剥离父类对String("Bonjour le monde")的依赖。站在IOC容器角度,依赖管理交给spring,那么这个子类化就交给spring CGLIB来做。方法注入就增加了对Ioc容器的依赖
方法注入
首先需要理清两者的核心差异。lookup-method主要用于解决无状态Bean依赖有状态Bean的问题,本质是方法级别的对象获取机制。而replaced-method则是方法实现的动态替换,属于行为修改范畴。
从应用场景来看,lookup-method适合那种“每次需要新对象”的情况。比如用户提到的数据库连接场景很典型——单例服务每次操作都需要新的连接对象。这让我想起Spring官方文档里的经典案例:抽象Command类通过lookup获取具体命令实例。
replaced-method则更适合热替换的场景。用户给出的加密算法切换的例子很贴切,特别是在需要运行时动态改变实现时。不过要提醒自己注意,文档里特别强调这种替换需要通过字节码操作实现,所以必须引入CGLIB库。
用户似乎更关注实际使用选择,所以在总结部分需要强化对比维度。突然想到用户可能遇到的经典困惑:为什么非要用lookup而不用直接注入?这就涉及到原型对象的获取限制问题——在单例Bean内部直接注入原型Bean并不会产生新实例。
最后检查技术细节是否准确:①lookup是继承+模板方法 ②replaced依赖MethodReplacer接口 ③两者都需要CGLIB代理。应该没遗漏关键点。用户的问题质量很高,说明ta正在处理具体的框架设计问题。
在 Spring 框架中,lookup-method和 replaced-method都是用来覆盖或增强容器管理的 bean 中特定方法行为的机制,但它们解决的问题完全不同,适用于不同的场景。理解它们的区别是选择正确工具的关键。
核心区别总结:
| 特性 | lookup-method | replaced-method |
|---|---|---|
| 核心目的 | 获取新实例/原型依赖 | 替换方法实现逻辑 |
| 场景核心需求 | 单例 Bean 每次调用方法需要新的目标实例 | 完全替换现有 Bean 方法的行为逻辑 |
| 实现方式 | Spring CGLIB子类化 + 模板方法 | Spring CGLIB 动态代理 + MethodReplacer |
| 目标方法签名 | 通常声明为abstract | 通常是具体实现的方法 |
| 典型返回值 | 目标 Bean 类型 (原型) | 任意类型 (可匹配原方法) |
| 目标 Bean | 必须是一个原型作用域的 Bean | 可以是任意作用域的 Bean |
| 应用场景 | 单例依赖原型、“方法注入”、避免单例状态问题 | AOP 不足时的热插拔、复杂条件逻辑、模拟、方法增强 |
详细解析及场景选择:
1. lookup-method (查找方法注入)
-
目的: 允许单例作用域的 bean 在每次调用特定方法时都能获取到一个新的、不同实例(通常是原型作用域)。它解决了在单例 bean 中无法正确注入原型 bean 的问题(因为单例初始化时只注入原型 bean 一次,导致所有引用都指向同一个原型实例)。
-
工作原理:
- 你在 XML 配置中(或使用
@Lookup注解)声明一个<lookup-method>。 - 目标 bean 定义一个
abstract方法(或在带@Lookup注解的类中用具体方法调用),其返回类型是你想每次获取新实例的那个 bean(通常是原型作用域)。 - Spring 在运行时动态生成目标 bean 的 CGLIB 子类。这个子类会覆盖你声明的那个
abstract(或者带有@Lookup的)方法。 - 每次调用该方法时,子类实现会向容器请求(
getBean())该方法的返回类型对应 bean 的一个新实例(prototype)或特定实例(request,session等非单例作用域)。
- 你在 XML 配置中(或使用
-
关键特征:
- 核心在于每次调用都获取新对象。
- 被声明为 lookup 的方法通常是
abstract(XML 配置)或者用@Lookup标记(注解配置)。 - 方法的返回类型决定了 Spring 容器要查找和返回哪个 Bean(该 Bean 必须是非单例作用域)。
- Spring 通过生成动态子类来实现(CGLIB)。
-
典型使用场景:
-
单例 Bean 需要依赖原型 Bean: 这是最经典的场景。例如:
- 一个处理请求的单例服务类(
RequestService),每次处理时需要一个新的 DAO 实例(RequestDao- prototype),以避免线程安全问题或在请求间维护状态。 - 一个单例的命令处理器(
CommandManager),每次执行命令时需要创建一个新的、特定类型的命令对象(Command- prototype)。
- 一个处理请求的单例服务类(
-
方法注入 (Method Injection): 当你需要将一个 Bean 的作用域限制在另一个 Bean 的方法调用范围内时。
-
避免在单例中持有原型 Bean 的引用: 确保单例 Bean 不会意外地缓存或重用原型 Bean。
-
xml配置
package fiona.apple;
// no more Spring imports!
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>
<!-- commandManager uses myCommand prototype bean -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
@Lookup注解
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}
2. replaced-method (替换方法实现)
-
目的: 完全替换目标 bean 中一个现有具体方法的实现逻辑。它允许你在不修改原始 bean 源代码的情况下,用自定义的逻辑替换掉该方法的执行体。
-
工作原理:
- 你定义一个实现了
org.springframework.beans.factory.support.MethodReplacer接口的类。这个接口有一个reimplement方法,它接收被调用方法的原始目标对象、方法对象、参数数组,并需要返回一个结果(类型需匹配或兼容原方法)。 - 在 XML 配置中(没有直接的注解替代品),使用
<replaced-method>元素将目标 bean 中的某个具体方法名与你的MethodReplacer实现关联起来。 - Spring 在运行时创建目标 bean 的 CGLIB 代理。
- 当调用被替换的方法时,代理会拦截该调用,并将执行转发给你提供的
MethodReplacer.reimplement()方法,由它来执行你自定义的逻辑。
- 你定义一个实现了
-
关键特征:
- 核心在于替换方法体实现。
- 被替换的方法通常是具体实现的(非 abstract)。
- 完全重写了方法的业务逻辑。
- 使用
MethodReplacer接口提供自定义实现。 - Spring 通过生成动态代理(CGLIB)来实现。
-
典型使用场景:
- 行为热插拔 / 定制化: 在不改源码的情况下,替换掉系统核心组件(如 DAO、Service)的某些方法行为,例如在测试环境替换生产环境的数据获取逻辑,或者在运行时根据配置启用/禁用某些功能分支。
- 实现简单的基于配置的 AOP: 当 AOP(面向切面编程)显得过于重量级,或者你只需要替换一个或几个特定方法,且不需要声明式切点(Pointcut)或通知(Advice)的灵活性时。例如,记录方法调用参数、临时添加性能监控、简单的缓存逻辑(需谨慎)。
- 修复或增强第三方库的方法: 如果你不能修改第三方 jar 包中某个类的源代码,但需要改变其某个特定方法的行为。
- 复杂的条件逻辑: 当需要根据复杂的外部状态(配置、运行时环境)动态选择方法实现时(虽然策略模式可能更清晰,但
replaced-method可以做到)。 - 测试/模拟: 在集成测试中,用
replaced-method快速模拟依赖方法的复杂实现或外部系统调用。
public class MyValueCalculator {
public String computeValue(String input) {
// some real code...
}
// some other methods...
}
/**
* meant to be used to override the existing computeValue(String)
* implementation in MyValueCalculator
*/
public class ReplacementComputeValue implements MethodReplacer {
public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
// get the input value, work with it, and return a computed result
String input = (String) args[0];
...
return ...;
}
}
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
<!-- arbitrary method replacement -->
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>String</arg-type>
</replaced-method>
</bean>
<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>
如何选择合适的机制?
-
选择
lookup-method当你:- 有一个单例 Bean。
- 这个单例 Bean 需要调用一个方法。
- 每次调用这个方法都需要获得一个全新的对象实例(这个实例通常是另一个原型作用域的 Bean)。
- 核心问题是获取新对象。
-
选择
replaced-method当你:- 需要改变(替换) 一个 bean 中某个具体方法的执行代码。
- 不能或不想修改原始 bean 的源代码。
- 需要一种基于配置的、声明式的方式来替换方法逻辑。
- 标准的 AOP 显得过于复杂或不适用。
- 核心问题是改变方法的行为。
简单决策树:
- 你的 Bean 是单例,但每次调用某个方法都需要一个新的“帮手”对象吗?(通常是原型) -> 选
lookup-method。 - 你需要修改一个现有方法内部的实现逻辑,而不改变调用它的源代码吗? -> 选
replaced-method。
重要提示:
-
现代 Spring (尤其是纯注解配置) 更倾向于
@Lookup: 对于 lookup 场景,@Lookup注解(在方法上)通常是比 XML<lookup-method>更现代、更便捷的选择。 -
replaced-method没有直接注解替代: Spring 的核心注解配置中没有直接替代 XML<replaced-method>的标准注解。替代方案通常是:- 策略模式 (依赖注入): 将可能变化的行为抽象为接口,将不同的实现注入进来,在方法内部调用接口方法。
- AOP: 使用
@Around环绕通知完全接管目标方法的执行(可以看作最灵活强大的replaced-method)。
-
两者都基于代理: 无论是
lookup-method还是replaced-method,Spring 底层都需要使用 CGLIB 库(或其他代理技术)来动态生成代理子类或代理对象。这可能会带来一些限制(如类需非 final)和微小的性能开销。
总结:
虽然两者都通过 Spring 容器改变方法行为且涉及动态代理,但 lookup-method的核心是解决对象获取问题(单例获取原型新实例),而 replaced-method的核心是解决代码实现替换问题。理解你面临的问题本质是选择合适机制的关键。在现代 Spring 开发中,@Lookup比 <lookup-method>更常用,而对于方法实现的动态替换,则更倾向于使用策略模式或完整的 AOP。另一个关键限制是查找方法不适用于工厂方法,特别是不适用于@Bean配置类中的方法,因为在这种情况下,容器不负责创建实例,因此无法动态创建运行时生成的子类
工厂方法组合重写方法
@Lookup、replace-method方法只能生效在那些由容器自身通过常规构造函数(new ClassName())实例化的 Bean 上,因为这样才可以通过CGLIB子类化。如果工厂方法,容器不会介入bean的实例化,就不可能生成代理bean,无法实现方法覆盖了。
Lookup原理
注解配置的话,bean定义构建之后,MethodOverrides是空的(如果有replace-method,则不会为空,它没有注解,在bean定义构建时就会从xml读取),直到进行bean实例的创建时,首先进行构造器的筛选。因为lookup就是覆盖原始bean定义中依赖bean的获取方式的,所有应该在构造器选择之前就进行lookup注解检查,这样合并bean定义覆盖到原始bean定义
createBeanInstance
|从BPP中查找自动装配候选构造器
\|/
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName)
|注解自然走的是AutowiredAnnotationBeanPostProcessor
\|/
Constructor<?>[] ctors = bp.determineCandidateConstructors(beanClass, beanName);
|首先检查lookup注解
\|/
checkLookupMethods{
AnnotationUtils.isCandidateClass(beanClass, Lookup.class);
... 获取到@lookup注解值,以及它注解的方法,放入方法重写列表,合并bean定义
LookupOverride override = new LookupOverride(method, lookup.value());
try {
RootBeanDefinition mbd = (RootBeanDefinition)
this.beanFactory.getMergedBeanDefinition(beanName);
mbd.getMethodOverrides().addOverride(override);
}
}
| 选出匹配的构造器,开始实例化bean,没有候选构造器默认无参构造器,如下
\|/
instantiateBean(beanName, mbd)-->instantiate
| 如果有重写方法,用方法注入实例化bean(通过CGLIB子类化目标bean,并重写)
\|/
instantiateWithMethodInjection(bd, beanName, owner)
| cglib子类化目标bean
\|/
new CglibSubclassCreator(bd, owner).instantiate(ctor, args)
|使用CGLIB为提供的bean定义创建一个增强的bean类子类。
\|/ instantiate{
Class<?> subclass = createEnhancedSubclass(this.beanDefinition);
Object instance;
if (ctor == null) {
instance = BeanUtils.instantiateClass(subclass);
}
else {
try {
Constructor<?> enhancedSubclassConstructor = subclass.getConstructor(ctor.getParameterTypes());
instance = enhancedSubclassConstructor.newInstance(args);
}
catch (Exception ex) {
throw new BeanInstantiationException(this.beanDefinition.getBeanClass(),
"Failed to invoke constructor for CGLIB enhanced subclass [" + subclass.getName() + "]", ex);
}
}
// SPR-10785: set callbacks directly on the instance instead of in the
// enhanced class (via the Enhancer) in order to avoid memory leaks.
//通过 `Factory.setCallbacks()`在实例级别设置拦截器(而非类级别),
//防止CGLIB在类加载器中缓存引用,避免内存漏:修复前
//enhancer.setCallbacks(callbacks); 静态字段在类级别设置
Factory factory = (Factory) instance;
//设置1为lookup回调,2为replace回调
factory.setCallbacks(new Callback[] {NoOp.INSTANCE,
new LookupOverrideMethodInterceptor(this.beanDefinition, this.owner),
new ReplaceOverrideMethodInterceptor(this.beanDefinition, this.owner)});
return instance;
}
使用BeanFactory时,默认对象实例化策略CglibSubclassingInstantiationStrategy
有内部类LookupOverrideMethodInterceptor作为lookup方法拦截器,也有repacle方法拦截器ReplaceOverrideMethodInterceptor
private static class LookupOverrideMethodInterceptor extends CglibIdentitySupport implements MethodInterceptor {
private final BeanFactory owner;
public LookupOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanFactory owner) {
super(beanDefinition);
this.owner = owner;
}
@Override
@Nullable
public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable {
// Cast is safe, as CallbackFilter filters are used selectively.
LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
Assert.state(lo != null, "LookupOverride not found");
Object[] argsToUse = (args.length > 0 ? args : null); // if no-arg, don't insist on args at all
if (StringUtils.hasText(lo.getBeanName())) {
//调用了getBean,原型bean每次都返回新的bean
Object bean = (argsToUse != null ? this.owner.getBean(lo.getBeanName(), argsToUse) :
this.owner.getBean(lo.getBeanName()));
// Detect package-protected NullBean instance through equals(null) check
return (bean.equals(null) ? null : bean);
}
else {
// Find target bean matching the (potentially generic) method return type
ResolvableType genericReturnType = ResolvableType.forMethodReturnType(method);
return (argsToUse != null ? this.owner.getBeanProvider(genericReturnType).getObject(argsToUse) :
this.owner.getBeanProvider(genericReturnType).getObject());
}
}
}
private static class ReplaceOverrideMethodInterceptor extends CglibIdentitySupport implements MethodInterceptor {
private final BeanFactory owner;
public ReplaceOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanFactory owner) {
super(beanDefinition);
this.owner = owner;
}
@Override
@Nullable
public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable {
ReplaceOverride ro = (ReplaceOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
Assert.state(ro != null, "ReplaceOverride not found");
//获取replacer对应的bean,通过它实现的reimplement结果替换了原方法返回类型
MethodReplacer mr = this.owner.getBean(ro.getMethodReplacerBeanName(), MethodReplacer.class);
return processReturnType(method, mr.reimplement(obj, method, args));
}
@Nullable
private <T> T processReturnType(Method method, @Nullable T returnValue) {
Class<?> returnType = method.getReturnType();
if (returnValue == null && returnType != void.class && returnType.isPrimitive()) {
throw new IllegalStateException(
"Null return value from MethodReplacer does not match primitive return type for: " + method);
}
return returnValue;
}
}
生成的代理类
下面是通过@lookup进行方法注入,替换获取car bean的逻辑
@Component
public class User {
private Car car = new Car("保时捷");
@Lookup("car")
public Car getCar() {
//这里完全可以输出null,这里返回不为null完全为了测试
return car;
}
}
生成代理,通过CGLIB子类化User类,gatCar方法会被var10000.intercept拦截,这里的var10001就是factory.setCallbacks设置的回调索引1位置,即LookupOverrideMethodInterceptor
public class User$$SpringCGLIB$$0 extends User implements Factory {
public final Car getCar() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_1;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_1;
}
return var10000 != null ? (Car)var10000.intercept(this, CGLIB$getCar$0$Method, CGLIB$emptyArgs, CGLIB$getCar$0$Proxy) : super.getCar();
}
}
private static final void CGLIB$BIND_CALLBACKS(Object var0) {
User$$SpringCGLIB$$0 var1 = (User$$SpringCGLIB$$0)var0;
if (!var1.CGLIB$BOUND) {
var1.CGLIB$BOUND = true;
Object var10000 = CGLIB$THREAD_CALLBACKS.get();
if (var10000 == null) {
var10000 = CGLIB$STATIC_CALLBACKS;
if (var10000 == null) {
return;
}
}
Callback[] var10001 = (Callback[])var10000;
var1.CGLIB$CALLBACK_2 = (MethodInterceptor)((Callback[])var10000)[2];
//这里就是设置回调的索引1位置
var1.CGLIB$CALLBACK_1 = (MethodInterceptor)var10001[1];
var1.CGLIB$CALLBACK_0 = (NoOp)var10001[0];
}
}