Spring注入之方法注入



前言

如果提到Spring的话,IOC和DI肯定是最重要的话题。那么Spring当中的方法注入你了解吗?在绝大多次的场景当中,bean都是单例的,所以当一个bean依赖于另一个bean的时候,我们只要简单的定义一个bean为另一个bean的属性即可。但是当bean的scope不相同的时候问题就出现了。比如在一个单例bean中注入一个原型bean,因为容器只会创建这个单例bean一次,也就是只有一次机会设置这个原型bean作为属性,所以容器没法在每次使用这个单例的时候内部是一个不同的原型bean。(injecting a shorter-lived scoped bean into a longer-lived scoped bean )


一、打破IOC解决单例bean注入原型bean

一个解决方案就是打破IOC,让这个单例Bean实现ApplicationContextAware接口,然后每次执行对应方法的时候通过容器去获取原型bean。如下所示

// Spring-API imports
import org.spring.boot.example.entity.Command;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class CommandManager implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    public Object process(Map commandState) {
        // grab a new instance of the appropriate Command
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    protected Command createCommand() {
        // notice the Spring API dependency!
        return this.applicationContext.getBean("command", Command.class);
    }

    public void setApplicationContext(
            ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

为啥说这打破了IOC,因为这样其实把获取依赖这个过程交给了CommandManager自己去解决(通过容器去查找依赖),这个动作是由CommandManager发起的。因此说上面的这种解决方法是不合理的,因为业务代码与Spring框架代码耦合了,某种程度上,Spring的IOC可以更优雅的解决这个问题。那就是通过方法注入的方式。

二、Lookup方法注入

查找方法注入是容器重写容器管理的Bean上的方法并返回容器中另一个命名Bean的查找结果的能力。 查找通常涉及原型bean,如上一节中所述。 Spring框架通过使用CGLIB库中的字节码生成来动态生成覆盖该方法的子类,从而实现此方法注入。

对于前面的代码片段中的CommandManager类,Spring容器动态地覆盖createCommand方法的实现。 如下所示,CommandManager类没有任何Spring依赖项:

package org.spring.boot.example.config;

import org.spring.boot.example.entity.Command;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.stereotype.Component;

@Component
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?
    @Lookup
    protected abstract Command createCommand();
}

在这里通过注解@Lookup实现了一个查找方法注入。通过加入这个注解,在解析生成BeanDefinition会设置methodOverrides属性。
在这里插入图片描述
在实例化bean对象的时候由于methodOverrides属性有值会进入通过CGLIB生成代理的逻辑,如下所示
在这里插入图片描述
最终会走到如下的代码

@Override
protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner,
		@Nullable Constructor<?> ctor, Object... args) {

	// Must generate CGLIB subclass...
	return new CglibSubclassCreator(bd, owner).instantiate(ctor, args);
}

这里涉及到CglibSubclassCreator这个类,以及instantiate方法。注意这里将当前容器的beanFactory作为owner传入到CglibSubclassCreator对象当中。
在这里插入图片描述
然后就是创建代理对象的过程

/**
 * Create a new instance of a dynamically generated subclass implementing the
 * required lookups.
 * @param ctor constructor to use. If this is {@code null}, use the
 * no-arg constructor (no parameterization, or Setter Injection)
 * @param args arguments to use for the constructor.
 * Ignored if the {@code ctor} parameter is {@code null}.
 * @return new instance of the dynamically generated subclass
 */
public Object instantiate(@Nullable Constructor<?> ctor, Object... args) {
	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 factory = (Factory) instance;
	factory.setCallbacks(new Callback[] {NoOp.INSTANCE,
			new LookupOverrideMethodInterceptor(this.beanDefinition, this.owner),
			new ReplaceOverrideMethodInterceptor(this.beanDefinition, this.owner)});
	return instance;
}

/**
 * Create an enhanced subclass of the bean class for the provided bean
 * definition, using CGLIB.
 */
private Class<?> createEnhancedSubclass(RootBeanDefinition beanDefinition) {
	Enhancer enhancer = new Enhancer();
	enhancer.setSuperclass(beanDefinition.getBeanClass());
	enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
	if (this.owner instanceof ConfigurableBeanFactory) {
		ClassLoader cl = ((ConfigurableBeanFactory) this.owner).getBeanClassLoader();
		enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(cl));
	}
	enhancer.setCallbackFilter(new MethodOverrideCallbackFilter(beanDefinition));
	enhancer.setCallbackTypes(CALLBACK_TYPES);
	return enhancer.createClass();
}

也就是说Spring实例化的CommandManager的实例是一个CGLIB代理对象,如下所示
在这里插入图片描述
所谓的代理其实创建一个子类并针对对应的方法进行增强,针对哪些方法进行增强呢?在这里是通过MethodOverrideCallbackFilter来指定的,其实就是一个过滤器,这里过滤掉那些不包含在methodOverride信息的方法。对于当前案例来说,就是过滤掉不包含@Lookup注解方法,只会对createCommand进行增强。MethodOverrideCallbackFilter的源码如下所示

/**
 * CGLIB callback for filtering method interception behavior.
 */
private static class MethodOverrideCallbackFilter extends CglibIdentitySupport implements CallbackFilter {

	private static final Log logger = LogFactory.getLog(MethodOverrideCallbackFilter.class);

	public MethodOverrideCallbackFilter(RootBeanDefinition beanDefinition) {
		super(beanDefinition);
	}

	@Override
	public int accept(Method method) {
		MethodOverride methodOverride = getBeanDefinition().getMethodOverrides().getOverride(method);
		if (logger.isTraceEnabled()) {
			logger.trace("MethodOverride for " + method + ": " + methodOverride);
		}
		if (methodOverride == null) {
			return PASSTHROUGH;
		}
		else if (methodOverride instanceof LookupOverride) {
			return LOOKUP_OVERRIDE;
		}
		else if (methodOverride instanceof ReplaceOverride) {
			return METHOD_REPLACER;
		}
		throw new UnsupportedOperationException("Unexpected MethodOverride subclass: " +
				methodOverride.getClass().getName());
	}
}

如果对应的方法通过了上面的过滤器,那么就会通过设置的回调拦截器进行增强,也就是执行对应的方法的时候会进入到拦截器MethodInterceptorintercept方法。在这里,最重要的就是LookupOverrideMethodInterceptorReplaceOverrideMethodInterceptor,当然在当前案例当中,主要是靠前者。
在这里插入图片描述
最后创建的bean如下所示为一个CGLIB代理对象。
在这里插入图片描述
所以在执行CommandManager类型bean的方法的时候,如果调用createCommand方法的话,就会进入到CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor#intercept方法了。
在这里插入图片描述
执行的逻辑就是通过bean工厂查找方法返回类型对应的bean,这里createCommand方法的返回类型为Command
在这里插入图片描述
而当前容器当中Command类定义如下,这是一个原型Bean。每次通过beanFactory查找都会返回不同的实例。

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Scope("prototype")
@Component
public class Command {

    private Object state;

    public void setState(Object state) {
        this.state = state;
    }

    public Object getState() {
        return state;
    }

    public Object execute() {
        System.out.println(this.hashCode() + " - execute");
        return this.hashCode();
    }
}

我们在execute方法当中会输出这个实例的hashCode。在获取CommandManager类型的bean(这是一个单例)之后调用process方法两次,此时控制台打印如下
在这里插入图片描述
可见通过查找方法注入我们实现了在单例bean中注入原型bean的目标。其实也就是通过动态代理的方式。

三、xml配置

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="org.spring.boot.example.entity.Command" scope="prototype">
    <!-- inject dependencies here as required -->
</bean>
<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="org.spring.boot.example.config.CommandManager">
    <lookup-method name="createCommand" bean="myCommand"/>
</bean>

四、Java configuration

@Bean
@Scope("prototype")
public Command asyncCommand() {
    Command command = new Command();
    // inject dependencies here as required
    return command;
}

@Bean
public CommandManager commandManager() {
    // return new anonymous implementation of CommandManager with createCommand()
    // overridden to return a new prototype Command object
    return new CommandManager() {
        @Override
        protected Command createCommand() {
            return asyncCommand();
        }
    };
}

以上这种配置方式已经不属于方法注入的范畴了


总结

与查找方法注入相比,方法注入的一种不太有用的形式是能够用另一种方法实现替换托管bean中的任意方法。 也就是上面ReplaceOverrideMethodInterceptor对应的逻辑了,大致的流程与查找方法注入差不多,只是使用方法不同而已,如果有需要可以查找相关资料,这里不继续阐述了,以下只提供一个案例。

package org.spring.boot.example.entity;

public class MyValueCalculator {
    public String computeValue(String input) {
        // some real code...
        return "default value";
    }
    // some other methods...
}
package org.spring.boot.example.entity;

import org.springframework.beans.factory.support.MethodReplacer;

import java.lang.reflect.Method;

/**
 * meant to be used to override the existing computeValue(String)
 * implementation in MyValueCalculator
 */
public class ReplacementComputeValue implements MethodReplacer {
    @Override
    public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
        // get the input value, work with it, and return a computed result
        return "reimplement in ReplacementComputeValue";
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="myValueCalculator" class="org.spring.boot.example.entity.MyValueCalculator">
        <!-- arbitrary method replacement -->
        <replaced-method name="computeValue" replacer="replacementComputeValue"/>
    </bean>
    <bean id="replacementComputeValue" class="org.spring.boot.example.entity.ReplacementComputeValue"/>

</beans>

在这里插入图片描述

This is not the behavior you want when injecting a shorter-lived scoped bean into a longer-lived scoped bean (for example, injecting an HTTP Session-scoped collaborating bean as a dependency into singleton bean). Rather, you need a single userManager object, and, for the lifetime of an HTTP
Session, you need a userPreferences object that is specific to the HTTP Session. Thus, the container creates an object that exposes the exact same public interface as the UserPreferences class (ideally an object that is a UserPreferences instance), which can fetch the real UserPreferences object from the scoping mechanism (HTTP request, Session, and so forth). The container injects this proxy object into the userManager bean, which is unaware that this UserPreferences reference is a proxy. In this example, when a UserManager instance invokes a method on the dependency-injected UserPreferences object, it is actually invoking a method on the proxy. The proxy then fetches the real UserPreferences object from (in this case) the HTTP Session and delegates the method invocation onto the retrieved real UserPreferences object.Thus, you need the following (correct and complete) configuration when injecting request- and session-scoped beans into collaborating objects, as the following example shows:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
	<aop:scoped-proxy/>
</bean>
<bean id="userManager" class="com.something.UserManager">
	<property name="userPreferences" ref="userPreferences"/>
</bean>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值