基於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; } }