我们在前几期演示了如何从零搭建微服务项目,但是随着我们微服务的增加,一个麻烦的问题就会凸显出来:配置文件的管理。我们之前已经讲了可以利用nacos作为配置中心来管理微服务的配置文件。
但是nacos本身的配置项如何管理呢?
如果我们nacos的地址做了修改,就会导致我们需要到每个服务的配置文件中去修改nacos的地址,无疑这很不方便,也不利于管理。于是我们需要一个更加方便的管理方式。
1. 思路首先要解决这个问题,我们要先了解到,springboot中的配置项实际上都是加载到Properties类中,所以我们的思路就是: 1、直接在代码中将这nacos的配置项写入到Properties中
Properties props = System.getProperties();
props.setProperty("spring.cloud.nacos.discovery.server-addr", "localhost:8848");
2、但是我们需要一个统一的地方在维护这些配置项,所以我们需要先创建一个接口类LauncherConstant来承装nacos相关的配置项
public interface LauncherConstant {
String NACOS_ADDR = "localhost:8848";
String NACOS_USERNAME="nacos";
String NACOS_PASSWORD="nacos";
}
3、真实开发中我们还需要考虑一个问题,那就是开发、测试、生产环境的切换。也就是说我们需要配置三个nacos地址,用于不同的环境。并且根据不同的环境进行自动切换。于是需要将LauncherConstant优化一下
public interface LauncherConstant {
String DEV_CODE = "dev";
String PROD_CODE = "prod";
String TEST_CODE = "test";
String NACOS_DEV_ADDR = "localhost:8848";
String NACOS_PROD_ADDR = "localhost:8848";
String NACOS_TEST_ADDR = "localhost:8848";
String NACOS_USERNAME = "nacos";
String NACOS_PASSWORD = "nacos";
String CONFIG_FORMAT_DEFAULT = "yaml";
String SHARE_DATA_ID_DEFAULT = "blade";
/**
* 动态获取nacos地址
*
* @param profile 环境变量
* @return addr
*/
static String nacosAddr(String profile) {
switch (profile) {
case (PROD_CODE):
return NACOS_PROD_ADDR;
case (TEST_CODE):
return NACOS_TEST_ADDR;
default:
return NACOS_DEV_ADDR;
}
}
static String dataId(String appName){
return appName + "." + CONFIG_FORMAT_DEFAULT;
}
static String dataId(String appName,String profile){
return dataId(appName + "-" + profile);
}
static String dataId(String appName,String profile,String format){
return appName + "-" + profile + "." + format;
}
static String sharedDataId(){
return SHARE_DATA_ID_DEFAULT+"."+CONFIG_FORMAT_DEFAULT;
}
static String sharedDataId(String profile){
return SHARE_DATA_ID_DEFAULT + "-" + profile + "." + CONFIG_FORMAT_DEFAULT;
}
}
4、其次,我们需要改装一下我们的启动类,让启动类能够执行加载这些配置文件的方法 先引入依赖
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
自定义启动类:
@EnableDiscoveryClient
@EnableAutoConfiguration
public class SpringSelfApplication {
public SpringSelfApplication(){}
public static ConfigurableApplicationContext run(String applicationName,Class source,String... args){
SpringApplicationBuilder builder = createBuilder(applicationName, source, args);
return builder.run(args);
}
public static SpringApplicationBuilder createBuilder(String appName, Class source, String... args) {
// 获取环境profile
ConfigurableEnvironment environment = new StandardEnvironment();
// 加载其他环境变量,比如从命令行输入的环境环境变量
MutablePropertySources propertySources = environment.getPropertySources();
propertySources.addFirst(new SimpleCommandLinePropertySource(args));
propertySources.addLast(new MapPropertySource("systemProperties", environment.getSystemProperties()));
propertySources.addLast(new SystemEnvironmentPropertySource("systemEnvironment", environment.getSystemEnvironment()));
String[] activeProfiles = environment.getActiveProfiles();
List profiles = Arrays.asList(activeProfiles);
List activeProfileList = new ArrayList(profiles);
Function joinFun = StringUtils::arrayToCommaDelimitedString;
SpringApplicationBuilder builder = new SpringApplicationBuilder(source);
String profile;
if (activeProfileList.isEmpty()) {
profile = "dev";
activeProfileList.add(profile);
builder.profiles(profile);
} else {
if (activeProfileList.size() != 1) {
throw new RuntimeException("同时存在环境变量:[" + StringUtils.arrayToCommaDelimitedString(activeProfiles) + "]");
}
profile = activeProfileList.get(0);
}
String activePros = joinFun.apply(activeProfileList.toArray());
System.out.printf("----启动中,当前环境为:[%s]", activePros);
Properties props = System.getProperties();
// 加载通用配置项
props.setProperty("spring.application.name", appName);
props.setProperty("spring.profiles.active", profile);
props.setProperty("file.encoding", StandardCharsets.UTF_8.name());
// 加载nacos配置项
props.setProperty("spring.cloud.nacos.discovery.server-addr", LauncherConstant.nacosAddr(profile));
props.setProperty("spring.cloud.nacos.config.server-addr", LauncherConstant.nacosAddr(profile));
props.setProperty("spring.cloud.nacos.config.username", "nacos");
props.setProperty("spring.cloud.nacos.config.password", "nacos");
props.setProperty("spring.cloud.nacos.discovery.username", "nacos");
props.setProperty("spring.cloud.nacos.discovery.password", "nacos");
props.setProperty("spring.cloud.nacos.config.file-extension", "yaml");
props.setProperty("spring.cloud.nacos.config.shared-configs[0].data-id", LauncherConstant.sharedDataId());
props.setProperty("spring.cloud.nacos.config.shared-configs[0].group", "DEFAULT_GROUP");
props.setProperty("spring.cloud.nacos.config.shared-configs[0].refresh", "true");
props.setProperty("spring.cloud.nacos.config.shared-configs[1].data-id", LauncherConstant.sharedDataId(profile));
props.setProperty("spring.cloud.nacos.config.shared-configs[1].group", "DEFAULT_GROUP");
props.setProperty("spring.cloud.nacos.config.shared-configs[1].refresh", "true");
return builder;
}
}
5、最后我们把上述的代码放到一个commons服务中,然后将其他微服务中引入commons模块
com.example
commons
0.0.1-SNAPSHOT
启动时就要用我们自定义的方法来启动了
@SpringBootApplication
public class UserServerApplication {
public static void main(String[] args) {
SpringSelfApplication.run("user-server",UserServerApplication.class,args);
}
}
如此我们的需求就能实现了,下面我们来看看完整的代码
2. 进阶思路 2.1 定义配置加载接口及实现类我们上述的nacos配置文件的加载是全部都放到了自定义的启动类中了
现在还是Nacos配置项,后续可能还有更多组件的配置项,比如seata,swagger等。如果我们都冗杂在一个地方,会导致自定义启动类的方法过分冗长。且未分类不易于管理,不符合我们的设计思想。
因此我们提出一个思路,定义一个接口类,然后将各个组件的配置文件加载放到这个接口的实现类中
接口如下所示,因为某些组件之间可能存在加载顺序关系,比如A组件的配置文件必须要在B组件之前加载
因此我们让该接口继承了Ordered,Comparable,主要用于多个实现类时,我们来通过排序和比较来定义各个实现类的加载顺序,从而将各种组件的加载顺序定义出来。通过getOrder来定义加载顺序
public interface LauncherService extends Ordered,Comparable {
void launcher(SpringApplicationBuilder builder, String applicationName, String profile, boolean isLocalDev);
@Override
default int getOrder() {
return 0;
}
@Override
default int compareTo(LauncherService o) {
return Integer.compare(this.getOrder(), o.getOrder());
}
}
Nacos配置文件加载实现类
public class NacosLauncherServiceImpl implements LauncherService {
@Override
public void launcher(SpringApplicationBuilder builder, String applicationName, String profile, boolean isLocalDev){
Properties props = System.getProperties();
// 通用注册
props.setProperty("spring.cloud.nacos.discovery.server-addr", LauncherConstant.nacosAddr(profile));
props.setProperty("spring.cloud.nacos.config.server-addr", LauncherConstant.nacosAddr(profile));
props.setProperty("spring.cloud.nacos.config.username", LauncherConstant.NACOS_USERNAME);
props.setProperty("spring.cloud.nacos.config.password", LauncherConstant.NACOS_PASSWORD);
props.setProperty("spring.cloud.nacos.discovery.username", LauncherConstant.NACOS_USERNAME);
props.setProperty("spring.cloud.nacos.discovery.password", LauncherConstant.NACOS_PASSWORD);
props.setProperty("spring.cloud.nacos.config.file-extension", "yaml");
props.setProperty("spring.cloud.nacos.config.shared-configs[0].data-id", LauncherConstant.sharedDataId());
props.setProperty("spring.cloud.nacos.config.shared-configs[0].group", "DEFAULT_GROUP");
props.setProperty("spring.cloud.nacos.config.shared-configs[0].refresh", "true");
props.setProperty("spring.cloud.nacos.config.shared-configs[1].data-id", LauncherConstant.sharedDataId(profile));
props.setProperty("spring.cloud.nacos.config.shared-configs[1].group", "DEFAULT_GROUP");
props.setProperty("spring.cloud.nacos.config.shared-configs[1].refresh", "true");
}
}
后续如果要添加配置的话,就可以通过增加LauncherService实现类并重写launcher方法来实现。
2.2 读取接口下的所有实现类有了实现类中加载各个组件的配置,那么我们下一步要做的,就是在自定义启动类中获取这些实现类,并且执行实现类的加载配置文件的方法
所以这里有一个核心诉求:获取指定接口的所有实现类
针对这点核心思路都是通过反射来实现,这里我们不做过多探讨,直接提供方法
1、我们可以通过 ServiceLoader.load(LauncherService.class)
方法实现,这个方法会返回一个接口实现类的迭代器,我们可以通过循环这个迭代器,来将实现类添加到一个集合中,如下所示
List launcherList = new ArrayList();
ServiceLoader.load(LauncherService.class).forEach(launcherList::add);
2、但是要保障ServiceLoader成功获取到实现类,还需要做一个配置,那就是在项目中的resources文件夹下创建一个META-INF
文件夹,再再这个文件夹下创建一个services
文件夹,注意这里要分开创建,不要直接创建一个META-INF.services
的文件夹!!!
3、然后在这个文件夹下创建一个LauncherService
接口类的全路径文件,比如我这里的全路径是
com.example.commons.launcher.LauncherService
4、然后在这个文件中将所有实现类的全路径名写进去
com.example.commons.launcher.impl.NacosLauncherServiceImpl
这一步必不可少,但是为什么要做这一步配置呢?
这是因为spring默认读取的是同包名下的类文件,假如我们将自定义的启动类放到product-server
服务中执行,那么扫描到的包名就是product-server
服务的包名,而我们的LauncherService
接口的实现类是放在commons
服务下的,其包名与product-server
并不一致
因此我们需要跨包名去读取类文件,想要实现跨包名读取类文件,就需要我们在resources
文件夹下创建META-INF.services
文件夹,以及文件名为接口类全路径名称的文件,并且将其所有的实现类的全路径书写在该文件中。
5、最后将我们自定义启动类中的方法再次优化下,将这些实现类按自定义顺序加载
List launcherList = new ArrayList();
ServiceLoader.load(LauncherService.class).forEach(launcherList::add);
(launcherList.stream().sorted(Comparator.comparing(LauncherService::getOrder)).collect(Collectors.toList())).forEach((launcherService) -> {
launcherService.launcher(builder, appName, profile, isLocalDev());
});
3. 完整实现
最后我们展示下完整实现,让大家把上述知识点串接起来 (以下实现参考springblade源码)
1、创建commons模块。引入nacos,spring web,lombok依赖 2、创建LauncherConstant接口类,用于承装配置项
public interface LauncherConstant {
String DEV_CODE = "dev";
String PROD_CODE = "prod";
String TEST_CODE = "test";
String NACOS_DEV_ADDR = "localhost:8848";
String NACOS_PROD_ADDR = "localhost:8848";
String NACOS_TEST_ADDR = "localhost:8848";
String NACOS_USERNAME = "nacos";
String NACOS_PASSWORD = "nacos";
String CONFIG_FORMAT_DEFAULT = "yaml";
String SHARE_DATA_ID_DEFAULT = "blade";
/**
* 动态获取nacos地址
*
* @param profile 环境变量
* @return addr
*/
static String nacosAddr(String profile) {
switch (profile) {
case (PROD_CODE):
return NACOS_PROD_ADDR;
case (TEST_CODE):
return NACOS_TEST_ADDR;
default:
return NACOS_DEV_ADDR;
}
}
static String dataId(String appName){
return appName + "." + CONFIG_FORMAT_DEFAULT;
}
static String dataId(String appName,String profile){
return dataId(appName + "-" + profile);
}
static String dataId(String appName,String profile,String format){
return appName + "-" + profile + "." + format;
}
static String sharedDataId(){
return SHARE_DATA_ID_DEFAULT+"."+CONFIG_FORMAT_DEFAULT;
}
static String sharedDataId(String profile){
return SHARE_DATA_ID_DEFAULT + "-" + profile + "." + CONFIG_FORMAT_DEFAULT;
}
}
3、创建接口类LauncherService
public interface LauncherService extends Ordered,Comparable {
void launcher(SpringApplicationBuilder builder, String applicationName, String profile, boolean isLocalDev);
@Override
default int getOrder() {
return 0;
}
@Override
default int compareTo(LauncherService o) {
return Integer.compare(this.getOrder(), o.getOrder());
}
}
4、创建Nacos配置文件加载实现类
后续如果我们还需要配置其他组件的配置加载类的话,就只需要创建一个LauncherService实现类即可
这里需要注意的是,我们还额外配置了两个共享配置文件shared-configs,如果不需要的话可以删除
public class NacosLauncherServiceImpl implements LauncherService {
@Override
public void launcher(SpringApplicationBuilder builder,String applicationName,String profile,boolean isLocalDev){
Properties props = System.getProperties();
// 通用注册
props.setProperty("spring.cloud.nacos.discovery.server-addr", LauncherConstant.nacosAddr(profile));
props.setProperty("spring.cloud.nacos.config.server-addr", LauncherConstant.nacosAddr(profile));
props.setProperty("spring.cloud.nacos.config.username", "nacos");
props.setProperty("spring.cloud.nacos.config.password", "nacos");
props.setProperty("spring.cloud.nacos.discovery.username", "nacos");
props.setProperty("spring.cloud.nacos.discovery.password", "nacos");
props.setProperty("spring.cloud.nacos.config.file-extension", "yaml");
props.setProperty("spring.cloud.nacos.config.shared-configs[0].data-id", LauncherConstant.sharedDataId());
props.setProperty("spring.cloud.nacos.config.shared-configs[0].group", "DEFAULT_GROUP");
props.setProperty("spring.cloud.nacos.config.shared-configs[0].refresh", "true");
props.setProperty("spring.cloud.nacos.config.shared-configs[1].data-id", LauncherConstant.sharedDataId(profile));
props.setProperty("spring.cloud.nacos.config.shared-configs[1].group", "DEFAULT_GROUP");
props.setProperty("spring.cloud.nacos.config.shared-configs[1].refresh", "true");
}
}
5、在commons服务的resources文件夹下创建META-INF
文件夹,再创建子文件夹services
,再在services
下创建com.example.commons.launcher.LauncherService
文件,文件内添加内容
com.example.commons.launcher.impl.NacosLauncherServiceImpl
6、重写启动方法,并且添加上@EnableDiscoveryClient,@EnableAutoConfiguration注解
@EnableDiscoveryClient
@EnableAutoConfiguration
public class SpringSelfApplication {
public SpringSelfApplication(){}
public static ConfigurableApplicationContext run(String applicationName,Class source,String... args){
SpringApplicationBuilder builder = createBuilder(applicationName, source, args);
return builder.run(args);
}
public static SpringApplicationBuilder createBuilder(String appName, Class source, String... args) {
// 获取环境profile
ConfigurableEnvironment environment = new StandardEnvironment();
// 加载其他环境变量,比如从命令行输入的环境环境变量
MutablePropertySources propertySources = environment.getPropertySources();
propertySources.addFirst(new SimpleCommandLinePropertySource(args));
propertySources.addLast(new MapPropertySource("systemProperties", environment.getSystemProperties()));
propertySources.addLast(new SystemEnvironmentPropertySource("systemEnvironment", environment.getSystemEnvironment()));
String[] activeProfiles = environment.getActiveProfiles();
List profiles = Arrays.asList(activeProfiles);
List activeProfileList = new ArrayList(profiles);
Function joinFun = StringUtils::arrayToCommaDelimitedString;
SpringApplicationBuilder builder = new SpringApplicationBuilder(source);
String profile;
if (activeProfileList.isEmpty()) {
profile = "dev";
activeProfileList.add(profile);
builder.profiles(profile);
} else {
if (activeProfileList.size() != 1) {
throw new RuntimeException("同时存在环境变量:[" + StringUtils.arrayToCommaDelimitedString(activeProfiles) + "]");
}
profile = activeProfileList.get(0);
}
String activePros = joinFun.apply(activeProfileList.toArray());
System.out.printf("----启动中,当前环境为:[%s]", activePros);
Properties props = System.getProperties();
// 加载通用配置项
props.setProperty("spring.application.name", appName);
props.setProperty("spring.profiles.active", profile);
props.setProperty("file.encoding", StandardCharsets.UTF_8.name());
Properties defaultProperties = new Properties();
defaultProperties.setProperty("spring.main.allow-bean-definition-overriding", "true");
builder.properties(defaultProperties);
List launcherList = new ArrayList();
ServiceLoader.load(LauncherService.class).forEach(launcherList::add);
(launcherList.stream().sorted(Comparator.comparing(LauncherService::getOrder)).collect(Collectors.toList())).forEach((launcherService) -> {
launcherService.launcher(builder, appName, profile, isLocalDev());
});
return builder;
}
public static boolean isLocalDev() {
String osName = System.getProperty("os.name");
return StringUtils.hasText(osName) && !"LINUX".equalsIgnoreCase(osName);
}
}
6、在其他微服务中添加上commons模块的依赖
com.example
commons
0.0.1-SNAPSHOT
7、重写启动类方法
这里我们可以将服务名再放到一个接口类中进行统一管理,这样也方便知道之前创建了哪些服务名
@SpringBootApplication
public class ProductServerFeignApplication {
public static void main(String[] args) {
SpringSelfApplication.run(ApplicationNameConstant.PRODUCT_SERVER_APPLICATION_NAME,ProductServerFeignApplication.class,args);
}
}
8、将commons install到本地maven仓库 9、启动服务,可以看到已经正常加载到nacos中了。
并且访问对应接口,发现mysql数据也获取出来了,说明读取了对应的nacos配置中心的配置文件,配置成功!!!
注意:以上代码演示基于专栏之前讲解的项目源码,可以直接到项目git地址中下载本次演示源码
本次项目演示源码
那么咱们本期的内容也就到此结束了
关注公众号 Elasticsearch之家,了解更多新鲜内容