springcloud:这么多个微服务,nacos配置文件只用写一遍(八)

0. 引言

我们在前几期演示了如何从零搭建微服务项目,但是随着我们微服务的增加,一个麻烦的问题就会凸显出来:配置文件的管理。我们之前已经讲了可以利用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、其次,我们需要改装一下我们的启动类,让启动类能够执行加载这些配置文件的方法

@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();
        String[] activeProfiles = environment.getActiveProfiles();
        List<String> profiles = Arrays.asList(activeProfiles);
        List<String> activeProfileList = new ArrayList<>(profiles);
        Function<Object[], String> 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模块

<dependency>
            <groupId>com.example</groupId>
            <artifactId>commons</artifactId>
            <version>0.0.1-SNAPSHOT</version>
</dependency>

启动时就要用我们自定义的方法来启动了

@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<LauncherService> {
    
    

    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<LauncherService> launcherList = new ArrayList<>();
ServiceLoader.load(LauncherService.class).forEach(launcherList::add);

2、但是要保障ServiceLoader成功获取到实现类,还需要做一个配置,那就是在项目中的resources文件夹下创建一个META-INF.services文件夹

3、然后在这个文件夹下创建一个LauncherService接口类的全路径文件,比如我这里的全路径是

com.example.commons.launcher.LauncherService

4、然后在这个文件中将所有实现类的全路径名写进去

com.example.commons.launcher.impl.NacosLauncherServiceImpl

在这里插入图片描述
5、最后将我们自定义启动类中的方法再次优化下,将这些实现类按自定义顺序加载

List<LauncherService> 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<LauncherService> {
    
    

    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文件夹,再创建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();
        String[] activeProfiles = environment.getActiveProfiles();
        List<String> profiles = Arrays.asList(activeProfiles);
        List<String> activeProfileList = new ArrayList<>(profiles);
        Function<Object[], String> 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<LauncherService> 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模块的依赖

<dependency>
            <groupId>com.example</groupId>
            <artifactId>commons</artifactId>
            <version>0.0.1-SNAPSHOT</version>
</dependency>

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地址中下载本次演示源码

项目git地址

本次项目演示源码

那么咱们本期的内容也就到此结束了

关注公众号,了解更多新鲜内容

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_24950043/article/details/124071691