Spring Cloud Config 解决单例 Bean 依赖非单例 Bean

Posted carl-zhao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud Config 解决单例 Bean 依赖非单例 Bean相关的知识,希望对你有一定的参考价值。

在大多数应用程序场景中,容器中的大多数 bean 都是单例的。当一个单例 bean 需要与另一个单例 bean 协作,或者一个非单例 bean 需要与另一个非单例 bean 协作时,您通常通过将一个bean 定义为另一个 bean 的属性来处理依赖关系。当 bean 的生命周期不同时,问题就出现了。假设单例 bean A 需要使用非单例(原型) bean B,可能是在 A 上的每个方法调用上。容器只创建一次单例 bean A,因此只有一次机会设置属性。容器不能在每次需要 bean B 的新实例时为 bean A 提供一个。

一个解决方案是放弃一些控制反转。您可以通过实现 ApplicationContextAware 接口,并在 bean A 每次需要 bean B 实例时向容器发出 getBean(“B”)调用来请求(通常是新的) bean B 实例,从而使 bean A 感知到容器。下面是这种方法的一个例子:

// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;

// Spring-API imports
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;
    

前面的方法是不可取的,因为业务代码知道 Spring 框架并与之耦合。方法注入是 Spring IoC 容器的一种高级特性,它允许以一种干净的方式处理这个用例。

1、Lookup method 注入

查找方法注入是容器覆盖容器托管bean上的方法的能力,以返回容器中另一个命名 bean 的查找结果。查找通常涉及到上一节描述的场景中的原型 bean。Spring 框架通过使用来自 CGLIB 库的字节码生成来动态生成覆盖该方法的子类,从而实现了这种方法注入。

要使这个动态子类工作,Spring bean容器将继承的类不能是 final 类,要重写的方法也不能是final类。

对具有抽象方法的类进行单元测试需要您自己创建该类的子类,并提供抽象方法的存根实现。

组件扫描也需要具体的方法,因为组件扫描需要具体的类。

进一步的关键限制是,查找方法不能与工厂方法一起工作,特别是不能与配置类中的 @Bean 方法一起工作,因为在这种情况下容器不负责创建实例,因此不能动态地创建运行时生成的子类。

在前面的代码片段中查看 CommandManager 类,可以看到Spring容器将动态覆盖createCommand() 方法的实现。你的 CommandManager 类将不会有任何 Spring 依赖,可以在重做的示例中看到:

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();

在包含要注入的方法的客户端类中(本例中是CommandManager),要注入的方法需要如下形式的签名:

<public|protected> [abstract] <return-type> theMethodName(no-arguments);

如果方法是抽象的,则动态生成的子类实现该方法。否则,动态生成的子类将重写在原始类中定义的具体方法。例如:

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
    <!-- inject dependencies here as required -->
</bean>

<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
    <lookup-method name="createCommand" bean="myCommand"/>
</bean>

标识为 commandManager的 bean 在需要 myCommand bean 的新实例时调用它自己的方法createCommand()。您必须小心地将 myCommand 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();

或者,更惯用的做法是,您可以依赖于目标 bean 根据查找方法声明的返回类型进行解析:

public abstract class CommandManager 

    public Object process(Object commandState) 
        MyCommand command = createCommand();
        command.setState(commandState);
        return command.execute();
    

    @Lookup
    protected abstract MyCommand createCommand();

请注意,您通常会用一个具体的存根实现来声明这种带注释的查找方法,以便它们与Spring的组件扫描规则兼容,其中抽象类在默认情况下会被忽略。这种限制不适用于显式注册或显式导入的bean类。

2、任意方法替换

与查找方法注入相比,方法注入的一种用处较小的形式是能够用另一种方法实现替换托管bean中的任意方法。用户可以安全地跳过本节的其余部分,直到真正需要该功能为止。

对于基于xml的配置元数据,您可以使用 replaced-method 元素将已部署 bean 的现有方法实现替换为另一个方法实现。考虑下面这个类,它有一个 computeValue 方法,我们想重写它:

public class MyValueCalculator 

    public String computeValue(String input) 
        // some real code...
    

    // some other methods...

实现org.springframework.bean .factory. support.methodreplacer接口的类提供了新的方法定义。

/**
 * 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 定义如下所示:

<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"/>

您可以在<replace -method/>元素中使用一个或多个包含的 <arg-type/> 元素来指示被重写方法的方法签名。只有当方法重载且类中存在多个变量时,参数的签名才有必要。为了方便起见,参数的类型字符串可以是完全限定类型名的子字符串。例如,以下所有匹配 java.lang.String:

java.lang.String
String
Str

因为参数的数量通常足以区分每个可能的选择,这种快捷方式可以通过允许您只键入匹配参数类型的最短字符串来节省大量的键入。

3、Spring Config 解决方案

其实不管是 Lookup method 方法注入或者是 replaced-method 它们的实现原理都是 Spring 框架通过使用来自 CGLIB 库的字节码生成来动态生成覆盖该方法的子类,从而实现了这种方法注入。它的实现可以查看CglibSubclassingInstantiationStrategy.CglibSubclassCreator#instantiate

CglibSubclassCreator#instantiate

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;

Lookup 的实现具体类是通过 LookupOverrideMethodInterceptor;方法替换的实现具体类是 ReplaceOverrideMethodInterceptor。它们都实现了 MethodInterceptor ,也就是通过 AOP 切面编程来增强这个 bean,然后传入 BeanFactory 每次通过 getBean() 动态获取 Bean。这样每次获取的 Bean 都是最新的。以 LookupOverrideMethodInterceptor 为例(ReplaceOverrideMethodInterceptor 实现原理类似):

LookupOverrideMethodInterceptor.java

private static class LookupOverrideMethodInterceptor extends CglibIdentitySupport implements MethodInterceptor 

	private final BeanFactory owner;

	public LookupOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanFactory owner) 
		super(beanDefinition);
		this.owner = owner;
	

	@Override
	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())) 
			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());
		
	

3.1 refresh scope 注册

在 Spring Cloud 里面要做到属性动态刷新首先需要在类上标注 @RefreshScope 注解。

@Target( ElementType.TYPE, ElementType.METHOD )
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope 

	/**
	 * @see Scope#proxyMode()
	 * @return proxy mode
	 */
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;



这样标注了 @RefreshScope 注解的 Bean 的 Scope 就是 refresh

RefreshScope 这个类的父类 GenericScope 实现了 BeanFactoryPostProcessor 以及 BeanDefinitionRegistryPostProcessor 这两个接口。在 BeanFactoryPostProcessor#postProcessBeanFactory 接口当中会注册 refresh Scope 的实现 (RefreshScope )这个类到 Spring 容器当中

GenericScoper#postProcessBeanFactory

	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
			throws BeansException 
		this.beanFactory = beanFactory;
		beanFactory.registerScope(this.name, this);
		setSerializationId(beanFactory);
	

3.2 refresh scope 类替换

BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry 方法的实现会把 refresh Scope 的 bean 的 Class 设置为 LockedScopedProxyFactoryBean

GenericScoper#postProcessBeanDefinitionRegistry

	@Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
			throws BeansException 
		for (String name : registry.getBeanDefinitionNames()) 
			BeanDefinition definition = registry.getBeanDefinition(name);
			if (definition instanceof RootBeanDefinition) 
				RootBeanDefinition root = (RootBeanDefinition) definition;
				if (root.getDecoratedDefinition() != null && root.hasBeanClass()
						&& root.getBeanClass() == ScopedProxyFactoryBean.class) 
					if (getName().equals(root.getDecoratedDefinition().getBeanDefinition()
							.getScope())) 
						root.setBeanClass(LockedScopedProxyFactoryBean.class);
						root.getConstructorArgumentValues().addGenericArgumentValue(this);
						// surprising that a scoped proxy bean definition is not already
						// marked as synthetic?
						root.setSynthetic(true);
					
				
			
		
	

3.3 refresh scope 获取 Bean

LockedScopedProxyFactoryBean 在对象创建的时候会使用 ReadWriteLock 加锁操作。它的父类 ScopedProxyFactoryBean 类在实现了容器感知类 BeanFactoryAware,当这个对象实例化的时候会调用 BeanFactoryAware#setBeanFactory 方法。

ScopedProxyFactoryBean#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());
	

这里把 refresh scope 类型的 bean 创建代理对象 SimpleBeanTargetSource 后面通过它来获取对象。它的实现方式其实和之前的 look-up 方法类似。都是通过 BeanFactory#getBean() 方法以及对应的 beanName 每次获取新的 Spring bean。

SimpleBeanTargetSource.java

public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource 

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


LockedScopedProxyFactoryBean 调用 getObject 方法的时候就会调用到 LockedScopedProxyFactoryBean#invoke 这个方法。

LockedScopedProxyFactoryBean.java

		@Override
		public void setTargetBeanName(String targetBeanName) 
			super.setTargetBeanName(targetBeanName);
			this.targetBeanName = targetBeanName;
		

		@Override
		public Object invoke(MethodInvocation invocation) throws Throwable 
			Method method = invocation.getMethod();
			if (AopUtils.isEqualsMethod(method) || AopUtils.isToStringMethod(method)
					|| AopUtils.isHashCodeMethod(method)
					|| isScopedObjectGetTargetObject(method)) 
				return invocation.proceed();
			
			Object proxy = getObject();
			ReadWriteLock readWriteLock =以上是关于Spring Cloud Config 解决单例 Bean 依赖非单例 Bean的主要内容,如果未能解决你的问题,请参考以下文章

Spring Cloud Config 解决单例 Bean 依赖非单例 Bean

Spring Cloud Config 解决单例 Bean 依赖非单例 Bean

Spring Cloud——Spring Cloud Alibaba 2021 Nacos Config bootstrap 配置文件失效解决方案

Spring cloud config客户端属性没有得到解决

Spring Cloud Config Server 是硬编码路径的有效解决方案吗?

(记录)整合spring cloud bus+rabbitmq后,config server/client启动报错及解决方式