基于dataid为yaml的文件扩展配置
spring-cloud-starter-alibaba-nacos-confifig默认支持的文件格式是properties, 如果我们想用其他格式的文件,可以只需要完成以下两步:
1、在应用的 bootstrap.properties 配置文件中显示的声明 dataid 文件扩展名。如下所示bootstrap.properties
spring.cloud.nacos.config.file-extension=yaml
2、在Nacos控制台,修改配置文件的类型,改成yml
针对profifile粒度配置
spring-cloud-starter-alibaba-nacos-confifig 在加载配置的时候,不仅仅加载了以 dataid 为${spring.application.name}.${file-extension:properties} 为前缀的基础配置,还加载了dataid为 ${spring.application.name}-${profile}.${file-extension:properties} 的基础配置。在日常开发中如果遇到多套环境下的不同配置,可以通过Spring 提供的${spring.profiles.active} 这个配置项来配置。
- 在bootstrap.properties中添加profifile
spring.profiles.active=develop
- Nacos 上新增一个dataid为:nacos-confifig-develop.yaml的基础配置,如下所示:
Data ID: nacos-dubbo-provider-develop.yaml
Group : DEFAULT_GROUP
配置格式: YAML
配置内容: current.env: develop-env
如果需要切换到生产环境,只需要更改 ${spring.profiles.active} 参数配置即可。如下所示:
spring.profiles.active=product
Nacos 中的Namespace和Group
在nacos中提供了namespace和group命名空间和分组的机制。,它是Nacos提供的一种数据模型,也就是我们要去定位到一个配置,需要基于namespace- > group ->dataid来实现。namespace可以解决多环境以及多租户数据的隔离问题。比如在多套环境下,可以根据指定环境创建不同的namespace,实现多环境隔离。或者在多租户的场景中,每个用户可以维护自己的namespace,实现每个用户的配置数据和注册数据的隔离。group是分组机制,它的纬度是实现服务注册信息或者DataId的分组管理机制,对于group的用法,没有固定的规则,它也可以实现不同环境下的分组,也可以实现同一个应用下不同配置类型或者不同业务类型的分组。
官方建议是,namespace用来区分不同环境,group可以专注在业务层面的数据分组。实际上在使用过程中,最重要的是提前定要统一的口径和规定,避免不同的项目团队混用导致后期维护混乱的问题。
自定义namespace
在没有明确指定 ${spring.cloud.nacos.config.namespace} 配置的情况下, 默认使用的是 Nacos上 Public 这个namespae。如果需要使用自定义的命名空间,可以通过以下配置来实现:
spring.cloud.nacos.config.namespace=b3404bc0-d7dc-4855-b519-570ed34b62d7
该配置必须放在 bootstrap.properties 文件中。此外spring.cloud.nacos.config.namespace 的值是 namespace 对应的 id,id 值可以在 Nacos的控制台获取。
并且在添加配置时注意不要选择其他的 namespae,否则将会导致读取不到正确的配置。
自定义group
在没有明确指定 ${spring.cloud.nacos.config.group} 配置的情况下, 默认使用的是DEFAULT_GROUP 。如果需要自定义自己的 Group,可以通过以下配置来实现:
spring.cloud.nacos.config.group=DEVELOP_GROUP
该配置必须放在 bootstrap.properties 文件中。并且在添加配置时 Group 的值一定要和spring.cloud.nacos.config.group 的配置值一致
自定义扩展的DataId
Spring Cloud Alibaba Nacos Confifig 从 0.2.1 版本后,可支持自定义 Data Id 的配置。关于这部分详细的设计可参考
这里。 一个完整的配置案例如下所示:
spring.application.name=opensource-service-provider
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
# config external configuration
# 1、Data Id 在默认的组 DEFAULT_GROUP,不支持配置的动态刷新
spring.cloud.nacos.config.extension-configs[0].data-id=ext-config-common01.properties
# 2、Data Id 不在默认的组,不支持动态刷新
spring.cloud.nacos.config.extension-configs[1].data-id=ext-config-common02.properties
spring.cloud.nacos.config.extension-configs[1].group=GLOBALE_GROUP
# 3、Data Id 既不在默认的组,也支持动态刷新
spring.cloud.nacos.config.extension-configs[2].data-id=ext-config-common03.properties
spring.cloud.nacos.config.extension-configs[2].group=REFRESH_GROUP
spring.cloud.nacos.config.extension-configs[2].refresh=true
可以看到:
- 通过 spring.cloud.nacos.config.extension-configs[n].data-id 的配置方式来支持多个Data Id 的配置。
- 通过 spring.cloud.nacos.config.extension-configs[n].group 的配置方式自定义 Data Id所在的组,不明确配置的话,默认是 DEFAULT_GROUP。
- 通过 spring.cloud.nacos.config.extension-configs[n].refresh 的配置方式来控制该Data Id 在配置变更时,是否支持应用中可动态刷新, 感知到最新的配置值。默认是不支持的。
多个 Data Id 同时配置时,他的优先级关系是 spring.cloud.nacos.config.extension-configs[n].data-id 其中 n 的值越大,优先级越高。
Note spring.cloud.nacos.config.extension-configs[n].data-id 的值必须带文件扩展名,文件扩展名既可支持 properties,又可以支持 yaml/yml。 此时spring.cloud.nacos.config.file-extension 的配置对自定义扩展配置的 Data Id 文件扩展名没有影响;通过自定义扩展的 Data Id 配置,既可以解决多个应用间配置共享的问题,又可以支持一个应用有多个配置文件。为了更加清晰的在多个应用间配置共享的 Data Id ,你可以通过以下的方式来配置:通过自定义扩展的 Data Id 配置,既可以解决多个应用间配置共享的问题,又可以支持一个应用有多个配置文件。为了更加清晰的在多个应用间配置共享的 Data Id ,你可以通过以下的方式来配置:
# 配置支持共享的 Data Id
spring.cloud.nacos.config.shared-configs[0].data-id=common.yaml
# 配置 Data Id 所在分组,缺省默认 DEFAULT_GROUP
spring.cloud.nacos.config.shared-configs[0].group=GROUP_APP1
# 配置Data Id 在配置变更时,是否动态刷新,缺省默认 false
spring.cloud.nacos.config.shared-configs[0].refresh=true
可以看到:
- 通过 spring.cloud.nacos.config.shared-configs[n].data-id 来支持多个共享 Data Id 的配置。
- 通过 spring.cloud.nacos.config.shared-configs[n].group 来配置自定义 Data Id 所在的组,不明确配置的话,默认是 DEFAULT_GROUP。
- 通过 spring.cloud.nacos.config.shared-configs[n].refresh 来控制该Data Id在配置变更时,是否支持应用中动态刷新,默认false。
配置的优先级
Spring Cloud Alibaba Nacos Confifig 目前提供了三种配置能力从 Nacos 拉取相关的配置。
A: 通过 spring.cloud.nacos.config.shared-configs[n].data-id 支持多个共享 Data Id 的配置
B: 通过 spring.cloud.nacos.config.extension-configs[n].data-id 的方式支持多个扩展Data Id 的配置
C: 通过内部相关规则(应用名、应用名+ Profifile )自动生成相关的 Data Id 配置当三种方式共同使用时,他们的一个优先级关系是:A < B < C
配置动态刷新
配置的动态刷新,仅需要使用@RefreshScope注解即可。
使用配置类来创建bean时,若要实现注入bean的刷新,需要在配置类和Bean创建方法上均加上@RefreshScope注解。在对应配置被修改后,所有开启了刷新的注入bean在下一次调用时会重新进行初始化并替换掉之前注入的Bean。因bean替换时,弃用的bean会执行销毁方法来释放资源,故自定义Bean建议实现Closeable接口。
需要注意的是,由于@RefreshScope注解底层是使用cglib动态代理来实现,而cglib是创建动态子类继承来完成功能的增强,在使用@RefreshScope注解刷新包含final属性/final方法的bean时,会导致返回的代理对象为null的情况。典型的例子比如Elasticsearch的RestHighLevelClient。此时需要将需要刷新的Bean封装一层,避免final属性/final方法的问题。
下面是常用的数据源类型bean的动态刷新配置代码:
Nacos配置
mybatis: datasource: enable: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 minIdle: 5 validationQuery: select version() keepAlive: true initialSize: 5 maxWait: 60000 poolPreparedStatements: true filters: stat,config type: com.alibaba.druid.pool.DruidDataSource driverClassName: org.postgresql.Driver url: jdbc:postgresql://127.0.0.1:5432/postgres username: xxx password: xxx maxPoolPreparedStatementPerConnectionSize: 20 testOnBorrow: true testWhileIdle: true timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 testOnReturn: false maxActive: 30 validationQueryTimeout: 10 jedispool: config: enable: true host: 127.0.0.1 port: 6379 password: timeOut: 30000 maxIdle: 500 maxWaitMillis: 50000 maxTotal: 10000 es: config: enable: true ip: 127.0.0.1 port: 9200
基于Jedis的redis数据源
配置类
package cn.com.geostar.datasource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Component; /** * @author xiawei * @date 2020/8/10 11:33 */ @Component @RefreshScope @ConfigurationProperties(prefix = "jedispool.config") public class JedisPoolConfigure { private boolean enable; private String host; private Integer port; private String password; private Integer timeOut; private Integer maxIdle; private Integer maxWaitMillis; private Integer maxTotal; // getter setter }
Bean创建
/** * jedis连接池,开启刷新 */ @Bean @RefreshScope public JedisPool jedisPool() { if (!poolConfig.isEnable()) { logger.warn("redis datasource is disable !"); return null; } String host = poolConfig.getHost(); Integer port = poolConfig.getPort(); if (StringUtils.isBlank(host) || port == null) { logger.warn("redis datasource config: host/port must be specified!"); return null; } int timeOut = Optional.ofNullable(poolConfig.getTimeOut()).orElseGet(() -> { logger.info("redis datasource config: timeOut not be specified, used default value 30000 ."); return 30000; }); Integer maxTotal = Optional.ofNullable(poolConfig.getMaxTotal()).orElseGet(() -> { logger.info("redis datasource config: maxTotal not be specified, used default value 10000 ."); return 10000; }); Integer maxIdle = Optional.ofNullable(poolConfig.getMaxIdle()).orElseGet(() -> { logger.info("redis datasource config: maxIdle not be specified, used default value 500 ."); return 500; }); Integer maxWaitMillis = Optional.ofNullable(poolConfig.getTimeOut()).orElseGet(() -> { logger.info("redis datasource config: maxWaitMillis not be specified, used default value 50000 ."); return 50000; }); String password = poolConfig.getPassword(); JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(maxTotal); config.setMaxIdle(maxIdle); config.setMaxWaitMillis(maxWaitMillis); if (StringUtils.isBlank(password)) { logger.info("redis datasource init successful, host = " + host + ", port = " + port); return new JedisPool(config, host, port, timeOut); } else { logger.info("redis datasource init successful, host = " + host + ", port = " + port); return new JedisPool(config, host, port, timeOut, password); } }
基于官方RestHighLevelClient的ES数据源
Client封装类
package cn.com.geostar.datasource; import org.elasticsearch.client.RestHighLevelClient; import java.io.Closeable; import java.io.IOException; /** * @author xiawei * @date 2020/8/11 9:29 */ public class ElasticSearchRestClient implements Closeable { private RestHighLevelClient restHighLevelClient; public RestHighLevelClient getRestHighLevelClient() { return restHighLevelClient; } public void setRestHighLevelClient(RestHighLevelClient restHighLevelClient) { this.restHighLevelClient = restHighLevelClient; } @Override public void close() throws IOException { restHighLevelClient.close(); } }
配置类
package cn.com.geostar.datasource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Component; /** * @author xiawei * @date 2020/8/10 14:26 */ @Component @RefreshScope @ConfigurationProperties(prefix = "es.config") public class ElasticSearchConfigure { private String ip; private Integer port; private boolean enable; // getter setter }
Bean创建
/** * es初始化,开启刷新 */ @Bean @RefreshScope public ElasticSearchRestClient restHighLevelClient() { String ip = elasticSearchConfig.getIp(); Integer port = elasticSearchConfig.getPort(); if (!elasticSearchConfig.isEnable()) { logger.warn("elasticsearch datasource is disable!"); return null; } if (StringUtils.isBlank(ip) || port == null) { logger.warn("elasticsearch datasource config: ip/port must be specified!"); return null; } RestClientBuilder builder = RestClient.builder(new HttpHost(ip, port, "http")); RestHighLevelClient client = new RestHighLevelClient(builder); ElasticSearchRestClient esrc = new ElasticSearchRestClient(); esrc.setRestHighLevelClient(client); logger.info("elasticsearch datasource init successful, ip = " + ip + ", port = " + port); return esrc; }
基于Druid连接池和Mybatis持久层框架的关系数据库
DataSource配置类
package cn.com.geostar.datasource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.stereotype.Component; /** * @author xiawei * @date 2020/8/11 11:25 */ @Component @RefreshScope @ConfigurationProperties(prefix = "mybatis.datasource") public class MybatisDataSourceConfig { private boolean enable; private String connectionProperties; private Integer minIdle; private String validationQuery; private Boolean keepAlive; private Integer initialSize; private Integer maxWait; private Boolean poolPreparedStatements; private String filters; private String type; private String driverClassName; private String url; private String username; private String password; private Integer maxPoolPreparedStatementPerConnectionSize; private Boolean testOnBorrow; private Boolean testWhileIdle; private Integer timeBetweenEvictionRunsMillis; private Integer minEvictableIdleTimeMillis; private Boolean testOnReturn; private Integer maxActive; private Integer validationQueryTimeout; // getter setter }
Bean创建
@Bean @RefreshScope public DataSource dataSource() throws SQLException { if (!mybatisDataSourceConfig.isEnable()) { logger.warn("mybatis datasource is disable !"); return null; } String validationQuery = mybatisDataSourceConfig.getValidationQuery(); String filters = mybatisDataSourceConfig.getFilters(); String driverClassName = mybatisDataSourceConfig.getDriverClassName(); String url = mybatisDataSourceConfig.getUrl(); String username = mybatisDataSourceConfig.getUsername(); String password = mybatisDataSourceConfig.getPassword(); if (StringUtils.isBlank(validationQuery) || StringUtils.isBlank(filters) || StringUtils.isBlank(driverClassName) || StringUtils.isBlank(url) || StringUtils.isBlank(username) || StringUtils.isBlank(password)) { logger.warn("mybatis datasource config: validationQuery、filters、driverClassName、url、username、password must be specified!"); return null; } DruidDataSource druidDataSource = checkDruidDataSourceParams(mybatisDataSourceConfig); logger.info("druidDataSource init successful, url = " + url); return druidDataSource; } private DruidDataSource checkDruidDataSourceParams(MybatisDataSourceConfig mybatisDataSourceConfig) throws SQLException { String connectionProperties = Optional.ofNullable(mybatisDataSourceConfig.getConnectionProperties()) .orElseGet(() -> { logger.info("mybatis datasource config: connectionProperties not be specified," + " used default value {druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000} ."); return "druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000"; }); Integer minIdle = Optional.ofNullable(mybatisDataSourceConfig.getMinIdle()) .orElseGet(() -> { logger.info("mybatis datasource config: minIdle not be specified, used default value {5} ."); return 5; }); Boolean keepAlive = Optional.ofNullable(mybatisDataSourceConfig.getKeepAlive()) .orElseGet(() -> { logger.info("mybatis datasource config: keepAlive not be specified, used default value {true} ."); return true; }); Integer initialSize = Optional.ofNullable(mybatisDataSourceConfig.getInitialSize()) .orElseGet(() -> { logger.info("mybatis datasource config: initialSize not be specified, used default value {5} ."); return 5; }); Integer maxWait = Optional.ofNullable(mybatisDataSourceConfig.getMaxWait()) .orElseGet(() -> { logger.info("mybatis datasource config: maxWait not be specified, used default value {60000} ."); return 60000; }); Boolean poolPreparedStatements = Optional.ofNullable(mybatisDataSourceConfig.getPoolPreparedStatements()) .orElseGet(() -> { logger.info("mybatis datasource config: poolPreparedStatements not be specified, used default value {true} ."); return true; }); Integer maxPoolPreparedStatementPerConnectionSize = Optional.ofNullable(mybatisDataSourceConfig.getMaxPoolPreparedStatementPerConnectionSize()) .orElseGet(() -> { logger.info("mybatis datasource config: maxPoolPreparedStatementPerConnectionSize not be specified, used default value {20} ."); return 20; }); Boolean testOnBorrow = Optional.ofNullable(mybatisDataSourceConfig.getTestOnBorrow()) .orElseGet(() -> { logger.info("mybatis datasource config: testOnBorrow not be specified, used default value {true} ."); return true; }); Boolean testWhileIdle = Optional.ofNullable(mybatisDataSourceConfig.getTestWhileIdle()) .orElseGet(() -> { logger.info("mybatis datasource config: testWhileIdle not be specified, used default value {true} ."); return true; }); Integer timeBetweenEvictionRunsMillis = Optional.ofNullable(mybatisDataSourceConfig.getTimeBetweenEvictionRunsMillis()) .orElseGet(() -> { logger.info("mybatis datasource config: timeBetweenEvictionRunsMillis not be specified, used default value {60000} ."); return 60000; }); Integer minEvictableIdleTimeMillis = Optional.ofNullable(mybatisDataSourceConfig.getMinEvictableIdleTimeMillis()) .orElseGet(() -> { logger.info("mybatis datasource config: minEvictableIdleTimeMillis not be specified, used default value {300000} ."); return 300000; }); Boolean testOnReturn = Optional.ofNullable(mybatisDataSourceConfig.getTestOnReturn()) .orElseGet(() -> { logger.info("mybatis datasource config: testOnReturn not be specified, used default value {false} ."); return false; }); Integer maxActive = Optional.ofNullable(mybatisDataSourceConfig.getMaxActive()) .orElseGet(() -> { logger.info("mybatis datasource config: maxActive not be specified, used default value {30} ."); return 30; }); Integer validationQueryTimeout = Optional.ofNullable(mybatisDataSourceConfig.getValidationQueryTimeout()) .orElseGet(() -> { logger.info("mybatis datasource config: validationQueryTimeout not be specified, used default value {10} ."); return 10; }); String validationQuery = mybatisDataSourceConfig.getValidationQuery(); String filters = mybatisDataSourceConfig.getFilters(); String driverClassName = mybatisDataSourceConfig.getDriverClassName(); String url = mybatisDataSourceConfig.getUrl(); String username = mybatisDataSourceConfig.getUsername(); String password = mybatisDataSourceConfig.getPassword(); DruidDataSource build = DataSourceBuilder.create().type(DruidDataSource.class) .build(); build.setConnectionProperties(connectionProperties); build.setMinIdle(minIdle); build.setValidationQuery(validationQuery); build.setKeepAlive(keepAlive); build.setInitialSize(initialSize); build.setMaxWait(maxWait); build.setPoolPreparedStatements(poolPreparedStatements); build.setFilters(filters); build.setDriverClassName(driverClassName); build.setUrl(url); build.setUsername(username); build.setPassword(password); build.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize); build.setTestOnBorrow(testOnBorrow); build.setTestWhileIdle(testWhileIdle); build.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); build.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); build.setTestOnReturn(testOnReturn); build.setMaxActive(maxActive); build.setValidationQueryTimeout(validationQueryTimeout); return build; }
SqlSessionFactory和TransactionManager
package cn.com.geostar.datasource; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.boot.autoconfigure.SpringBootVFS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; import java.io.IOException; /** * @author xiawei * @date 2020/8/9 15:03 */ @Configuration @MapperScan(basePackages = {"cn.com.geostar.dao"}, sqlSessionFactoryRef = "sqlSessionFactoryBean") @EnableTransactionManagement public class MybatisSqlAndTransactionConfig { private final Logger logger = LoggerFactory.getLogger(MybatisSqlAndTransactionConfig.class); @Autowired private DataSource dataSource; @Bean(value = "sqlSessionFactoryBean") public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException { if ("null".equals(dataSource.toString())) { logger.warn("sqlSessionFactoryBean init failed , because dataSource is null !"); } SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setVfs(SpringBootVFS.class); sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("/config/mybatis-config.xml")); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("/config/mapper/*.xml")); return sqlSessionFactoryBean; } @Bean(name = "transactionManagerMybatis") public PlatformTransactionManager dataSourceTransactionManager() { if ("null".equals(dataSource.toString())) { logger.warn("dataSourceTransactionManager init failed , because dataSource is null !"); } DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dataSource); return dataSourceTransactionManager; } }