方法注入

293 阅读13分钟

前言

依赖注入其实只有两种:构造器注入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 一次,导致所有引用都指向同一个原型实例)。

  • ​工作原理:​

    1. 你在 XML 配置中(或使用 @Lookup注解)声明一个 <lookup-method>
    2. 目标 bean 定义一个abstract方法(或在带@Lookup注解的类中用具体方法调用),其返回类型是你想​​每次获取新实例的那个 bean​​(通常是原型作用域)。
    3. ​Spring 在运行时动态生成目标 bean 的 CGLIB 子类​​。这个子类会覆盖你声明的那个abstract(或者带有@Lookup的)方法。
    4. 每次调用该方法时,子类实现会​​向容器请求​​(getBean())该方法的返回类型对应 bean 的一个​​新实例​​(prototype)或​​特定实例​​(requestsession等非单例作用域)。
  • ​关键特征:​

    • 核心在于​​每次调用都获取新对象​​。
    • 被声明为 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 源代码​​的情况下,用​​自定义的逻辑​​替换掉该方法的执行体。

  • ​工作原理:​

    1. 你定义一个实现了 org.springframework.beans.factory.support.MethodReplacer接口的类。这个接口有一个 reimplement方法,它接收被调用方法的原始目标对象、方法对象、参数数组,并需要返回一个结果(类型需匹配或兼容原方法)。
    2. 在 XML 配置中(​​没有直接的注解替代品​​),使用 <replaced-method>元素将目标 bean 中的某个具体方法名与你的 MethodReplacer实现关联起来。
    3. ​Spring 在运行时创建目标 bean 的 CGLIB 代理。​
    4. 当调用被替换的方法时,代理会​​拦截​​该调用,并​​将执行转发给你提供的 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 显得过于复杂或不适用。
    • ​核心问题是改变方法的行为。​

​简单决策树:​

  1. ​你的 Bean 是单例,但每次调用某个方法都需要一个新的“帮手”对象吗?(通常是原型)​​ -> ​​选 lookup-method​。
  2. ​你需要修改一个现有方法内部的实现逻辑,而不改变调用它的源代码吗?​​ -> ​​选 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];
    }

}