本篇文章涉及底层设计以及原理,以及问题定位,比较深入,篇幅较长,所以拆分成上下两篇:

  • :问题简单描述以及 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 中并使用动态刷新的机制进行刷新。官方提供了这个配置方法,参考:官方文档 - 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 = "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 = "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 是如何实现这些的呢?我们接下来简单分析下源码

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

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

启用 Spring-Cloud-OpenFeign 配置可刷新,项目无法启动,我 TM 人傻了(上)的更多相关文章

  1. 微服务生态组件之Spring Cloud OpenFeign详解和源码分析

    Spring Cloud OpenFeign 概述 Spring Cloud OpenFeign 官网地址 https://spring.io/projects/spring-cloud-openfe ...

  2. Spring Cloud Config 配置刷新

    客户端进行刷新操作. 1.添加 actuator包,这样 /refresh url才处于可用状态. <dependency> <groupId>org.springframew ...

  3. Spring Cloud Config 配置中心实践过程中,你需要了解这些细节!

    本文导读: Spring Cloud Config 基本概念 Spring Cloud Config 客户端加载流程 Spring Cloud Config 基于消息总线配置 Spring Cloud ...

  4. 跟我学SpringCloud | 第七篇:Spring Cloud Config 配置中心高可用和refresh

    SpringCloud系列教程 | 第七篇:Spring Cloud Config 配置中心高可用和refresh Springboot: 2.1.6.RELEASE SpringCloud: Gre ...

  5. Spring Cloud OpenFeign使用教程

    文章目录 Spring Cloud OpenFeign Demo 怎么配置OpenFeignServer 怎么配置OpenFeignClient 多个参数传递问题 FeignClient的日志问题 多 ...

  6. SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(1)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务 ...

  7. spring cloud config配置记录

    1. spring cloud config配置记录 1.1. pom <!-- 分布式配置中心 --> <dependency> <groupId>org.spr ...

  8. 微服务SpringCloud之Spring Cloud Config配置中心Git

    微服务以单个接口为颗粒度,一个接口可能就是一个项目,如果每个项目都包含一个配置文件,一个系统可能有几十或上百个小项目组成,那配置文件也会有好多,对后续修改维护也是比较麻烦,就和前面的服务注册一样,服务 ...

  9. 微服务SpringCloud之Spring Cloud Config配置中心服务化

    在前面两篇Spring Cloud Config配置中心的博客中都是需要指定配置服务的地址url:spring.cloud.config.uri,客户端都是直接调用配置中心的server端来获取配置文 ...

  10. 通过消息总线Spring Cloud Bus实现配置文件刷新(使用Kafka或RocketMQ)

    如果需要客户端获取到最新的配置信息需要执行refresh,我们可以利用webhook的机制每次提交代码发送请求来刷新客户端,当客户端越来越多的时候,需要每个客户端都执行一遍,这种方案就不太适合了.使用 ...

随机推荐

  1. 安全|常见的Web攻击手段之CSRF攻击

    对于常规的Web攻击手段,如XSS.CRSF.SQL注入.(常规的不包括文件上传漏洞.DDoS攻击)等,防范措施相对来说比较容易,对症下药即可,比如XSS的防范需要转义掉输入的尖括号,防止CRSF攻击 ...

  2. Mysql慢查询explain

    转自:https://www.toutiao.com/i6776461352522220036/?tt_from=weixin&utm_campaign=client_share&wx ...

  3. 在Spring中使用静态工厂时发生的无法得到对象的问题

    因为我测试时,使用的是之前的包Spring 3.x一类的jar包,发现我的代码毫无问题,但是就是运行不出来,后面去Spring官网上发现,JDK6+至少都要使用Spring4.x了,而为了配合JDK8 ...

  4. Java程序设计学习笔记(六) — 网络编程

    时间:2016-5-8 02:03 --网络编程        网络传输实际上就是进行数据传输.    一.传输的步骤:        1.对对方IP地址进行定位.        2.将数据打包发送到 ...

  5. VS code快速创建vue模板

    忘记了.vue文件的格式或者不想手动敲那段模板代码怎么办?VS code快速创建vue模板帮你偷个小懒 第一步:新建模板并保存 打开 VS code,依次点击 file > Preference ...

  6. Learning ROS: Managing System dependencies

    Download and install the system dependencies for turtlesim: roscd turtlesim cat package.xml rosdep i ...

  7. 新版idea无法导入mavenweb模板

    目前没有任何办法,最好是下个旧版的

  8. viper配置管理

    安装 go get github.com/spf13/viper viper支持的功能 1.可以设置默认值 2.可以加载多种格式的配置文件,如JSON,TOML,YAML,HCL和Java属性配置文件 ...

  9. NOIP模拟50

    过分的神圣,往往比恶魔更加恶质. 前言 最大的一个收获就是不要动不动就码线段树,一定要审清楚题目之后再码!! T1 一开始理解错题了,以为答案是就是 \(\dfrac{\operatorname{le ...

  10. 动环监控系统中B接口的实现

    动环监控系统简述 1.术语介绍 1.1 省集中监控中心-Province Supervision Center(PSC) 面向多FSU管理的高级监控层次,即省集中监控中心,通过开放的数据协议,连接监控 ...