This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.
This article involves the underlying design and principle, as well as the problem positioning. It is in-depth and long, so it is divided into two parts:
- Above: A brief description of the problem and the principle of Spring Cloud RefreshScope
- Current spring-cloud-OpenFeign + Spring-cloud-sleuth bugs and how to fix them
Recently in the project to implement OpenFeign configuration can be dynamically updated (mainly Feign Options configuration), for example:
Feign: client: config: default: # connectTimeout: 500 # readTimeout readTimeout: 8000Copy the code
We might observe that the timeout for calling a FeignClient is not reasonable and needs to be changed temporarily. We don’t want to restart the process or refresh the entire ApplicationContext because of this. So put this part of the configuration into spring-Cloud-config and refresh it using a dynamic refresh mechanism. This configuration method is provided by the official documentation – spring@refreshScope Support
Add configuration to the project:
feign.client.refresh-enabled: true
Copy the code
In our project, however, after adding this configuration, the startup failed and the related Bean was not found:
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'feign.Request.Options-testService1Client' available at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:8 63) at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1344 ) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:309) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213) at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1160) at org.springframework.cloud.openfeign.FeignContext.getInstance(FeignContext.java:57) at org.springframework.cloud.openfeign.FeignClientFactoryBean.getOptionsByName(FeignClientFactoryBean.java:363) at org.springframework.cloud.openfeign.FeignClientFactoryBean.configureUsingConfiguration(FeignClientFactoryBean.java:195) at org.springframework.cloud.openfeign.FeignClientFactoryBean.configureFeign(FeignClientFactoryBean.java:158) at org.springframework.cloud.openfeign.FeignClientFactoryBean.feign(FeignClientFactoryBean.java:132) at org.springframework.cloud.openfeign.FeignClientFactoryBean.getTarget(FeignClientFactoryBean.java:382) at org.springframework.cloud.openfeign.FeignClientFactoryBean.getObject(FeignClientFactoryBean.java:371) at org.springframework.cloud.openfeign.FeignClientsRegistrar.lambda$registerFeignClient$0(FeignClientsRegistrar.java:235) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableB eanFactory.java:1231) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableB eanFactory.java:1173) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFac tory.java:564) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFacto ry.java:524) ... 74 moreCopy the code
Problem analysis
From the Bean name, you can actually see that this Bean is the feign.options that we started with to dynamically refresh, with connection timeout, read timeout, and so on. The part after the name is the contextId in the @FeignClient annotation on the FeignClient we created.
The feign.options Bean needs to be loaded when creating FeignClient. Each FeignClient has its own ApplicationContext. The Feign.Options Bean belongs to the individual ApplicationContext of each FeignClient. This is implemented through the Spring Cloud NamedContextFactory. For an in-depth analysis of NamedContextFactory, please refer to my article:
To enable dynamic refresh for OpenFeign configuration, you need to refresh the feign.options Bean for each FeignClient. So how do you do that? Let’s start by looking at how spring-Cloud’s dynamic refresh Bean is implemented. First of all, we need to make clear what Scope is.
The Scope of Bean
Literally, Scope is the Scope of a Bean. From the implementation perspective, Scope is how we get the Bean when we get it.
The Spring framework comes with two familiar scopes, Singleton and Prototype. A singleton is a getBean that gets a Bean from a BeanFactory. It returns the same object each time for the same Bean. Prototype creates a new object for the same Bean every time it retrieves a Bean from the BeanFactory (factory mode).
At the same time, we can also extend the Scope and define how to get the Bean as needed. To take a simple example, let’s customize a TestScope. Custom Scope need to define an implementation org. Springframework. Beans. Factory. Config. The Scope of the interface class, defined in the Scope of the Bean’s access to related operations.
Object get(String name, ObjectFactory<? > objectFactory); / / in the call. The BeanFactory destroyScopedBean, @ Nullable will invoke the method Object remove (String name); // Register the destroy callback // This is an optional implementation that provides a callback to the external registered destroy bean. The callback passed in here can be executed at remove. void registerDestructionCallback(String name, Runnable callback); // If a bean is not in the BeanFactory, it is created in context, such as a separate bean for each HTTP request, so that it is not retrieved from the BeanFactory, Implement Object resolveContextualObject(String Key); // Optional implementation, similar to session ID user context-specific String getConversationId(); }Copy the code
Let’s implement a simple Scope:
public static class TestScope implements Scope {
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
return objectFactory.getObject();
}
@Override
public Object remove(String name) {
return null;
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return null;
}
}
Copy the code
This Scope simply implements the GET method. Create a new bean directly from the objectFactory passed in. Each call to BeanFactory.getFactory in this Scope returns a new Bean, and the beans in this Scope that are automatically loaded into different beans are also different instances. Writing tests:
@configuration public static class Config {@bean // The name of the custom Scope is testScope @org.springframework.context.annotation.Scope(value = "testScope") public A a() { return new A(); } @autowired private A A; } public static class A { public void test() { System.out.println(this); }}Copy the code
Public static void main (String [] args) {/ / create a ApplicationContext AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(); . / / registered our custom Scope annotationConfigApplicationContext getBeanFactory () registerScope (" testScope ", new testScope ()); / / registration we need the configuration of the Bean annotationConfigApplicationContext. Register (Config. Class); / / call the refresh initialization ApplicationContext annotationConfigApplicationContext. Refresh (); / / get the Config the Bean Config Config. = annotationConfigApplicationContext getBean (Config. Class); // Call the auto-loaded Bean config.a.test(); / / from the BeanFactory call getBean obtain A annotationConfigApplicationContext getBean (A.c lass). The test (); annotationConfigApplicationContext.getBean(A.class).test(); }Copy the code
Executing the code, we can see from the cluster output that the three A’s are different objects:
com.hopegaming.spring.cloud.parent.ScopeTest$A@5241cf67
com.hopegaming.spring.cloud.parent.ScopeTest$A@716a7124
com.hopegaming.spring.cloud.parent.ScopeTest$A@77192705
Copy the code
Let’s modify our Bean to be a Disposable Bean:
public static class A implements DisposableBean { public void test() { System.out.println(this); } @Override public void destroy() throws Exception { System.out.println(this + " is destroyed"); }}Copy the code
Let’s modify our custom Scope:
public static class TestScope implements Scope { private Runnable callback; @Override public Object get(String name, ObjectFactory<? > objectFactory) { return objectFactory.getObject(); } @Override public Object remove(String name) { System.out.println(name + " is removed"); this.callback.run(); System.out.println("callback finished"); return null; } @Override public void registerDestructionCallback(String name, Runnable callback) { System.out.println("registerDestructionCallback is called"); this.callback = callback; } @Override public Object resolveContextualObject(String key) { System.out.println("resolveContextualObject is called"); return null; } @Override public String getConversationId() { System.out.println("getConversationId is called"); return null; }}Copy the code
In the test code, add a call to destroyScopedBean to destroy the bean:
annotationConfigApplicationContext.getBeanFactory().destroyScopedBean("a");
Copy the code
Run the code and see the corresponding output:
registerDestructionCallback is called
a is removed
com.hopegaming.spring.cloud.parent.ScopeTest$A@716a7124 is destroyed
callback finished
Copy the code
For DisposableBean types of beans or other related life cycle, the BeanFactory will pass registerDestructionCallback will need lifecycle callback operation incoming. . Use the BeanFactory destroyScopedBean destruction of Bean, would call the Scope of the remove method, we can be in operation is complete, invoking the callback complete Bean lifecycle callback.
Next we try to implement a singleton Scope in a very simple way based on ConcurrentHashMap:
public static class TestScope implements Scope {
private final ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Runnable> callback = new ConcurrentHashMap<>();
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
System.out.println("get is called");
return map.compute(name, (k, v) -> {
if (v == null) {
v = objectFactory.getObject();
}
return v;
});
}
@Override
public Object remove(String name) {
this.map.remove(name);
System.out.println(name + " is removed");
this.callback.get(name).run();
System.out.println("callback finished");
return null;
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
System.out.println("registerDestructionCallback is called");
this.callback.put(name, callback);
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return null;
}
}
Copy the code
We use two ConCurrenthashMaps to cache beans under this Scope, along with the corresponding Destroy Callback. Under this implementation, it is similar to the implementation of the singleton pattern. Use the following test program to test:
public static void main(String[] args) { AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(); annotationConfigApplicationContext.getBeanFactory().registerScope("testScope", new TestScope()); annotationConfigApplicationContext.register(Config.class); annotationConfigApplicationContext.refresh(); Config config = annotationConfigApplicationContext.getBean(Config.class); config.a.test(); annotationConfigApplicationContext.getBean(A.class).test(); / / Config class registration method name as a Bean, so Bean name for a annotationConfigApplicationContext getBeanFactory () destroyScopedBean (" a "); config.a.test(); annotationConfigApplicationContext.getBean(A.class).test(); }Copy the code
Before destroying the Bean, we request A Bean using autoload and beanFactory.getBean and call the test method, respectively. Then destroy the Bean. After that, use the auto-loaded and beanFactory.getBean to request A and call the test method, respectively. As you can see from the output, beanFactory.getBean is requesting a new Bean, but the auto-loaded Bean still contains the destroyed Bean. So how do you implement auto-loaded beans that are also new, that is, reinjected?
This leads to another configuration above the Scope annotation, which specifies the proxy mode:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
@AliasFor("scopeName")
String value() default "";
@AliasFor("value")
String scopeName() default "";
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}
Copy the code
The third configuration, ScopedProxyMode, configures whether the original Bean object or the proxy Bean object is retrieved when the Bean is fetched (which also affects auto-loading) :
Public enum ScopedProxyMode {// Default configuration, // Use the original object as Bean NO, // use JDK dynamic proxy INTERFACES, // use CGLIB dynamic proxy TARGET_CLASS}Copy the code
To test the effect of specifying the Scope Bean’s actual object as a proxy, let’s modify the above test code to use CGLIB dynamic proxy. Modify code:
@Configuration public static class Config { @Bean @org.springframework.context.annotation.Scope(value = "testScope" ProxyMode = scopedProxymode.target_class) public A A() {return new A(); } @Autowired private A a; }Copy the code
Write the test master method:
public static void main(String[] args) { AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(); annotationConfigApplicationContext.getBeanFactory().registerScope("testScope", new TestScope()); annotationConfigApplicationContext.register(Config.class); annotationConfigApplicationContext.refresh(); Config config = annotationConfigApplicationContext.getBean(Config.class); config.a.test(); annotationConfigApplicationContext.getBean(A.class).test(); // Check the type of the Bean instance system.out.println (config.a.gettclass ()); System.out.println(annotationConfigApplicationContext.getBean(A.class).getClass()); // We need to note that the name of the proxy Bean has changed, This is obtained via ScopedProxyUtils annotationConfigApplicationContext.getBeanFactory().destroyScopedBean(ScopedProxyUtils.getTargetBeanName("a")); config.a.test(); annotationConfigApplicationContext.getBean(A.class).test(); }Copy the code
Execute the program with the output:
get is called
registerDestructionCallback is called
com.hopegaming.spring.cloud.parent.ScopeTest$A@3dd69f5a
get is called
com.hopegaming.spring.cloud.parent.ScopeTest$A@3dd69f5a
class com.hopegaming.spring.cloud.parent.ScopeTest$A$$EnhancerBySpringCGLIB$$2fa625ee
class com.hopegaming.spring.cloud.parent.ScopeTest$A$$EnhancerBySpringCGLIB$$2fa625ee
scopedTarget.a is removed
com.hopegaming.spring.cloud.parent.ScopeTest$A@3dd69f5a is destroyed
callback finished
get is called
registerDestructionCallback is called
com.hopegaming.spring.cloud.parent.ScopeTest$A@3aa3193a
get is called
com.hopegaming.spring.cloud.parent.ScopeTest$A@3aa3193a
Copy the code
As can be seen from the output:
- Every time a call is made to an auto-loaded Bean, the custom Scope’s GET method is called to retrieve the Bean
- Each time a Bean is retrieved from the BeanFactory, the custom Scope’s GET method is also called to retrieve the Bean again
- The Bean instance obtained is a CGLIB proxy object
- After the Bean is destroyed, either the Bean obtained through the BeanFactory or the Bean automatically loaded is a new Bean
So how does Scope implement this? Let’s briefly analyze the source code
Scope principle
If a Bean does not declare any Scope, its Scope is assigned as a singleton, which means that the default Bean is singleton. BeanFactory: BeanFactory: BeanFactory: BeanFactory: BeanFactory: BeanFactory: BeanFactory: BeanFactory: BeanFactory
AbstractBeanFactory
protected RootBeanDefinition getMergedBeanDefinition( String beanName, BeanDefinition bd, @ Nullable BeanDefinition containingBd) throws BeanDefinitionStoreException {/ / omitted we don't care about the source of the if (! StringUtils.hasLength(mbd.getScope())) { mbd.setScope(SCOPE_SINGLETON); } // omit source code that we don't care about}Copy the code
Before declaring a Bean to have a special Scope, we need to define the custom Scope and register it with the BeanFactory. This Scope name must be globally unique, because it will be used to distinguish different scopes later. Register Scope source code:
AbstractBeanFactory
@Override public void registerScope(String scopeName, Scope scope) { Assert.notNull(scopeName, "Scope identifier must not be null"); Assert.notNull(scope, "Scope must not be null"); / / not for singleton and prototype of the two preset scope if (SCOPE_SINGLETON. Equals (scopeName) | | SCOPE_PROTOTYPE. Equals (scopeName)) { throw new IllegalArgumentException("Cannot replace existing scopes 'singleton' and 'prototype'"); Scope Scope previous = this.scopes. Put (scopeName, Scope); // We can see that the last one will replace the first one, which we should avoid. if (previous ! = null && previous ! = scope) { if (logger.isDebugEnabled()) { logger.debug("Replacing scope '" + scopeName + "' from [" + previous + "] to [" + scope + "]"); } } else { if (logger.isTraceEnabled()) { logger.trace("Registering scope '" + scopeName + "' with implementation [" + scope + "]"); }}}Copy the code
When a Bean is declared to have a special Scope, there is special logic to fetch the Bean, refer to the core source code of the BeanFactory:
AbstractBeanFactory
@SuppressWarnings("unchecked") protected <T> T doGetBean( String name, @Nullable Class<T> requiredType, @Nullable Object[] args, Boolean typeCheckOnly) throws BeansException {// omit source code we don't care about // Create Bean instance if (mbd.issingleton ()) {// create or return singleton instance} else Scope String scopeName = mbd.getScope(); if (mbd.isPrototype()) {mbd.isprototype () = mbd.getScope(); // Must have Scope name if (! Stringutils.haslength (scopeName)) {throw new IllegalStateException("No scope name defined for bean ยด" + beanName + "'"); } Scope Scope = this.scopes. Get (scopeName);} Scope Scope = this.scopes. if (scope == null) { throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); Scope.get (beanName, () -> {// Call the Bean Object scope.get(beanName, () -> { Create Bean beforePrototypeCreation(beanName); try { return createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); }}); beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); } catch (IllegalStateException ex) { throw new ScopeNotActiveException(beanName, scopeName, ex); }} // omit source code that we don't care about}Copy the code
If the Scope Bean definition is CGLIB, the Scope Bean definition will be created according to the original Bean definition.
ScopedProxyUtils
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition, BeanDefinitionRegistry registry, Boolean proxyTargetClass) {String originalBeanName = definition. GetBeanName (); / / get the original target Bean definitions BeanDefinition targetDefinition = definition. GetBeanDefinition (); String targetBeanName = getTargetBeanName(originalBeanName); // Create a Bean of type ScopedProxyFactoryBean RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class); // Configure the properties of the proxy Bean according to the properties defined by the original target Bean. Copy to the broker Bean definitions proxyDefinition. SetAutowireCandidate (targetDefinition. IsAutowireCandidate ()); proxyDefinition.setPrimary(targetDefinition.isPrimary()); if (targetDefinition instanceof AbstractBeanDefinition) { proxyDefinition.copyQualifiersFrom((AbstractBeanDefinition) targetDefinition); } // Set the original Bean to be not automatically loaded and not Primary // So that the Bean obtained through the BeanFactory and automatically loaded are proxy beans instead of the original target Bean targetDefinition.setAutowireCandidate(false); targetDefinition.setPrimary(false); / / to use the new name registered Bean registry. RegisterBeanDefinition (targetBeanName targetDefinition); return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases()); } private static final String TARGET_NAME_PREFIX = "scopedTarget."; // This is the utility method to get the name of the proxy Bean, Public static String getBeanname (String originalBeanName) {return TARGET_NAME_PREFIX + originalBeanName; }Copy the code
What does this proxy Bean do? The main use is that every time any method of the Bean is called, the BeanFactory gets the Bean and calls it. Reference source:
The proxy classScopedProxyFactoryBean
public class ScopedProxyFactoryBean extends ProxyConfig implements FactoryBean<Object>, BeanFactoryAware, AopInfrastructureBean { private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource(); // This is the actual proxy generated by SimpleBeanTargetSource. All Bean method calls are made through this proxy. }Copy the code
SimpleBeanTargetSource is the actual proxy source. Its implementation is very simple. The core method is to get the Bean from the BeanFactory using the Bean name:
public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource { @Override public Object getTarget() throws Exception { return getBeanFactory().getBean(getTargetBeanName()); }}Copy the code
Get the BeanFactory BeanFactory BeanFactory BeanFactory BeanFactory BeanFactory BeanFactory BeanFactory BeanFactory
Then the destruction of Bean, create the Bean in the BeanFactory object, is called a custom Scope of registerDestructionCallback Bean destroyed the callback into:
AbstractBeanFactory
protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) { AccessControlContext acc = (System.getSecurityManager() ! = null ? getAccessControlContext() : null); if (! mbd.isPrototype() && requiresDestruction(bean, MBD)) {if (mbd.isSingleton()) {// For singleton registerDisposableBean(beanName, new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessorCache().destructionAware, acc)); } else {// For custom Scope Scope Scope = this.scopes. Get (mbd.getScope()); if (scope == null) { throw new IllegalStateException("No Scope registered for scope name '" + mbd.getScope() + "'"); } / / call registerDestructionCallback scope. RegisterDestructionCallback (beanName, new DisposableBeanAdapter (bean, beanName, mbd, getBeanPostProcessorCache().destructionAware, acc)); }}}Copy the code
When we want to destroy a Scope Bean, we call the BeanFactory destroyScopedBean method, which calls the remove of our custom Scope:
AbstractBeanFactory
@Override public void destroyScopedBean(String beanName) { RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); / / only for custom Scope Bean using an if (MBD) isSingleton () | | MBD. IsPrototype ()) {throw new IllegalArgumentException (" Bean name '" + beanName + "' does not correspond to an object in a mutable scope"); } String scopeName = mbd.getScope(); Scope scope = this.scopes.get(scopeName); if (scope == null) { throw new IllegalStateException("No Scope SPI registered for scope name '" + scopeName + "'"); } Object bean = scope.remove(beanName); if (bean ! = null) { destroyBean(beanName, bean, mbd); }}Copy the code
Wechat search “my programming meow” public account, a daily brush, easy to improve skills, won a variety of offers