历览千载书,时时见遗烈。这篇文章主要讲述启用 Spring-Cloud-OpenFeign 配置可刷新,项目无法启动,我 TM 人傻了(上)相关的知识,希望能为你提供帮助。
文章图片
本篇文章涉及底层设计以及原理,以及问题定位,比较深入,篇幅较长,所以拆分成上下两篇:
- 上:问题简单描述以及 Spring Cloud RefreshScope 的原理
- 下:当前 spring-cloud-openfeign + spring-cloud-sleuth 带来的 bug 以及如何修复
feign:
client:
config:
default:
# 链接超时
connectTimeout: 500
# 读取超时
readTimeout: 8000
我们可能会观察到调用某个 FeignClient 的超时时间不合理,需要临时修改下,我们不想因为这种事情重启进程或者刷新整个 ApplicationContext,所以将这部分配置放入 spring-cloud-config 中并使用动态刷新的机制进行刷新。官方提供了这个配置方法,参考:官方文档 - Spring @RefreshScope Support
即在项目中增加配置:
feign.client.refresh-enabled: true
但是在我们的项目中,增加了这个配置后,启动失败,报找不到相关 Bean 的错误:
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:863)
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(AbstractAutowireCapableBeanFactory.java:1231)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1173)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:564)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(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.beans.factory.config.Scope
接口的类,来定义在这个 Scope 下的 Bean 的获取相关的操作。public interface Scope {
//获取这个 bean,在 BeanFactory.getBean 的时候会被调用
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) {
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;
}
}
这个 Scope 只是实现了 get 方法。直接通过传入的 objectFactory 创建一个新的 bean。这种 Scope 下每次调用 BeanFactory.getFactory 都会返回一个新的 Bean,自动装载到不同 Bean 的这种 Scope 下的 Bean 也是不同的实例。编写测试:
@Configuration
public static class Config {
@Bean
//自定义 Scope 的名字是 testScope
@org.springframework.context.annotation.Scope(value = "https://www.songbingjia.com/android/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
annotationConfigApplicationContext.getBeanFactory().registerScope("testScope", new TestScope());
//注册我们需要的配置 Bean
annotationConfigApplicationContext.register(Config.class);
//调用 refresh 初始化 ApplicationContext
annotationConfigApplicationContext.refresh();
//获取 Config 这个 Bean
Config config = annotationConfigApplicationContext.getBean(Config.class);
//调用自动装载的 Bean
config.a.test();
//从 BeanFactory 调用 getBean 获取 A
annotationConfigApplicationContext.getBean(A.class).test();
annotationConfigApplicationContext.getBean(A.class).test();
}
执行代码,丛输出上可以看出,这三个 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) {
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;
}
}
在测试代码中,增加调用 destroyScopedBean 销毁 bean:
annotationConfigApplicationContext.getBeanFactory().destroyScopedBean("a");
运行代码,可以看到对应的输出:
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 回调完成 Bean 生命周期。接下来我们尝试实现一种单例的 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 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;
}
}
我们使用两个 ConcurrentHashMap 缓存这个 Scope 下的 Bean,以及对应的 Destroy Callback。在这种实现下,就类似于单例模式的实现了。再使用下面的测试程序测试下:
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 类中注册 Bean 的方法名称为 a,所以 Bean 名称也为 a
annotationConfigApplicationContext.getBeanFactory().destroyScopedBean("a");
config.a.test();
annotationConfigApplicationContext.getBean(A.class).test();
}
我们在销毁 Bean 之前,使用自动装载和 BeanFactory.getBean 分别去请求获取 A 这个 Bean 并调用 test 方法。然后销毁这个 Bean。在这之后,再去使用自动装载的和 BeanFactory.getBean 分别去请求获取 A 这个 Bean 并调用 test 方法。可以从输出中看出, BeanFactory.getBean 请求的是新的 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
@org.springframework.context.annotation.Scope(value = "https://www.songbingjia.com/android/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();
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();
//查看 Bean 实例的类型
System.out.println(config.a.getClass());
System.out.println(annotationConfigApplicationContext.getBean(A.class).getClass());
//这时候我们需要注意,代理 Bean 的名称有所变化,需要通过 ScopedProxyUtils 获取
annotationConfigApplicationContext.getBeanFactory().destroyScopedBean(ScopedProxyUtils.getTargetBeanName("a"));
config.a.test();
annotationConfigApplicationContext.getBean(A.class).test();
}
执行程序,输出为:
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 基本原理如果一个 Bean 没有声明任何 Scope,那么他的 Scope 就会被赋值成 singleton,也就是默认的 Bean 都是单例的。这个对应 BeanFactory 注册 Bean 之前需要生成 Bean 定义,在 Bean 定义的时候会赋上这个默认值,对应源码:
AbstractBeanFactory
protected RootBeanDefinition getMergedBeanDefinition(
String beanName, BeanDefinition bd, @Nullable BeanDefinition containingBd)
throws BeanDefinitionStoreException {
//省略我们不关心的源码
if (!StringUtils.hasLength(mbd.getScope())) {
mbd.setScope(SCOPE_SINGLETON);
}
//省略我们不关心的源码
}
在声明一个 Bean 具有特殊 Scope 之前,我们需要定义这个自定义 Scope 并把它注册到 BeanFactory 中。这个 Scope 名称必须全局唯一,因为之后区分不同 Scope 就是通过这个名字进行区分的。注册 Scope 对应源码:
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");
//不能为 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()) {
logger.trace("Registering scope \'" + scopeName + "\' with implementation [" + scope + "]");
}
}
}
当声明一个 Bean 具有特殊的 Scope 之后,获取这个 Bean 的时候,就会有特殊的逻辑,参考通过 BeanFactory 获取 Bean 的核心源码代码:
AbstractBeanFactory
@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 = mbd.getScope();
//必须有 Scope 名称
if (!StringUtils.hasLength(scopeName)) {
throw new IllegalStateException("No scope name defined for bean ′" + beanName + "\'");
}
//通过 Scope 名称获取对应的 Scope,自定义 Scope 需要手动注册进来
Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name \'" + scopeName + "\'");
}
try {
//调用自定义 Scope 的 get 方法获取 Bean
Object scopedInstance = scope.get(beanName, () ->
{
//同时将创建 Bean 需要的生命周期的回调传入,用于创建 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);
}
}
//省略我们不关心的源码
}
同时,如果我们定义 Scope Bean 的代理方式为 CGLIB,那么在获取 Bean 定义的时候,就会根据原始 Bean 定义创建 Scope 代理的 Bean 定义,对应源码:
ScopedProxyUtils
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
BeanDefinitionRegistry registry, boolean proxyTargetClass) {//原始目标 Bean 名称
String originalBeanName = definition.getBeanName();
//获取原始目标 Bean 定义
BeanDefinition targetDefinition = definition.getBeanDefinition();
//获取代理 Bean 名称
String targetBeanName = getTargetBeanName(originalBeanName);
//创建类型为 ScopedProxyFactoryBean 的 Bean
RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
//根据原始目标 Bean 定义的属性,配置代理 Bean 定义的相关属性,省略这部分源码//根据原始目标 Bean 的自动装载属性,复制到代理 Bean 定义
proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
proxyDefinition.setPrimary(targetDefinition.isPrimary());
if (targetDefinition instanceof AbstractBeanDefinition) {
proxyDefinition.copyQualifiersFrom((AbstractBeanDefinition) targetDefinition);
}//设置原始 Bean 定义为不自动装载并且不为 Primary
//这样通过 BeanFactory 获取 Bean 以及自动装载的都是代理 Bean 而不是原始目标 Bean
targetDefinition.setAutowireCandidate(false);
targetDefinition.setPrimary(false);
//使用新名称注册 Bean
registry.registerBeanDefinition(targetBeanName, targetDefinition);
return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}private static final String TARGET_NAME_PREFIX = "scopedTarget.";
//这个就是获取代理 Bean 名称的工具方法,我们上面 Destroy Bean 的时候也有用到
public static String getTargetBeanName(String originalBeanName) {
return TARGET_NAME_PREFIX + originalBeanName;
}
这个代理 Bean 有啥作用呢?其实主要用处就是每次调用 Bean 的任何方法的时候,都会通过 BeanFactory 获取这个 Bean 进行调用。参考源码:
代理类
ScopedProxyFactoryBean
public class ScopedProxyFactoryBean extends ProxyConfig
implements FactoryBean<
Object>
, BeanFactoryAware, AopInfrastructureBean {
private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource();
//这个就是通过 SimpleBeanTargetSource 生成的实际代理,对于 Bean 的方法调用都会通过这个 proxy 进行调用
private Object proxy;
}
SimpleBeanTargetSource
就是实际的代理源,他的实现非常简单,核心方法就是使用 Bean 名称通过 BeanFactory 获取这个 Bean:public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
@Override
public Object getTarget() throws Exception {
return getBeanFactory().getBean(getTargetBeanName());
}
}
通过 BeanFactory 获取这个 Bean,通过上面源码分析可以知道,对于自定义 Scope 的 Bean 就会调用自定义 Scope 的 get 方法。
然后是 Bean 的销毁,在 BeanFactory 创建这个 Bean 对象的时候,就会调用自定义 Scope 的 registerDestructionCallback 将 Bean 销毁的回调传入:
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()) {
//对于 singleton
registerDisposableBean(beanName, new DisposableBeanAdapter(
bean, beanName, mbd, getBeanPostProcessorCache().destructionAware, acc));
}
else {
//对于自定义 Scope
Scope scope = this.scopes.get(mbd.getScope());
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name \'" + mbd.getScope() + "\'");
}
//调用 registerDestructionCallback
scope.registerDestructionCallback(beanName, new DisposableBeanAdapter(
bean, beanName, mbd, getBeanPostProcessorCache().destructionAware, acc));
}
}
}
在我们想销毁 Scope Bean 的时候,需要调用的是 BeanFactory 的 destroyScopedBean 方法,这个方法会调用自定义 Scope 的 remove:
AbstractBeanFactory
@Override
public void destroyScopedBean(String beanName) {
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
//仅针对自定义 Scope Bean 使用
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 + "\'");
}
//调用自定义 Scope 的 remove 方法
Object bean = scope.remove(beanName);
if (bean != null) {
destroyBean(beanName, bean, mbd);
}
}
【启用 Spring-Cloud-OpenFeign 配置可刷新,项目无法启动,我 TM 人傻了(上)】
文章图片
推荐阅读
- SSM整合配置文件详解
- OpenMLDB(一文了解带参数查询语句(paramterized query statement))
- 启用 Spring-Cloud-OpenFeign 配置可刷新,项目无法启动,我 TM 人傻了(下)
- minikube addons enable ingress 启动错误
- 线程池的各个参数的含义()
- LFS 系列从零开始 DIY Linux 系统(构建 LFS 系统 - Kbd-2.0.2)
- spring-data-redis 上百万的 QPS 压力太大连接失败,我 TM 人傻了
- LFS 系列从零开始 DIY Linux 系统(构建 LFS 系统 - Gperf-3.0.4)
- LFS 系列从零开始 DIY Linux 系统(构建 LFS 系统 - GRUB-2.02~beta2)