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

Dynamic configuration refresh in Spring Cloud

In fact, in the test program, we have implemented a simple Bean refresh design. Spring Cloud automatically refreshes two elements:

  • Refresh the configuration, that isEnvironment.getProperties@ConfigurationPropertiesRefresh of related beans
  • added@RefreshScopeRefresh of annotated beans

The @refreshScope annotation is similar to the annotation configuration we used for our custom Scope above, specifying refresh as the name and using the CGLIB proxy:

RefreshScope

@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Scope("refresh") @Documented public @interface RefreshScope {  ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; }Copy the code

Need to define the Scope of registration at the same time, the custom Scope namely org. Springframework. Cloud. Context. The Scope. Refresh. RefreshScope, he inherited the GenericScope, Let’s start with the parent class and focus on the three Scope interface methods we tested earlier, starting with GET:

private BeanLifecycleWrapperCache cache = new BeanLifecycleWrapperCache(new StandardScopeCache()); @Override public Object get(String name, ObjectFactory<? > objectFactory) {// Put into cache BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory)); this.locks.putIfAbsent(name, new ReentrantReadWriteLock()); Return value.getbean (); return value.getbean (); } catch (RuntimeException e) { this.errors.put(name, e); throw e; }}Copy the code

Then there is the callback to register Destroy, which is placed in the corresponding Bean and will be called upon removal:

@Override
public void registerDestructionCallback(String name, Runnable callback) {
	BeanLifecycleWrapper value = this.cache.get(name);
	if (value == null) {
		return;
	}
	value.setDestroyCallback(callback);
}
Copy the code

Finally, remove the Bean, which is even easier, by removing the Bean from the cache:

@Override
public Object remove(String name) {
	BeanLifecycleWrapper value = this.cache.remove(name);
	if (value == null) {
		return null;
	}
	return value.getBean();
}
Copy the code

This way, if the bean in the cache is removed, the bean will be regenerated the next time get is called. And because the default ScopedProxyMode in the RefreshScope annotation is the CGLIB proxy mode, every time a Bean is retrieved from the BeanFactory and automatically loaded Bean is called, Call the Scope get method here.

In Spring Cloud, the dynamic refresh interface is exposed via Spring Boot Actuator/REFRESH. The corresponding path is/Actuator /refresh.

RefreshEndpoint

@Endpoint(id = "refresh") public class RefreshEndpoint { private ContextRefresher contextRefresher; public RefreshEndpoint(ContextRefresher contextRefresher) { this.contextRefresher = contextRefresher; } @WriteOperation public Collection<String> refresh() { Set<String> keys = this.contextRefresher.refresh(); return keys; }}Copy the code

It can be seen that its core is ContextRefresher, and its core logic is very simple:

ContextRefresher

public synchronized Set<String> refresh() { Set<String> keys = refreshEnvironment(); // refresh the RefreshScope this.scope.refreshall (); return keys; } public synchronized Set<String> refreshEnvironment() { Object> before = extract(this.context.getEnvironment().getPropertySources()); // Update all the Environment attributes from the configuration source updateEnvironment(); // Compare with before refresh, Extract all changed the attribute Set of < String > keys = changes (before, extract (enclosing context. GetEnvironment () getPropertySources ())). The keySet (); / / put the changed the properties of the in EnvironmentChangeEvent and publish this. Context. PublishEvent (new EnvironmentChangeEvent (enclosing context, keys)); // Return keys; }Copy the code

Call RefreshScope RefreshAll, is called the GenericScope destroy, which we said after publish RefreshScopeRefreshedEvent:

public void refreshAll() {
	super.destroy();
	this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
Copy the code

GenericScope’s destroy simply clears the cache so that all beans annotated with the @refreshScope annotation are rebuilt.

Problem orientation

If you want to refresh Feign.Options dynamically, you can’t put it in the ApplicationContext generated by NamedContextFactory. Instead, you need to put it in the root ApplicationContext of your project so that the Refresh Interface exposed by Spring Cloud can refresh correctly. Spring-cloud-openfeign does the same thing.

If configured

feign.client.refresh-enabled: true
Copy the code

When initializing each FeignClient, the feign. Options Bean is registered with the root ApplicationContext.

FeignClientsRegistrar

private void registerOptionsBeanDefinition(BeanDefinitionRegistry registry, String contextId) {if (isClientRefreshEnabled()) {// Use "feign.request.options -FeignClient contextId" as the Bean name String beanName = Request.Options.class.getCanonicalName() + "-" + contextId; BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder .genericBeanDefinition(OptionsFactoryBean.class); / / set to RefreshScope definitionBuilder. SetScope (" refresh "); definitionBuilder.addPropertyValue("contextId", contextId); BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(definitionBuilder.getBeanDefinition(), beanName); / / register for additional proxy Bean definitionHolder = ScopedProxyUtils. CreateScopedProxy (definitionHolder, registry, true); / / registered Bean BeanDefinitionReaderUtils. RegisterBeanDefinition (definitionHolder, registry); } } private boolean isClientRefreshEnabled() { return environment.getProperty("feign.client.refresh-enabled", Boolean.class, false); }Copy the code

In this way, the feign. Options are also refreshed when the /actuator/refresh interface is called. But how does the corresponding FeignClient get the Bean to use if registered in the root ApplicationContext? How do you find this Bean in the ApplicationContext generated in Feign’s NamedContextFactory?

We don’t have to worry about this, because the parent of all ApplicationContext generated by NamedContextFactory is set to the root ApplicationContext.

public abstract class NamedContextFactory<C extends NamedContextFactory.Specification> implements DisposableBean, ApplicationContextAware { private ApplicationContext parent; @Override public void setApplicationContext(ApplicationContext parent) throws BeansException { this.parent = parent; } protected AnnotationConfigApplicationContext createContext (String name) {/ / omit other code if (this. The parent! = null) { // Uses Environment from parent as well as beans context.setParent(this.parent); } // omit other code}}Copy the code

If the FeignClient cannot find the parent’s ApplicationContext, it will look for the root ApplicationContext.

In this case, the design was fine, but our project didn’t start because of other dependencies.

We break point debugging where we get the feign.options Bean and discover that the Bean is not directly fetched from the FeignContext, but from the TraceFeignContext of Spring-Cloud-sleuth.

Spring-cloud-sleuth adds buried points in many places to maintain links, and OpenFeign is no exception. In FeignContextBeanPostProcessor FeignContext packing a layer into TraceFeignContext:

public class FeignContextBeanPostProcessor implements BeanPostProcessor {

	private final BeanFactory beanFactory;

	public FeignContextBeanPostProcessor(BeanFactory beanFactory) {
		this.beanFactory = beanFactory;
	}

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		if (bean instanceof FeignContext && !(bean instanceof TraceFeignContext)) {
			return new TraceFeignContext(traceFeignObjectWrapper(), (FeignContext) bean);
		}
		return bean;
	}

	private TraceFeignObjectWrapper traceFeignObjectWrapper() {
		return new TraceFeignObjectWrapper(this.beanFactory);
	}

}
Copy the code

Thus, FeignClient reads the Bean from the TraceFeignContext, not the FeignContext. The feign.options beans registered in the root ApplicationContext are not found because TraceFeignContext does not set parent to the root ApplicationContext.

To solve the problem

For this Bug, I added a change to Spring-Cloud-sleuth and Spring-Cloud-Commons:

  • add getter for parent in NamedContextFactory
  • fix #2023, add parent in the new TraceFeignContext

If you use Spring-Cloud-sleuth in your project and want to enable auto-refresh for Spring-Cloud-OpenFeign, consider using class replacement code with the same name and path to solve this problem first. A new version of the code waiting for my submission has been released.

Reference code:

public class FeignContextBeanPostProcessor implements BeanPostProcessor {
    private static final Field PARENT;
    private static final Log logger = LogFactory.getLog(FeignContextBeanPostProcessor.class);

    static {
        try {
            PARENT = NamedContextFactory.class.getDeclaredField("parent");
            PARENT.setAccessible(true);
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    private final BeanFactory beanFactory;

    public FeignContextBeanPostProcessor(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof FeignContext && !(bean instanceof TraceFeignContext)) {
            FeignContext feignContext = (FeignContext) bean;
            TraceFeignContext traceFeignContext = new TraceFeignContext(traceFeignObjectWrapper(), feignContext);
            try {
                traceFeignContext.setApplicationContext((ApplicationContext) PARENT.get(bean));
            } catch (IllegalAccessException e) {
                logger.warn("Cannot find parent in FeignContext: " + beanName);
            }
            return traceFeignContext;
        }
        return bean;
    }

    private TraceFeignObjectWrapper traceFeignObjectWrapper() {
        return new TraceFeignObjectWrapper(this.beanFactory);
    }
}
Copy the code

Wechat search “my programming meow” public account, a daily brush, easy to improve skills, won a variety of offers