Springboot整合acm动态配置

一、背景

当前项目基于springBoot, 且配置中心使用的阿里云acm,但是配置中心暂未整合动态配置。

二、思路

首先整理了以下几点切入点:

  1. spring framework有一个@RefreshScope注解,用来做配置刷新。
  2. 阿里acm官方有文档帮助整合。
  3. 看看acm控制台都有哪些功能。
  4. 了解spring-actuator。

根据以上几个切入点开始整合。

三、接入过程

1. 问题

按照文档 的描述,只需要添加maven依赖,然后直接使用@RefreshScope就可以了。但是实际这样存在问题。

问题1: 在acm控制后台,只能查到推送轨迹,而没有监听查询记录。 问题2: 在acm控制后台修改后,client端配置并没有更新。

❓推测1: 我的应用部署在容器里,之后暴露端口到机器上,机器上有一层nginx代理,再上面还有一层LBS。会不会是acm server推过来压根就失败了,只是推送轨迹显示推送成功。

❓推测2:会不会是我的监听器注册失败,所以在acm server上没有监听查询记录。

2. 解决过程

1⃣️首先看一下spring-cloud-starter-acm里都有什么。

2⃣️里面直接就是starter相关的类。找到AcmAutoConfiguration类,看看里面都有什么。

3⃣️看一下这几个类的源码,果然在AcmContextRefresher类里看到了registerDiamondListener()方法,这个就是注册监听器的核心逻辑了。

4⃣️debug AcmContextRefresher类,看到当registerDiamondListenersForApplications里的acmPropertySourceRepository.getAll() size为0时,不会去调用registerDiamondListener()

5⃣️点进去debug一下,可以看到这里的逻辑是找到一个AcmPropertySource类型的源参数类后放到list,之后根据这个list来迭代,注册listener。现在的问题是找不到AcmPropertySource类型的类。

6⃣️acm阿里官网文档有说:

访问 ip+端口/actuator/acm 访问acm的Endpoint。

{
    "runtime": {
        "sources": [
            {
                "lastSynced": "",
                "dataId": ""
            }
        ],
        "refreshHistory": [
        ]
    },
    "config": {
        "group": "group",
        "timeOut": 3000,
        "endpoint": "acm.aliyun.com",
        "namespace": "xxxxx",
        "accessKey": "xxx",
        "secretKey": "xxxx",
        "ramRoleName": null,
        "fileExtension": "yaml"
    }
}

我的访问结果dataId、lastSynced、refreshHistory都没有值,而acm阿里官网是有值的。

阿里官网的acm长这样:

{
  "runtime": {
    "sources": [
      {
        "dataId": "com.alibaba.cloud.acm:sample-app.properties",
        "lastSynced": "2017-10-10 10:46:27"
      }
    ],
    "refreshHistory": [
      {
        "timestamp": "2017-10-10 10:46:24",
        "dataId": "com.alibaba.cloud.acm:sample-app.properties",
        "md5": "8692ae986ec7bc345b3f0f4de602ff13"
      }
    ]
  },
  "config": {
    "group": "DEFAULT_GROUP",
    "timeOut": 3000,
    "endpoint": "xxx",
    "namespace": "xxx",
    "accessKey": "xxx",
    "secretKey": "xxx"
  }
}

仔细观察官网的结构,发现dataId跟我在acm控制台设置的dataId长的比较类似。脑瓜子一闪,这里的dataId是不是就是acm控制台配置的dataId。

7⃣️再次debug刚刚的acmPropertySourceRepository.getAll(),仔细看结果发现了dataId。 此时的PropertySource并不是CompositePropertySource,而是BootstrapPropertySource,所以这个result为空.

8⃣️明白以上之后,我直接重写掉这部分逻辑是不是就可以了。

3.重写方案

1⃣️重写AcmPropertySourceRepository

public class AcmCustomPropertySourceRepository extends AcmPropertySourceRepository {

    private final ApplicationContext applicationContext;

    public AcmCustomPropertySourceRepository(ApplicationContext applicationContext) {
        super(applicationContext);
        this.applicationContext = applicationContext;
    }

    public List<AcmPropertySource> getAll() {
        List<AcmPropertySource> result = new ArrayList<>();
        ConfigurableApplicationContext ctx = (ConfigurableApplicationContext)applicationContext;
        for (PropertySource p : ctx.getEnvironment().getPropertySources()) {
            if (p instanceof AcmPropertySource) {
                result.add((AcmPropertySource)p);
            } else if (p instanceof BootstrapPropertySource) {
                BootstrapPropertySource bps = (BootstrapPropertySource)p;
                PropertySource propertySource = bps.getDelegate();
                if (propertySource instanceof AcmPropertySource) {
                    result.add((AcmPropertySource)propertySource);
                }

            }
        }
        return result;
    }

}

2⃣️配置类

@Configuration
public class AcmCustomConfiguration implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Bean("acmCustomPropertySourceRepository")
    @Primary
    public AcmPropertySourceRepository acmPropertySourceRepository() {
        return new AcmCustomPropertySourceRepository(applicationContext);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

}

debug看一下,这时候的result有值,并且listner成功注册。

校验:

1.再次访问: ip+端口/actuator/acm

{
    "runtime": {
        "sources": [
            {
                "lastSynced": "2020-08-05 17:35:02",
                "dataId": "com.xxxxtest2.yaml"
            }
        ],
        "refreshHistory": [
            {
                "timestamp": "2020-08-05 17:39:38",
                "dataId": "com.xxxxxtest2.yaml",
                "md5": "20f6572e9f097d8cbf075f83eabe958e"
            }
        ]
    },
    "config": {
        "group": "group",
        "timeOut": 3000,
        "endpoint": "acm.aliyun.com",
        "namespace": "xxxx",
        "accessKey": "xxxx",
        "secretKey": "xxxx",
        "ramRoleName": null,
        "fileExtension": "yaml"
    }
}

2.在acm控制台修改配置, 已经看到了正确的监听记录。

四、使用

1. 使用@Value()

当使用@Value读取配置文件,且需要配置文件热更新时,需要在类上添加@RefreshScope注解。

例如:

@RequestMapping("/test")
@RestController
@RefreshScope
public class TestController {
   @Value("${shareLink.joinUrl}")
    private String str;
    @GetMapping("/test")
    public String test(){
        return str;
    }
}

2.使用@Bean

当使用@Bean读取一个配置的properties class,且需要配置的properties class热更新时,需要在@Bean的注解下面添加@RefreshScope注解。

例如:

@Getter
@Setter
@ConfigurationProperties(prefix = "aliyun.oss")
public class OssProperties {
    private String endpoint;
    private String regionId;
    private String region;
    private String accessKeyId;
    private String accessKeySecret;
    private String stsRoleArn;
    private String bucket;
    private String userId;
}
@Configuration
@EnableConfigurationProperties(OssProperties.class)
public class OssAutoConfiguration {

    @Bean
    @RefreshScope
    public OSSClient ossClient(OssProperties ossProperties) {
        CredentialsProvider credentialsProvider = new DefaultCredentialProvider(ossProperties.getAccessKeyId(),
                ossProperties.getAccessKeySecret());
        ClientConfiguration config = new ClientConfiguration();
        return new OSSClient(ossProperties.getEndpoint(), credentialsProvider, config);
    }
}

以上,搞定。