启用 Spring-Cloud-OpenFeign 配置可刷新,项目无法启动,我 TM 人傻了(下)

出门莫恨无人随,书中车马多如簇。这篇文章主要讲述启用 Spring-Cloud-OpenFeign 配置可刷新,项目无法启动,我 TM 人傻了(下)相关的知识,希望能为你提供帮助。

启用 Spring-Cloud-OpenFeign 配置可刷新,项目无法启动,我 TM 人傻了(下)

文章图片

本篇文章涉及底层设计以及原理,以及问题定位,比较深入,篇幅较长,所以拆分成上下两篇:
  • 上:问题简单描述以及 Spring Cloud RefreshScope 的原理
  • 下:当前 spring-cloud-openfeign + spring-cloud-sleuth 带来的 bug 以及如何修复
Spring Cloud 中的配置动态刷新其实在测试的程序中,我们已经实现了一个简单的 Bean 刷新的设计。Spring Cloud 的自动刷新中,包含两种元素的刷新,分别是:
  • 配置刷新,即 Environment.getProperties@ConfigurationProperties 相关 Bean 的刷新
  • 添加了 @RefreshScope 注解的 Bean 的刷新
@RefreshScope 注解其实和我们上面自定义 Scope 使用的注解配置类似,即指定名称为 refresh,同时使用 CGLIB 代理:
RefreshScope
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Scope("refresh") @Documented public @interface RefreshScope { ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; }

同时需要自定义 Scope 进行注册,这个自定义的 Scope 即 org.springframework.cloud.context.scope.refresh.RefreshScope,他继承了 GenericScope,我们先来看这个父类,我们专注我们前面测试的那三个 Scope 接口方法,首先是 get:
private BeanLifecycleWrapperCache cache = new BeanLifecycleWrapperCache(new StandardScopeCache()); @Override public Object get(String name, ObjectFactory< ?> objectFactory) { //放入缓存 BeanLifecycleWrapper value = https://www.songbingjia.com/android/this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory)); this.locks.putIfAbsent(name, new ReentrantReadWriteLock()); try { //这里在第一次调用会创建 Bean 实例,所以需要上锁,保证只创建一次 return value.getBean(); } catch (RuntimeException e) { this.errors.put(name, e); throw e; } }

然后是注册 Destroy 的回调,其实就放在对应的 Bean 中,在移除的时候,会调用这个回调:
@Override public void registerDestructionCallback(String name, Runnable callback) { BeanLifecycleWrapper value = https://www.songbingjia.com/android/this.cache.get(name); if (value == null) { return; } value.setDestroyCallback(callback); }

最后是移除 Bean,就更简单了,从缓存中移除这个 Bean:
@Override public Object remove(String name) { BeanLifecycleWrapper value = https://www.songbingjia.com/android/this.cache.remove(name); if (value == null) { return null; } return value.getBean(); }

这样,如果缓存中的 bean 被移除,下次调用 get 的时候,就会重新生成 Bean。并且,由于 RefreshScope 注解中默认的 ScopedProxyMode 为 CGLIB 代理模式,所以每次通过 BeanFactory 获取 Bean 以及自动装载的 Bean 调用的时候,都会调用这里 Scope 的 get 方法。
Spring Cloud 将动态刷新接口通过 Spring Boot Actuator 进行暴露,对应路径是 /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; }}

可以看出其核心是 ContextRefresher,他的核心逻辑也非常简单:
ContextRefresher
public synchronized Set< String> refresh() { Set< String> keys = refreshEnvironment(); //刷新 RefreshScope this.scope.refreshAll(); return keys; }public synchronized Set< String> refreshEnvironment() { //提取 SYSTEM、JNDI、SERVLET 之外所有参数变量 Map< String, Object> before = extract(this.context.getEnvironment().getPropertySources()); //从配置源更新 Environment 中的所有属性 updateEnvironment(); //与刷新前作对比,提取出所有变了的属性 Set< String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet(); //将该变了的属性,放入 EnvironmentChangeEvent 并发布 this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys)); //返回所有改变的属性 return keys; }

调用 RefreshScope 的 RefreshAll,其实就是调用我们上面说的 GenericScope 的 destroy,之后发布 RefreshScopeRefreshedEvent:
public void refreshAll() { super.destroy(); this.context.publishEvent(new RefreshScopeRefreshedEvent()); }

GenericScope 的 destroy 其实就是将缓存清空,这样所有标注 @RefreshScope 注解的 Bean 都会被重建。
问题定位通过上篇的源码分析,我们知道,如果想实现 Feign.Options 的动态刷新,目前我们不能把它放入 NamedContextFactory 生成的 ApplicationContext 中,而是需要将它放入项目的根 ApplicationContext 中,这样 Spring Cloud 暴露的 refresh actuator 接口,才能正确刷新。spring-cloud-openfeign 中,也是这么实现的。
如果配置了
feign.client.refresh-enabled: true

那么在初始化每个 FeignClient 的时候,就会将 Feign.Options 这个 Bean 注册到根 ApplicationContext,对应源码:
FeignClientsRegistrar
private void registerOptionsBeanDefinition(BeanDefinitionRegistry registry, String contextId) { if (isClientRefreshEnabled()) { //使用 "feign.Request.Options-FeignClient 的 contextId" 作为 Bean 名称 String beanName = Request.Options.class.getCanonicalName() + "-" + contextId; BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder .genericBeanDefinition(OptionsFactoryBean.class); //设置为 RefreshScope definitionBuilder.setScope("refresh"); definitionBuilder.addPropertyValue("contextId", contextId); BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(definitionBuilder.getBeanDefinition(), beanName); //注册为 CGLIB 代理的 Bean definitionHolder = ScopedProxyUtils.createScopedProxy(definitionHolder, registry, true); //注册 Bean BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry); } }private boolean isClientRefreshEnabled() { return environment.getProperty("feign.client.refresh-enabled", Boolean.class, false); }

这样,在调用 /actuator/refresh 接口的时候,这些 Feign.Options 也会被刷新。但是注册到根 ApplicationContext 中的话,对应的 FeignClient 如何获取这个 Bean 使用呢?即在 Feign 的 NamedContextFactory (即 FeignContext )中生成的 ApplicationContext 中,如何找到这个 Bean 呢?
这个我们不用担心,因为所有的 NamedContextFactory 生成的 ApplicationContext 的 parent,都设置为了根 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) { //省略其他代码 if (this.parent != null) { // Uses Environment from parent as well as beans context.setParent(this.parent); } //省略其他代码 } }

这样设置后,FeignClient 在自己的 ApplicationContext 中如果找不到的话,就会去 parent 的 ApplicationContext 也就是根 ApplicationContext 去找。
这样看来,设计是没问题的,但是我们的项目启动不了,应该是启用其他依赖导致的。
我们在获取 Feign.Options Bean 的地方打断点调试,发现并不是直接从 FeignContext 中获取 Bean,而是从 spring-cloud-sleuth 的 TraceFeignContext 中获取的。
spring-cloud-sleuth 为了保持链路,在很多地方增加了埋点,对于 OpenFeign 也不例外。在 FeignContextBeanPostProcessor,将 FeignContext 包装了一层变成了 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); }}

【启用 Spring-Cloud-OpenFeign 配置可刷新,项目无法启动,我 TM 人傻了(下)】这样,FeignClient 会从这个 TraceFeignContext 中读取 Bean,而不是 FeignContext。但是通过源码我们发现,TraceFeignContext 并没有设置 parent 为根 ApplicationContext,所以找不到注册到根 ApplicationContext 中的 Feign.Options 这些 Bean。
解决问题针对这个 Bug,我向 spring-cloud-sleuth 和 spring-cloud-commons 分别提了修改:
  • add getter for parent in NamedContextFactory
  • fix #2023, add parent in the new TraceFeignContext
大家如果在项目中使用了 spring-cloud-sleuth,对于 spring-cloud-openfeign 想开启自动刷新的话,可以考虑使用同名同路径的类替换代码先解决这个问题。等待我提交的代码发布新版本了。
参考代码:
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); } }

启用 Spring-Cloud-OpenFeign 配置可刷新,项目无法启动,我 TM 人傻了(下)

文章图片


    推荐阅读