Spring注入之单例bean注入原型bean



前言

我们知道在Spring当中bean的Scope类型包含有单例(singleton)和多例(prototype),后者又叫原型。当然,还有request类型、session类型等。其中单例类型是单个Spring容器只存在一个,而原型类型每次从容器中获取都会生成一个新的,因此从容器生命周期来看,单例类型的bean可以认为是longer-lived scoped bean(长生命周期bean),而原型类型的bean可以认为是shorter-lived scoped bean(短生命周期bean)。由此可以引出一个问题:在单例bean当中注入原型bean之后,这个原型bean还是原型bean吗?很多人可能会问,这个问题有意义吗?平时根本碰不到。其实很多人对于单例bean和原型bean适用什么样的场景是分不清的,好像单例bean与原型bean的区别只不过一个是存储在单例缓存中,所以每次获取都是一样的,而原型bean不存在啥缓存,所以每次从容器获取都会重新创建一个。但这是从结果来分析的,还没说清楚单例bean和原型bean的适用场景。在官方文档中有如下这句话:

As a rule, you should use the prototype scope for all stateful beans and the singleton scope for stateless beans.

这句话可以说很好的总结了单例bean和原型bean的适用场景,前者适用有状态的,后者适用无状态的。啥是有状态的?啥是无状态的?比如常见的服务类,除了依赖的bean和方法之外没有其他记录状态的属性,假如在一个服务类中存在一个boolean类型的属性,还将其作为单例bean,这样就变为有状态的了,因为这个boolean类型属性随时可能被另一个使用者改变,此时为了隔离,就必须考虑每个使用者使用一个了,也就是要使用原型bean了。我们平时之所以很少遇到单例bean注入多例bean的问题的主要原因就是我们平时设计的控制层、业务层和数据层尽量屏蔽了有状态。所以上面的问题其实要回答的是,在单例bean当中注入一个原型bean之后,这个原型bean之中的状态是共享的吗?


一、结果分析

首先定义一个原型bean,在这个原型bean当中,有一个stateCount属性,每次调用print都会将这个属性加一并输出。对于原型bean,每次都获取一个新的,这个方法只会输出1.

package org.spring.boot.example.entity;

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

import java.util.concurrent.atomic.AtomicInteger;

@Scope(scopeName = "prototype")
@Component
public class PrototypeInstance {

    AtomicInteger stateCount = new AtomicInteger(0);

    public void print() {
        System.out.println(stateCount.incrementAndGet());
    }

}

再定义一个单例bean,并注入上面的原型bean,在这个单例的printCode方法中调用原型bean的方法。

package org.spring.boot.example.entity;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class SigletonInstance {

    @Autowired
    private PrototypeInstance prototypeInstance;

    public void setPrototypeInstance(PrototypeInstance prototypeInstance) {
        this.prototypeInstance = prototypeInstance;
    }

    public PrototypeInstance getPrototypeInstance() {
        return prototypeInstance;
    }

    public void printCode() {
        prototypeInstance.print();
    }
}

从容器中获取这个单例bean,并多次调用其中的printCode。
在这里插入图片描述
从结果来看,这个状态变量其实是共享的,也就是说这个单例bean里面的这个原型bean其实只有一个。仔细想一下,在这个单例bean在容器中只会创建一次,也就是说注入原型bean也只有一次机会,所以单例bean当中的prototypeInstance属性只可能是一个。所以最后注入的原型bean其实变成了一个单例bean了。那么这是我们想要的结果吗?当然不是,我们期待的结果是,每次调用prototypeInstance这个属性的方法,应该不会共享状态的。那么,可以办到吗?
在这里插入图片描述
像上面这样,我们在这个原型bean上面的Scope注解中添加属性值proxyMode = ScopedProxyMode.TARGET_CLASS,然后再次调用。
在这里插入图片描述
从结果来看,此时这个原型bean成了真正的原型bean,不再共享变量了。达到了我们的目标,那么,这里面的玄机是什么呢?

二、原理分析

按照道理来说,SigletonInstance单例bean中注入PrototypeInstance原型bean只有一次机会,这个prototypeInstance属性只可能是一个对象,而不可能每次都不一样,所以这个对象肯定不能是一个真实的PrototypeInstance对象,而是一个代理对象,这样才能在每次调用其方法的时候有机会被改变。那么Spring中是怎么做到的呢?首先Spring容器中为这个原型bean保存了两个bean实例。一个名称为prototypeInstance,另一个名称为scopedTarget.prototypeInstance,在前面的名称前面加了scopedTarget..如下图所示
在这里插入图片描述
仔细看下这两个bean的实际类型,前者为org.springframework.aop.scope.ScopedProxyFactoryBean,后者为org.spring.boot.example.entity.PrototypeInstance,是不是很奇怪,其实Spring就是将真实的原型bean改了名字,而原来的名字对应了一个FactoryBean。而获取这个FactoryBean的真实结果是通过getObject方法来获取的,如下所示

@Override
public Object getObject() {
	if (this.proxy == null) {
		throw new FactoryBeanNotInitializedException();
	}
	return this.proxy;
}

在这里插入图片描述

那么这个proxy属性是啥呢?其实这个proxy就是通过CGLIB生成的一个代理对象,在生成这个代理对象的时候会设置targetSource为SimpleBeanTargetSource类型的对象。上面的proxy和这个targetSource都是上面FactoryBean实例的属性,在调用这个代理的时候会从targetSource中获取到真实对象,然后调用真实对象的属性,Spring只要在这个时候从beanFactory中获取原型bean(此时的bean名称为scopedTarget.prototypeInstance),那么每次方法调用都是不一样的。而Spring正是这么实现的。
在这里插入图片描述
这个SimpleBeanTargetSource中属性如上所示,而getTarget方法源码如下

public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {

	@Override
	public Object getTarget() throws Exception {
		return getBeanFactory().getBean(getTargetBeanName());
	}

}

在每次调用代理的方法时候,会进入到org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept方法中,如下所示
在这里插入图片描述
不难看出,这里就是通过注入一个代理对象,在每次调用这个代理对象的时候,获取一个原型Bean,这样就相当于注入了一个原型bean。

三、流程分析

1、注册阶段

在Spring扫描注册bean的时候会进入到方法ClassPathBeanDefinitionScanner#doScan当中,这个方法当中有如下代码

definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);

而这个applyScopedProxyMode方法就会根据类上面的ScopedProxyMode执行不同的策略,默认值为ScopedProxyMode.NO

static BeanDefinitionHolder applyScopedProxyMode(
		ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) {

	ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
	if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
		return definition;
	}
	boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
	return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);
}

从上面源码可以看出,只有当ScopedProxyModeTARGET_CLASS才会进入到下面创建ScopedProxy的逻辑,这也是为啥我们在第一节中在原型bean的scope注解中添加proxyMode = ScopedProxyMode.TARGET_CLASS的原因,只有修改了proxyMode才会进入下面的逻辑。

public static BeanDefinitionHolder createScopedProxy(
		BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry, boolean proxyTargetClass) {

	return ScopedProxyUtils.createScopedProxy(definitionHolder, registry, proxyTargetClass);
}

而createScopedProxy的逻辑其实就是将原来要注册的原型bean改个名字注册,另外注册一个ScopedProxyFactoryBean类型的bean并使用原来原型bean的名称。对应的源码如下

/**
 * Generate a scoped proxy for the supplied target bean, registering the target
 * bean with an internal name and setting 'targetBeanName' on the scoped proxy.
 * @param definition the original bean definition
 * @param registry the bean definition registry
 * @param proxyTargetClass whether to create a target class proxy
 * @return the scoped proxy definition
 * @see #getTargetBeanName(String)
 * @see #getOriginalBeanName(String)
 */
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
		BeanDefinitionRegistry registry, boolean proxyTargetClass) {

	String originalBeanName = definition.getBeanName();
	BeanDefinition targetDefinition = definition.getBeanDefinition();
	String targetBeanName = getTargetBeanName(originalBeanName);

	// Create a scoped proxy definition for the original bean name,
	// "hiding" the target bean in an internal target definition.
	RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
	proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
	proxyDefinition.setOriginatingBeanDefinition(targetDefinition);
	proxyDefinition.setSource(definition.getSource());
	proxyDefinition.setRole(targetDefinition.getRole());

	proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName);
	if (proxyTargetClass) {
		targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
		// ScopedProxyFactoryBean's "proxyTargetClass" default is TRUE, so we don't need to set it explicitly here.
	}
	else {
		proxyDefinition.getPropertyValues().add("proxyTargetClass", Boolean.FALSE);
	}

	// Copy autowire settings from original bean definition.
	proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
	proxyDefinition.setPrimary(targetDefinition.isPrimary());
	if (targetDefinition instanceof AbstractBeanDefinition) {
		proxyDefinition.copyQualifiersFrom((AbstractBeanDefinition) targetDefinition);
	}

	// The target bean should be ignored in favor of the scoped proxy.
	targetDefinition.setAutowireCandidate(false);
	targetDefinition.setPrimary(false);

	// Register the target bean as separate bean in the factory.
	registry.registerBeanDefinition(targetBeanName, targetDefinition);

	// Return the scoped proxy definition as primary bean definition
	// (potentially an inner bean).
	return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}

在这里将targetDefinition的AutowireCandidate和Primary属性设置为false,这样在自动注入的时候就不会考虑原型bean了,而是对应的FactoryBean.

2、实例化阶段

在容器启动过程中,会实例化单例bean,这个时候需要注入这个原型bean,其实会触发ScopedProxyFactoryBean类型bean的初始化。我们看一下这个bean的定义。
在这里插入图片描述
其中ProxyConfig用于为生成代理提供配置,AopInfrastructureBean是一个标识接口,标识这个是Spring的AOP的一个基础结构,因此这个类不会被自动代理,即使被匹配上,也就是说这个类在Spring AOP框架中是不会再生成其他的代理的。另外实现了FactoryBean接口,这样这个bean的真实对象是通过getObject方法返回的,另外这个类实现了BeanFactoryAware接口,这样在这个bean初始化的时候会调用回调方法setBeanFactory,而这个回调方法也是生成代理的核心,如下所示

@Override
public void setBeanFactory(BeanFactory beanFactory) {
	if (!(beanFactory instanceof ConfigurableBeanFactory)) {
		throw new IllegalStateException("Not running in a ConfigurableBeanFactory: " + beanFactory);
	}
	ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) beanFactory;

	this.scopedTargetSource.setBeanFactory(beanFactory);

	ProxyFactory pf = new ProxyFactory();
	pf.copyFrom(this);
	pf.setTargetSource(this.scopedTargetSource);

	Assert.notNull(this.targetBeanName, "Property 'targetBeanName' is required");
	Class<?> beanType = beanFactory.getType(this.targetBeanName);
	if (beanType == null) {
		throw new IllegalStateException("Cannot create scoped proxy for bean '" + this.targetBeanName +
				"': Target type could not be determined at the time of proxy creation.");
	}
	if (!isProxyTargetClass() || beanType.isInterface() || Modifier.isPrivate(beanType.getModifiers())) {
		pf.setInterfaces(ClassUtils.getAllInterfacesForClass(beanType, cbf.getBeanClassLoader()));
	}

	// Add an introduction that implements only the methods on ScopedObject.
	ScopedObject scopedObject = new DefaultScopedObject(cbf, this.scopedTargetSource.getTargetBeanName());
	pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject));

	// Add the AopInfrastructureBean marker to indicate that the scoped proxy
	// itself is not subject to auto-proxying! Only its target bean is.
	pf.addInterface(AopInfrastructureBean.class);

	this.proxy = pf.getProxy(cbf.getBeanClassLoader());
}

这里不但生成了代理,而且设置了SimpleBeanTargetSource,这样后面代理才能获取到真实的目标对象。最后实例化的对象如下所示
在这里插入图片描述
注册到单例bean中的其实就是对象中的proxy属性对象。

3、调用阶段

调用单例bean中多例bean的方法的时候,会调用proxy代理对象的方法,这是一个CGLIB代理对象,所以会首先进入org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept方法中,如下所示

@Override
@Nullable
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
	Object oldProxy = null;
	boolean setProxyContext = false;
	Object target = null;
	TargetSource targetSource = this.advised.getTargetSource();
	try {
		if (this.advised.exposeProxy) {
			// Make invocation available if necessary.
			oldProxy = AopContext.setCurrentProxy(proxy);
			setProxyContext = true;
		}
		// Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
		target = targetSource.getTarget();
		Class<?> targetClass = (target != null ? target.getClass() : null);
		List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
		Object retVal;
		// Check whether we only have one InvokerInterceptor: that is,
		// no real advice, but just reflective invocation of the target.
		if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
			// We can skip creating a MethodInvocation: just invoke the target directly.
			// Note that the final invoker must be an InvokerInterceptor, so we know
			// it does nothing but a reflective operation on the target, and no hot
			// swapping or fancy proxying.
			Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
			retVal = methodProxy.invoke(target, argsToUse);
		}
		else {
			// We need to create a method invocation...
			retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
		}
		retVal = processReturnType(proxy, target, method, retVal);
		return retVal;
	}
	finally {
		if (target != null && !targetSource.isStatic()) {
			targetSource.releaseTarget(target);
		}
		if (setProxyContext) {
			// Restore old proxy.
			AopContext.setCurrentProxy(oldProxy);
		}
	}
}

其中org.springframework.aop.TargetSource#getTarget就是获取原型bean的关键了。


总结

如果实在xml中配置的话

<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="prototypeInstance" class="org.spring.boot.example.entity.PrototypeInstance" scope="prototype">
    <aop:scoped-proxy proxy-target-class="true"/>
</bean>
<bean id="sigletonInstance" class="org.spring.boot.example.entity.SigletonInstance">
    <property name="prototypeInstance" ref="prototypeInstance"/>
</bean>

除了以上的方式解决单例bean注册原型bean的问题之外,还有另一种方式-方法注入,但是本质其实都是差不多的,都是通过CGLIB动态代理的方式,可以参考博客:Spring注入之方法注入

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

lang20150928

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

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

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

打赏作者

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

抵扣说明:

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

余额充值