编程知识 cdmana.com

Enable spring cloud openfeign configuration to refresh, and the project cannot be started. I'm stupid (Part 2)

image

This article deals with the underlying design and principles , And problem location , More in-depth , Length is longer than the , So split it into two parts :

  • On : A brief description of the problem and Spring Cloud RefreshScope Principle
  • Next : At present spring-cloud-openfeign + spring-cloud-sleuth It brings bug And how to fix it

Spring Cloud Configuration dynamic refresh in

In fact, in the test program , We have a simple implementation Bean Refresh design .Spring Cloud Automatic refresh of , Refresh with two elements , Namely :

  • Configure the refresh , namely Environment.getProperties and @ConfigurationProperties relevant Bean Refresh of
  • Added @RefreshScope Annotated Bean Refresh of

@RefreshScope The annotation is actually the same as what we customized above Scope The annotation configuration used is similar to , That is, the specified name is refresh, Use at the same time CGLIB agent :

RefreshScope

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

}

It also needs to be customized Scope To register , This custom Scope namely org.springframework.cloud.context.scope.refresh.RefreshScope, He inherited the role of GenericScope, Let's look at this parent class first , We focus on the three we tested earlier Scope Interface method , First of all 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());
	try {
	    // Here, the first call will create  Bean  example , So it needs to be locked , Make sure to create... Only once 
		return value.getBean();
	}
	catch (RuntimeException e) {
		this.errors.put(name, e);
		throw e;
	}
}

Then register Destroy The callback , In fact, it is placed in the corresponding Bean in , When removed , This callback will be called :

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

Finally, remove Bean, It's even simpler , Remove this from the cache Bean:

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

such , If... In the cache bean Removed , Next call get When , It will regenerate Bean. also , because RefreshScope The default in the annotation is ScopedProxyMode by CGLIB The proxy pattern , So every time I pass BeanFactory obtain Bean And automatically loaded Bean When called , Will call here Scope Of get Method .

Spring Cloud Pass the dynamic refresh interface through Spring Boot Actuator Exposure , The corresponding path is /actuator/refresh, The corresponding source code is :

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;
	}

}

It can be seen that the core is ContextRefresher, His core logic is also very simple :

ContextRefresher

public synchronized Set<String> refresh() {
	Set<String> keys = refreshEnvironment();
	// Refresh  RefreshScope
	this.scope.refreshAll();
	return keys;
}

public synchronized Set<String> refreshEnvironment() {
    // extract  SYSTEM、JNDI、SERVLET  All parameter variables except 
	Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
	// Update from configuration source  Environment  All properties in 
	updateEnvironment();
	// Compare with before refresh , Extract all the changed attributes 
	Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
	// Change the properties that should be changed , Put in  EnvironmentChangeEvent  And publish 
	this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
	// Return all changed properties 
	return keys;
}

call RefreshScope Of RefreshAll, In fact, it calls what we said above GenericScope Of destroy, Post release RefreshScopeRefreshedEvent:

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

GenericScope Of destroy It's actually emptying the cache , So all the labels @RefreshScope Annotated Bean Will be rebuilt .

Problem location

Through the source code analysis of the previous article , We know , If you want to achieve Feign.Options Dynamic refresh of , At present, we can't put it in NamedContextFactory Generated ApplicationContext in , Instead, you need to put it at the root of the project ApplicationContext in , such Spring Cloud exposed refresh actuator Interface , To refresh correctly .spring-cloud-openfeign in , That's how it works .

If the

feign.client.refresh-enabled: true

Then initialize each FeignClient When , Will be Feign.Options This Bean Register to root ApplicationContext, Corresponding source code :

FeignClientsRegistrar

private void registerOptionsBeanDefinition(BeanDefinitionRegistry registry, String contextId) {
	if (isClientRefreshEnabled()) {
	    // Use  "feign.Request.Options-FeignClient  Of  contextId"  As  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 as  CGLIB  Acting  Bean
		definitionHolder = ScopedProxyUtils.createScopedProxy(definitionHolder, registry, true);
		// register  Bean
		BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
	}
}

private boolean isClientRefreshEnabled() {
	return environment.getProperty("feign.client.refresh-enabled", Boolean.class, false);
}

such , Calling /actuator/refresh At the interface , these Feign.Options Will also be refreshed . But register to the root ApplicationContext In the words of , Corresponding FeignClient How to get this Bean How to use it? ? That is to say Feign Of NamedContextFactory ( namely FeignContext ) Generated in the ApplicationContext in , How to find this Bean Well ?

We don't have to worry about this , Because of all the NamedContextFactory Generated ApplicationContext Of parent, All set to root ApplicationContext, Reference source code :

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.parent != null) {
			// Uses Environment from parent as well as beans
			context.setParent(this.parent);
		}
		// Omit other code 
	}
}

After this setting ,FeignClient In their own ApplicationContext If you can't find it , Will go parent Of ApplicationContext That's the root ApplicationContext Look for .

So it looks like , Design is no problem , But our project can't start , It should be caused by enabling other dependencies .

We are getting Feign.Options Bean Where to interrupt debugging , Discovery is not directly from FeignContext In order to get Bean, But from spring-cloud-sleuth Of TraceFeignContext From .

spring-cloud-sleuth In order to maintain the link , Buried points have been added in many places , about OpenFeign No exception . stay FeignContextBeanPostProcessor, take FeignContext A layer of packaging turned 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);
	}

}

such ,FeignClient From this TraceFeignContext Read from Bean, instead of FeignContext. But through the source code, we find that ,TraceFeignContext It's not set parent Root ApplicationContext, So we can't find the registered root ApplicationContext Medium Feign.Options these Bean.

solve the problem

Aiming at this Bug, I asked spring-cloud-sleuth and spring-cloud-commons Amendments are proposed respectively :

If you use spring-cloud-sleuth, about spring-cloud-openfeign If you want to turn on automatic refresh , You can consider replacing the code with a class with the same name and path to solve this problem first . Waiting for the new version of the code I submitted .

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

WeChat search “ My programming meow ” Official account , Once a day , Easy to upgrade technology , Capture all kinds of offer

版权声明
本文为[Dry goods are full]所创,转载请带上原文链接,感谢
https://cdmana.com/2021/10/20211002145410476i.html

Scroll to Top