zoukankan      html  css  js  c++  java
  • 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-server
    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的事件监听模式的实质就是观察者模式,其主要分为两个类

    1、ApplicationListener(监听器)>>> public interface ApplicationListener<E extends ApplicationEvent> extends EventListener

    2、ApplicationEvent(监听对象)>>> public abstract class ApplicationEvent extends EventObject

    3、自定义监听器

     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 class MyApplicationEvent extends ApplicationEvent {
    14 
    15     public MyApplicationEvent(Object source) {
    16         super(source);
    17     }
    18 }

    健康检查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集群,然后通过负载均衡器实现高可用

    2、服务模式:将config server注册到eureka

  • 相关阅读:
    jenkins+svn+Ant+tomcat+非maven项目构建
    阿里云服务器CentOS6.9安装jenkins
    jenkins构建项目时报错缺少com.sun.image.codec.jpeg包解决方案
    不间断会话服务:screen
    数据库备份与还原:mysqldump,source
    多表更新:update,join
    删除数据高级用法:delete,truncate
    远程传输文件命令:scp
    远程控制服务:配置sshd服务,密钥验证登陆
    绑定多块网卡:负载均衡
  • 原文地址:https://www.cnblogs.com/bzfsdr/p/11589902.html
Copyright © 2011-2022 走看看