解决启⽤Spring-Cloud-OpenFeign配置可刷新项⽬⽆法启动的问题
本篇⽂章涉及底层设计以及原理,以及问题定位,⽐较深⼊,篇幅较长,所以拆分成上下两篇:
上:问题简单描述以及 Spring Cloud RefreshScope 的原理
下:当前 spring-cloud-openfeign + spring-cloud-sleuth 带来的 bug 以及如何修复
最近在项⽬中想实现 OpenFeign 的配置可以动态刷新(主要是 Feign 的 Options 配置),例如:
feign:
client:
config:
default:
# 链接超时
connectTimeout: 500
# 读取超时
readTimeout: 8000
我们可能会观察到调⽤某个 FeignClient 的超时时间不合理,需要临时修改下,我们不想因为这种事情重启进程或者刷新整个 ApplicationContext,所以将这部分配置放⼊ spring-cloud-config 中并使⽤动态刷新的机制进⾏刷新。官⽅提供了这个配置⽅法,参考:
即在项⽬中增加配置:
fresh-enabled: true
但是在我们的项⽬中,增加了这个配置后,启动失败,报不到相关 Bean 的错误:
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'feign.Request.Options-testService1Client'
available
at org.springframework.beans.factory.BeanDefinition(DefaultListableBeanFactory.java:863)
at org.springframework.beans.factory.MergedLocalBeanDefinition(AbstractBeanFactory.java:1344)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:309)
at org.springframework.beans.factory.Bean(AbstractBeanFactory.java:213)
at t.Bean(AbstractApplicationContext.java:1160)
at org.springframework.cloud.Instance(FeignContext.java:57)
at org.springframework.cloud.OptionsByName(FeignClientFactoryBean.java:363)
at org.springframework.cloud.figureUsingConfiguration(FeignClientFactoryBean.java:195)
at org.springframework.cloud.figureFeign(FeignClientFactoryBean.java:158)
at org.springframework.cloud.openfeign.FeignClientFactoryBean.feign(FeignClientFactoryBean.java:132)
at org.springframework.cloud.Target(FeignClientFactoryBean.java:382)
at org.springframework.cloud.Object(FeignClientFactoryBean.java:371)
at org.springframework.cloud.openfeign.FeignClientsRegistrar.lambda$registerFeignClient$0(FeignClientsRegistrar.java:235)
at
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1231) at
org.springframework.beans.factory.ateBeanInstance(AbstractAutowireCapableBeanFactory.java:1173) at
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:564) at org.springframework.beans.factory.ateBean(AbstractAutowireCapableBeanFactory.java:524) ... 74 more
问题分析
通过这个 Bean 名称,其实可以看出来这个 Bean 是我们开始提到要动态刷新的 Feign.Options,⾥⾯有连接超时、读取超时等配置。名字后⾯的部分是我们创建的 FeignClient 上⾯@FeignClient注解⾥⾯的 contextId。
在创建 FeignClient 的时候,需要加载这个 Feign.Options Bean,每个 FeignClient 都有⾃⼰的 ApplicationContext,这个 Feign.Options Bean 就是属于每个FeignClient 单独的 ApplicationContext 的。这个是通过 Spring Cloud 的 NamedContextFactory 实现的。对于 NamedContextFactory 的深⼊分析,可以参考
我的这篇⽂章:
对于 OpenFeign 的配置开启动态刷新,其实就是对于 FeignClient 就是要刷新每个 FeignClient 的 Feign.Options 这个 Bean。那么如何实现呢?我们先来看spring-cloud 的动态刷新 Bean 的实现⽅式。⾸先我们要搞清楚,什么是 Scope。
Bean 的 Scope
从字⾯意思上⾯理解,Scope 即 Bean 的作⽤域。从实现上⾯理解,Scope 即我们在获取 Bean 的时候,这个 Bean 是如何获取的。
Spring 框架中⾃带两个⽿熟能详的 Scope,即 singleton 和 prototype。singleton 即每次从 BeanFactory 获取⼀个 Bean 的时候(getBean),对于同⼀个 Bean 每次返回的都是同⼀个对象,即单例模式。prototype 即每次从 BeanFactory 获取⼀个 Bean 的时候,对于同⼀个 Bean 每次都新创建⼀个对象返回,即⼯⼚模式。
同时,我们还可以根据⾃⼰需要去扩展 Scope,定义获取 Bean 的⽅式。举⼀个简单的例⼦,我们⾃定义⼀个 TestScope。⾃定义的 Scope 需要先定义⼀个实现org.springframework.fig.Scope接⼝的类,来定义在这个 Scope 下的 Bean 的获取相关的操作。
public interface Scope {
//获取这个 bean,在 Bean 的时候会被调⽤
Object get(String name, ObjectFactory<?> objectFactory);
//在调⽤ BeanFactory.destroyScopedBean 的时候,会调⽤这个⽅法
@Nullable
Object remove(String name);
//注册 destroy 的 callback
//这个是可选实现,提供给外部注册销毁 bean 的回调。可以在 remove 的时候,执⾏这⾥传⼊的 callback。
void registerDestructionCallback(String name, Runnable callback);
//如果⼀个 bean 不在 BeanFactory 中,⽽是根据上下⽂创建的,例如每个 http 请求创建⼀个独⽴的 bean,这样从 BeanFactory 中就拿不到了,就会从这⾥拿
//这个也是可选实现
Object resolveContextualObject(String key);
//可选实现,类似于 session id ⽤户区分不同上下⽂的
String getConversationId();
}
我们来实现⼀个简单的 Scope:
public static class TestScope implements Scope {
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Object();
}
@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;
}
}
这个 Scope 只是实现了 get ⽅法。直接通过传⼊的 objectFactory 创建⼀个新的 bean。这种 Scope 下
每次调⽤ Factory 都会返回⼀个新的Bean,⾃动装载到不同 Bean 的这种 Scope 下的 Bean 也是不同的实例。编写测试:
@Configuration
public static class Config {
@Bean
//⾃定义 Scope 的名字是 testScope
@t.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);
}
}
public static void main(String[] args) {
//创建⼀个 ApplicationContext
AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
//注册我们⾃定义的 Scope
//注册我们需要的配置 Bean
//调⽤ refresh 初始化 ApplicationContext
//获取 Config 这个 Bean
Config config = Bean(Config.class);
//调⽤⾃动装载的 Bean
st();
//从 BeanFactory 调⽤ getBean 获取 A
}
执⾏代码,丛输出上可以看出,这三个 A 都是不同的对象:
com.hopegaming.spring.cloud.parent.ScopeTest$A@5241cf67
com.hopegaming.spring.cloud.parent.ScopeTest$A@716a7124
com.hopegaming.spring.cloud.parent.ScopeTest$A@77192705
我们再来修改我们的 Bean,让它成为⼀个 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");
}
}
再修改下我们的⾃定义 Scope:
public static class TestScope implements Scope {
private Runnable callback;
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Object();
}
@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;
}
}
在测试代码中,增加调⽤ destroyScopedBean 销毁 bean:
运⾏代码,可以看到对应的输出:
registerDestructionCallback is called
a is removed
com.hopegaming.spring.cloud.parent.ScopeTest$A@716a7124 is destroyed
callback finished
对于 DisposableBean 或者其他有相关⽣命周期类型的 Bean,BeanFactory 会通过 registerDestructionCallback 将⽣命周期需要的操作回调传进来。使⽤BeanFactory.destroyScopedBean销毁 Bean 的时候,会调⽤ Scope 的 remove ⽅法,我们可以在操作完成时,调⽤ callback 回调完成 B
ean ⽣命周期。
接下来我们尝试实现⼀种单例的 Scope,⽅式⾮常简单,主要基于 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 mappute(name, (k, v) -> {
if (v == null) {
v = Object();
}
return v;
});
}
@Override
public Object remove(String name) {
ve(name);
System.out.println(name + " is removed");
(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;
}
}
我们使⽤两个 ConcurrentHashMap 缓存这个 Scope 下的 Bean,以及对应的 Destroy Callback。在这种实现下,就类似于单例模式的实现了。再使⽤下⾯的测试程序测试下:
public static void main(String[] args) {
AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
Config config = Bean(Config.class);
st();
//Config 类中注册 Bean 的⽅法名称为 a,所以 Bean 名称也为 a
st();
}
我们在销毁 Bean 之前,使⽤⾃动装载和 Bean 分别去请求获取 A 这个 Bean 并调⽤ test ⽅法。然后销毁这个 Bean。在这之后,再去使⽤⾃动装载的和 Bean 分别去请求获取 A 这个 Bean 并调⽤ test ⽅法。可以从输出中看出, Bean 请求的是新的 Bean 了,但是⾃动装载的⾥⾯还是已销毁的那个 bean。那么如何实现让⾃动装载的也是新的 Bean,也就是重新注⼊呢?
这就涉及到了 Scope 注解上⾯的另⼀个配置,即指定代理模式:
@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;
}
其中第三个配置,ScopedProxyMode 是配置获取这个 Bean 的时候,获取的是原始 Bean 对象还是代理的 Bean 对象(这也同时影响了⾃动装载):
public enum ScopedProxyMode {
//⾛默认配置,没有其他外围配置则是 NO
DEFAULT,
//使⽤原始对象作为 Bean
NO,
//使⽤ JDK 的动态代理
INTERFACES,
//使⽤ CGLIB 动态代理
TARGET_CLASS
}
我们来测试下指定 Scope Bean 的实际对象为代理的效果,我们修改下上⾯的测试代码,使⽤ CGLIB 动态代理。修改代码:
@Configuration
public static class Config {
@Bean
@t.annotation.Scope(value = "testScope"
//指定代理模式为基于 CGLIB
, proxyMode = ScopedProxyMode.TARGET_CLASS
)
public A a() {
return new A();
}
@Autowired
private A a;
}
编写测试主⽅法:
public static void main(String[] args) {
AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
Config config = Bean(Config.class);
st();
//查看 Bean 实例的类型
System.out.println(Class());
System.out.Bean(A.class).getClass());
//这时候我们需要注意,代理 Bean 的名称有所变化,需要通过 ScopedProxyUtils 获取
springcloud怎么读音st();
}
执⾏程序,输出为:
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
从输出中可以看出:
每次对于⾃动装载的 Bean 的调⽤,都会调⽤⾃定义 Scope 的 get ⽅法重新获取 Bean
每次通过 BeanFactory 获取 Bean,也会调⽤⾃定义 Scope 的 get ⽅法重新获取 Bean
获取的 Bean 实例,是⼀个 CGLIB 代理对象
在 Bean 被销毁后,⽆论是通过 BeanFactory 获取 Bean 还是⾃动装载的 Bean,都是新的 Bean
那么 Scope 是如何实现这些的呢?我们接下来简单分析下源码
Scope 基本原理
如果⼀个 Bean 没有声明任何 Scope,那么他的 Scope 就会被赋值成 singleton,也就是默认的 Bean
都是单例的。这个对应 BeanFactory 注册 Bean 之前需要⽣成 Bean 定义,在 Bean 定义的时候会赋上这个默认值,对应源码:
protected RootBeanDefinition getMergedBeanDefinition(
String beanName, BeanDefinition bd, @Nullable BeanDefinition containingBd)
throws BeanDefinitionStoreException {
//省略我们不关⼼的源码
if (!StringUtils.Scope())) {
mbd.setScope(SCOPE_SINGLETON);
}
//省略我们不关⼼的源码
}
在声明⼀个 Bean 具有特殊 Scope 之前,我们需要定义这个⾃定义 Scope 并把它注册到 BeanFactory 中。这个 Scope 名称必须全局唯⼀,因为之后区分不同Scope 就是通过这个名字进⾏区分的。注册 Scope 对应源码:
@Override
public void registerScope(String scopeName, Scope scope) {
//不能为 singleton 和 prototype 这两个预设的 scope
if (SCOPE_SINGLETON.equals(scopeName) || SCOPE_PROTOTYPE.equals(scopeName)) {
throw new IllegalArgumentException("Cannot replace existing scopes 'singleton' and 'prototype'");
}
//放⼊ scopes 这个 map 中,key 为名称,value 为⾃定义 Scope
Scope previous = this.scopes.put(scopeName, scope);
//可以看出,后⾯放⼊的会替换前⾯的,这个我们要尽量避免出现。
if (previous != null && previous != scope) {
if (logger.isDebugEnabled()) {
logger.debug("Replacing scope '" + scopeName + "' from [" + previous + "] to [" + scope + "]");
}
}
else {
if (logger.isTraceEnabled()) {
}
}
}
当声明⼀个 Bean 具有特殊的 Scope 之后,获取这个 Bean 的时候,就会有特殊的逻辑,参考通过 BeanFactory 获取 Bean 的核⼼源码代码:
@SuppressWarnings("unchecked")
protected <T> T doGetBean(
String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
throws BeansException {
//省略我们不关⼼的源码
// 创建 Bean 实例
if (mbd.isSingleton()) {
//创建或者返回单例实例
} else if (mbd.isPrototype()) {
//每次创建⼀个新实例
} else {
//⾛到这⾥代表这个 Bean 属于⾃定义 Scope
String scopeName = Scope();
//必须有 Scope 名称
if (!StringUtils.hasLength(scopeName)) {
throw new IllegalStateException("No scope name defined for bean ´" + beanName + "'");
}
//通过 Scope 名称获取对应的 Scope,⾃定义 Scope 需要⼿动注册进来
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论