SpringCloud学习笔记(二、SpringCloud Config)

目录:

  • 配置中心简介
  • SpringCloud Config的使用
    • SpringCloud Config服务端
    • SpringCloud Config客户端
  • 动态配置属性bean
  • 一些补充(源码分析):Spring事件监听、健康检查health()、高可用的分布式配置中心

配置中心简介:

1、什么是配置中心

从字面意思上来说,配置中心就是管理程序配置的一个公共服务;它管理了系统业务相关的配置内容,在系统启动时去加载这些数据。

2、使用配置中心的好处

  • 统一管理配置的格式,可以更加有效的维护配置
  • 让私密配置更安全(配置不在项目中,而是在配置中心,使生产配置不可见)
  • 。。。。。。

SpringCloud Config的使用

SpringCloud Config服务端:

1、添加maven依赖

1 <dependency>
2     <groupId>org.springframework.cloud</groupId>
3     <artifactId>spring-cloud-config-server</artifactId>
4 </dependency>

2、启动类加上@EnableConfigServer注解

3、配置properties

 1 # 配置中心实例名
 2 spring.application.name=config-server
 3 # 端口
 4 server.port=9090
 5 
 6 ## 远程仓库配置
 7 # git仓库地址
 8 spring.cloud.config.server.git.uri=https://github.com/xxx/xxx
 9 # 本地备份
10 spring.cloud.config.server.git.basedir=git-config
11 
12 ## 本地仓库(也可以不读远端git、svn等,直接读本地配置)
13 # spring.profiles.active=native
14 # spring.cloud.config.server.native.search-locations=file:///E:/SpringCloud Config
15 
16 ## Actuator
17 # 关闭安全校验
18 management.security.enabled=false

 SpringCloud客户端:

1、添加maven依赖

1 <dependency>
2     <groupId>org.springframework.cloud</groupId>
3     <artifactId>spring-cloud-config-client</artifactId>
4 </dependency>

2、配置bootstrap.properties、application.properties

1 ## Config Server配置信息
2 ## 配置中心的服务地址
3 spring.cloud.config.uri=http://localhost:9090/
4 ## Environment 的 application 客户端的应用名称
5 spring.cloud.config.name=test
6 ## spring.profiles.active配置
7 spring.cloud.config.profile=dev
8 ## 配置的版本(git/svn 分支)
9 spring.cloud.config.label=master
1 ## 客户端的实例名
2 spring.application.name=config-client
3 ## 客户端提供端口
4 server.port=8080

动态刷新配置:

首先我们知道SpringCloud Config分为服务端和客户端,服务端用于拉取远端的配置客户端用于拉取服务端配置以供应用使用,那么一次刷新配置的过程应该有以下几点:

1、服务端拉取最新的git配置(只要获取一次数据就会拉取远端的数据)

2、客户端拉取服务端的配置(通过调用public java.lang.Object org.springframework.cloud.endpoint.GenericPostableMvcEndpoint.invoke()方法让客户端获取服务端最新的配置)

 1 public class GenericPostableMvcEndpoint extends EndpointMvcAdapter {
 2 
 3     public GenericPostableMvcEndpoint(Endpoint<?> delegate) {
 4         super(delegate);
 5     }
 6 
 7     @RequestMapping(method = RequestMethod.POST)
 8     @ResponseBody
 9     @Override
10     public Object invoke() {
11         if (!getDelegate().isEnabled()) {
12             return new ResponseEntity<>(Collections.singletonMap(
13                     "message", "This endpoint is disabled"), HttpStatus.NOT_FOUND);
14         }
15         return super.invoke();
16     }
17 
18 }

根据我们上面的定义可知客户端的port=8080,也就是只需要POST调用http://localhost:8080/refresh即可在客户端拉取服务端最新的配置。

3、刷新bean(此时我们会发现尽管客户端的数据已经刷新了,但为什么我们配置的数据还是旧值呢,原因是spring已经将这个bean注入了,所以我们也需要刷新这个bean:@RefreshScope)

——————————————————————————————————————————————————————————————————————

说到这里我们就会有一个疑问,难道我们每次刷新客户端的配置都需要手动调用这个invoke()方法???

答案是否定的,方法有很多种,其中最简单的就是写一个定时器,来定期的调用invoke()方法(当然你也可以定期的调用localhost:8080/refresh)。

知己知彼方能百战不殆嘛,所以我们还是要知道invoke()是如何实现滴。

1、我们看public java.lang.Object org.springframework.cloud.endpoint.GenericPostableMvcEndpoint.invoke(),得知invoke调用父类的invoke,我们再看下父类的invoke;

 1 @RequestMapping(method = RequestMethod.POST)
 2 @ResponseBody
 3 @Override
 4 public Object invoke() {
 5     if (!getDelegate().isEnabled()) {
 6         return new ResponseEntity<>(Collections.singletonMap(
 7                 "message", "This endpoint is disabled"), HttpStatus.NOT_FOUND);
 8     }
 9     return super.invoke();
10 }

2、父类invoke >>> public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()调用其父类invoke;

1 @Override
2 @ActuatorGetMapping
3 @ResponseBody
4 public Object invoke() {
5     return super.invoke();
6 }

3、protected java.lang.Object org.springframework.boot.actuate.endpoint.mvc.AbstractEndpointMvcAdapter.invoke()调用一个对象的invoke,而这个对象时通过构造方法传进来的;我们在一步步看构造方法的delegate是从哪来的

 1 private final E delegate;
 2 
 3 public AbstractEndpointMvcAdapter(E delegate) {
 4     Assert.notNull(delegate, "Delegate must not be null");
 5     this.delegate = delegate;
 6 }
 7 
 8 protected Object invoke() {
 9     if (!this.delegate.isEnabled()) {
10         // Shouldn't happen - shouldn't be registered when delegate's disabled
11         return getDisabledResponse();
12     }
13     return this.delegate.invoke();
14 }

 4、通过一步步网上看,发现是最开始的public java.lang.Object org.springframework.cloud.endpoint.GenericPostableMvcEndpoint,然后我们看看哪里有调用GenericPostableMvcEndpoint的构造

 

从图中我们可以看出有三处调用,而因为我们是刷新操作,我们就大胆的猜测是第一个refreshMvcEndpoint,我们在点进去看看。

发现入参是org.springframework.cloud.endpoint.RefreshEndpoint,所以我们只要看看RefreshEndpoint是怎样实现的就可以了。

 1 @ConfigurationProperties(prefix = "endpoints.refresh", ignoreUnknownFields = false)
 2 @ManagedResource
 3 public class RefreshEndpoint extends AbstractEndpoint<Collection<String>> {
 4 
 5     private ContextRefresher contextRefresher;
 6 
 7     public RefreshEndpoint(ContextRefresher contextRefresher) {
 8         super("refresh");
 9         this.contextRefresher = contextRefresher;
10     }
11 
12     @ManagedOperation
13     public String[] refresh() {
14         Set<String> keys = contextRefresher.refresh();
15         return keys.toArray(new String[keys.size()]);
16     }
17 
18     @Override
19     public Collection<String> invoke() {
20         return Arrays.asList(refresh());
21     }
22 
23 }

从中的我们可以看出刷新原来调用的就是RefreshEndpoint的invoke方法,而Set<String> keys = contextRefresher.refresh()便是刷新时所执行的函数!!!!!!∑(゚Д゚ノ)ノ

综上所述:我们的job中调用Set<String> keys = contextRefresher.refresh()这段代码即可刷新配置啦!!!

你之前不是才说调用public java.lang.Object org.springframework.cloud.endpoint.GenericPostableMvcEndpoint.invoke()嘛,咋又说只要调用contextRefresher.refresh()了呢。

哈哈,因为GenericPostableMvcEndpoint.invoke()的底层是contextRefresher.refresh(),而直接调用的效率当然比代理转发后的要高,所以才会建议直接调用contextRefresher.refresh()会好些。

——————————————————————————————————————————————————————————————————————

至此我们已经了解到了SpringCloud Config可以通过job定期调用contextRefresher.refresh()来实现定期刷新配置(SpringCloud Bus可以通过MQ的方式来实现),那这个刷新操作到底做了什么呢,我们来进一步的挖掘它。

 1 public synchronized Set<String> refresh() {
 2     // 步骤1
 3     Map<String, Object> before = extract(
 4             this.context.getEnvironment().getPropertySources());
 5     // 步骤2
 6     addConfigFilesToEnvironment();
 7     // 步骤4
 8     Set<String> keys = changes(before,
 9             // 步骤3
10             extract(this.context.getEnvironment().getPropertySources())).keySet();
11     // 步骤5
12     this.context.publishEvent(new EnvironmentChangeEvent(context, keys));
13     // 步骤6
14     this.scope.refreshAll();
15     return keys;
16 }

通过大致的分析refresh可分为6大步骤。

1、提取标准参数之外的数据源,非SYSTEM、JNDI、SERVLET。

 1 private Map<String, Object> extract(MutablePropertySources propertySources) {
 2     Map<String, Object> result = new HashMap<String, Object>();
 3     List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>();
 4     for (PropertySource<?> source : propertySources) {
 5         sources.add(0, source);
 6     }
 7     for (PropertySource<?> source : sources) {
 8         // 只保留标准数据源之外的数据源:standardSources
 9         if (!this.standardSources.contains(source.getName())) {
10             extract(source, result);
11         }
12     }
13     return result;
14 }
15 
16 private Set<String> standardSources = new HashSet<>(
17         Arrays.asList(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
18                 StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
19                 StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME,
20                 StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME,
21                 StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));

2、把原来的Environment里的参数放到一个新建的Spring Context容器下重新加载,完事之后关闭新容器。

3、同1

4、只拿到更新过的配置key值。

5、发布环境变更事件(Spring事件监听模型)。

6、使用刚更新的环境参数重新生成Bean(刷新bean的属性值)。

上面的主要步骤便是2和6,即拿到最新的配置后通过事件监听模型,把我的配置已经更新这个消息告诉配置变更监听器

Spring事件监听模型:

上面一节我们说到了SpringCloud Config的客户端配置刷新是基于Spring事件监听模型来实现的,那么这里就来简单的聊一聊。

Spring的事件监听模式的实质就是观察者模式,其主要分为两个类:

  • ApplicationListener(监听器)>>> public interface ApplicationListener<E extends ApplicationEvent> extends EventListener
  • ApplicationEvent(监听对象)>>> public abstract class ApplicationEvent extends EventObject

自定义监听器的使用:

 1 public class SpringEventDemo {
 2 
 3     public static void main(String[] args) {
 4         AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
 5         context.addApplicationListener(event -> System.err.println("监听到事件" + event.getSource()));
 6 
 7         context.refresh();
 8         context.publishEvent(new MyApplicationEvent("event1"));
 9         context.publishEvent(new MyApplicationEvent("event2"));
10     }
11     
12 }
13 
14 class MyApplicationEvent extends ApplicationEvent {
15 
16     public MyApplicationEvent(Object source) {
17         super(source);
18     }
19     
20 }

上述代码的第7行为啥要额外调用refresh方法呢,因为在Spring容器初始化的时候就已经将内部的监听器给初始化完毕了,如果你还想增加自定义的监听器就需要手动调用容器刷新方法,让Spring容器将你新加的放入到自己的监听器集合中(后续说源码的时候你就会明白了)。

——————————————————————————————————————————————————————————————————————

接下来我们来聊聊Spring事件监听模型的实现原理:

上面说到事件监听模型的实质是观察者模式,那么这里肯定就会有两个角色和一个事件源;一个观察者,一个被观察者,一个事件源。

按照上面的示例,观察者是监听事件是否发生的监听器,也就是ApplicationListener;被观察是发布事件的事件发布者ApplicationEvent;事件源则是监听器监听的对象,即ApplicationEvent。

——————————————————————————————————————————————————————————————————————

监听器实现:

 1 @Override
 2 public void addApplicationListener(ApplicationListener<?> listener) {
 3     Assert.notNull(listener, "ApplicationListener must not be null");
 4     if (this.applicationEventMulticaster != null) {
 5         this.applicationEventMulticaster.addApplicationListener(listener);
 6     }
 7     else {
 8         this.applicationListeners.add(listener);
 9     }
10 }

首先我们看下添加一个监听器是需要传入一个ApplicationListener,而根据ApplicationListener的源码可知,传入的事件源必须是继承与ApplicationEvent。

且我们根据ApplicationListener的类定义可得出其时基于JDK内置的java.util.EventListener实现,原理很简单不做赘述。

而添加一个监听器也仅是将传入的ApplicationListener添加到new LinkedHashSet<ApplicationListener<?>>()而已。

事件发布者实现:

1 @Override
2 public void publishEvent(ApplicationEvent event) {
3     publishEvent(event, null);
4 }

事件发布者,接受一个事件源,并调用监听器的监听方法。

 1 protected void publishEvent(Object event, ResolvableType eventType) {
 2     Assert.notNull(event, "Event must not be null");
 3     if (logger.isTraceEnabled()) {
 4         logger.trace("Publishing event in " + getDisplayName() + ": " + event);
 5     }
 6 
 7     // Decorate event as an ApplicationEvent if necessary
 8     ApplicationEvent applicationEvent;
 9     if (event instanceof ApplicationEvent) {
10         applicationEvent = (ApplicationEvent) event;
11     }
12     else {
13         applicationEvent = new PayloadApplicationEvent<Object>(this, event);
14         if (eventType == null) {
15             eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
16         }
17     }
18 
19     // Multicast right now if possible - or lazily once the multicaster is initialized
20     if (this.earlyApplicationEvents != null) {
21         this.earlyApplicationEvents.add(applicationEvent);
22     }
23     else {
24         getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
25     }
26 
27     // Publish event via parent context as well...
28     if (this.parent != null) {
29         if (this.parent instanceof AbstractApplicationContext) {
30             ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
31         }
32         else {
33             this.parent.publishEvent(event);
34         }
35     }
36 }
 1 @Override
 2 public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
 3     ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
 4     // 拿到相应的监听对象,分别调用invokeListener方法
 5     for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
 6         Executor executor = getTaskExecutor();
 7         if (executor != null) {
 8             executor.execute(new Runnable() {
 9                 @Override
10                 public void run() {
11                     invokeListener(listener, event);
12                 }
13             });
14         }
15         else {
16             invokeListener(listener, event);
17         }
18     }
19 }
20 
21 protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
22     ErrorHandler errorHandler = getErrorHandler();
23     if (errorHandler != null) {
24         try {
25             // 执行监听方法
26             doInvokeListener(listener, event);
27         }
28         catch (Throwable err) {
29             errorHandler.handleError(err);
30         }
31     }
32     else {
33         doInvokeListener(listener, event);
34     }
35 }
36 
37 @SuppressWarnings({"unchecked", "rawtypes"})
38 private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
39     try {
40         // 调用监听器的监听方法
41         listener.onApplicationEvent(event);
42     }
43     catch (ClassCastException ex) {
44         String msg = ex.getMessage();
45         if (msg == null || matchesClassCastMessage(msg, event.getClass())) {
46             // Possibly a lambda-defined listener which we could not resolve the generic event type for
47             // -> let's suppress the exception and just log a debug message.
48             Log logger = LogFactory.getLog(getClass());
49             if (logger.isDebugEnabled()) {
50                 logger.debug("Non-matching event type for listener: " + listener, ex);
51             }
52         }
53         else {
54             throw ex;
55         }
56     }
57 }

综上,发布事件就是取出相应的监听器,并分别调用他们的监听方法。

事件源实现:

ApplicationEvent更为简单,基于JDK的java.util.EventObject实现,源码很容易理解,我简单说说。

 1 public class EventObject implements java.io.Serializable {
 2 
 3     private static final long serialVersionUID = 5516075349620653480L;
 4 
 5     /**
 6      * 事件源
 7      */
 8     protected transient Object  source;
 9 
10     /**
11      * 仅一个构造,也就是必传非空的事件源
12      */
13     public EventObject(Object source) {
14         if (source == null)
15             throw new IllegalArgumentException("null source");
16 
17         this.source = source;
18     }
19 
20     /**
21      * 获取事件源对象
22      */
23     public Object getSource() {
24         return source;
25     }
26 
27     /**
28      * Returns a String representation of this EventObject.
29      *
30      * @return  A a String representation of this EventObject.
31      */
32     public String toString() {
33         return getClass().getName() + "[source=" + source + "]";
34     }
35 }

而Spring提供的事件源ApplicationEvent也仅是在此基础上增加了事件创建的时间戳而已。

 1 public abstract class ApplicationEvent extends EventObject {
 2 
 3     /** use serialVersionUID from Spring 1.2 for interoperability */
 4     private static final long serialVersionUID = 7099057708183571937L;
 5 
 6     /** System time when the event happened */
 7     private final long timestamp;
 8 
 9 
10     /**
11      * Create a new ApplicationEvent.
12      * @param source the object on which the event initially occurred (never {@code null})
13      */
14     public ApplicationEvent(Object source) {
15         super(source);
16         this.timestamp = System.currentTimeMillis();
17     }
18 
19 
20     /**
21      * Return the system time in milliseconds when the event happened.
22      */
23     public final long getTimestamp() {
24         return this.timestamp;
25     }
26 
27 }

健康检查health():

健康检查health,主要作用是对当前系统的运行态进行展示的一个接口,我们可以定制自己系统的健康检查,也可以使用已提供好的。

如果我们要实现自己的健康检查,那应该如何实现呢,我们先看下health()的源码。

1、我们从启动日志中可以看到health()接口是调用:

public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(javax.servlet.http.HttpServletRequest,java.security.Principal)方法

我们跟进去看看

 1 @ActuatorGetMapping
 2 @ResponseBody
 3 public Object invoke(HttpServletRequest request, Principal principal) {
 4     if (!getDelegate().isEnabled()) {
 5         // Shouldn't happen because the request mapping should not be registered
 6         return getDisabledResponse();
 7     }
 8     Health health = getHealth(request, principal);
 9     HttpStatus status = getStatus(health);
10     if (status != null) {
11         return new ResponseEntity<Health>(health, status);
12     }
13     return health;
14 }

2、从上述代码中可以看出invoke的主要逻辑在第8行,我们继续跟进

1 private Health getHealth(HttpServletRequest request, Principal principal) {
2     Health currentHealth = getCurrentHealth();
3     if (exposeHealthDetails(request, principal)) {
4         return currentHealth;
5     }
6     return Health.status(currentHealth.getStatus()).build();
7 }

同理主要代码是第2行,继续跟进

 1 private Health getCurrentHealth() {
 2     long accessTime = System.currentTimeMillis();
 3     CachedHealth cached = this.cachedHealth;
 4     if (cached == null || cached.isStale(accessTime, getDelegate().getTimeToLive())) {
 5         Health health = getDelegate().invoke();
 6         this.cachedHealth = new CachedHealth(health, accessTime);
 7         return health;
 8     }
 9     return cached.getHealth();
10 }

3、从第5行可以看出,又是代理类org.springframework.boot.actuate.endpoint.mvc.AbstractEndpointMvcAdapter的invoke方法 !!!∑(゚Д゚ノ)ノ

那我们看看AbstractEndpointMvcAdapter的实现

从中可以肯定实现类是第二个HealthMvcEndpoint

4、所以只能看HealthMvcEndpoint的invoke方法咯 ψ(*`ー´)ψ

1 @Override
2 public Health invoke() {
3     return this.healthIndicator.health();
4 }

这就很简单了,仅一行代码;继续跟进后可以得知healthIndicator是一个接口,然后我们找到这个接口是在构造函数中初始化的,我们来看看初始化的对象是谁

 1 public HealthEndpoint(HealthAggregator healthAggregator, Map<String, HealthIndicator> healthIndicators) {
 2     super("health", false);
 3     Assert.notNull(healthAggregator, "HealthAggregator must not be null");
 4     Assert.notNull(healthIndicators, "HealthIndicators must not be null");
 5     CompositeHealthIndicator healthIndicator = new CompositeHealthIndicator(
 6             healthAggregator);
 7     for (Map.Entry<String, HealthIndicator> entry : healthIndicators.entrySet()) {
 8         healthIndicator.addHealthIndicator(getKey(entry.getKey()), entry.getValue());
 9     }
10     this.healthIndicator = healthIndicator;
11 }

哈哈,原来healthIndicator就是CompositeHealthIndicator healthIndicator = new CompositeHealthIndicator(healthAggregator);

所以this.healthIndicator.health()就是CompositeHealthIndicator的health了,来来来我们看看CompositeHealthIndicator的health

1 @Override
2 public Health health() {
3     Map<String, Health> healths = new LinkedHashMap<String, Health>();
4     for (Map.Entry<String, HealthIndicator> entry : this.indicators.entrySet()) {
5         healths.put(entry.getKey(), entry.getValue().health());
6     }
7     return this.healthAggregator.aggregate(healths);
8 }

5、从代码中我们可以看出CompositeHealthIndicator的health就是从this.indicators拿出所有的HealthIndicator,并调用其health()方法

而HealthIndicator是一个接口,所以我们可以通过实现HealthIndicator接口进行自定义的health()健康检查

但实际上我们并不需要再去包装一层,springboot已经实现了一个org.springframework.boot.actuate.health.AbstractHealthIndicator,所以我们实现AbstractHealthIndicator,并重写doHealthCheck方法就可以了

综上所述:如果我们要实现自己的健康检查,只需要重写AbstractHealthIndicator的doHealthCheck方法就可以了

1 public class MyHealthIndicator extends AbstractHealthIndicator {
2     @Override
3     protected void doHealthCheck(Health.Builder builder) throws Exception {
4         System.out.println("自定义健康检查 MyHealthIndicator");
5         builder.down().withDetail("This is MyHealthIndicator", "just so so!");
6     }
7 }

注意:需要将MyHealthIndicator注入成一个bean哦 (✪ω✪)

高可用的分布式配置中心:

1、传统模式

因为Config Server是无状态的,配置始终还是要从git取,所以传统模式只要保证Config Server稳定就行,也就是配置Config Server集群,然后通过负载均衡器实现高可用。

2、服务模式:将Config Server注册到eureka,因为Config Server在整个SpringCloud体系中就是一个基础服务的节点,所以把它当成一个节点时就可以注册到注册中心来实现高可用

posted @ 2019-09-30 21:20  被猪附身的人  阅读(574)  评论(0编辑  收藏  举报