单JVM进程启动多个SpringBoot项目探索及调优

该文章已生成可运行项目,

1.背景

使用SpringBoot技术栈进行REST HTTP接口开发服务时,一般来说如果模块较多或者涉及多人协作开发,大家会不自觉的将每个模块独立成一个单独的项目进行开发,部署时则将每个服务进行单独部署和运行。服务间的调用则通过FeignClients,服务的接入、负载、路由则是在前面摆个SpringCloud Gateway,同时服务注册/发现、配置则使用一个统一的Nacos。这样做好处显而易见,例如:开发时的代码冲突及分支合并、运行时系统资源分配及性能优化等都不打架、对某个服务的扩缩容也方便、K8S容器化也方便。这些对于公有云来说确实就应该这样搞,但是假如哪天要私有化售卖和交付的话,这样又问题颇多:一个服务一个K8S容器势必导致服务器数量的增加、私有自动化部署成本大等。那么面对这样的情况,最好的方式无非是:单独部署和统一集成部署双支持。想要的结果现在已经很明确了,可是咋搞呢?现在已经这样了,难道把所有分散的子项目合并到一个项目下,然后用一个SpringApplication.run()去启动?那么问题来了:之前是N个服务,现在是一个服务,人肉合并代码工作量大,Nacos中的一堆配置要改要整合等;那么如果能让nacos中的配置保持不变,各个服务代码不变或者很少变动则一种比较合适的方案。

2. 实现方案

基于上面的背景和前提,这里给出一个具体的实例,出于时间问题,我就不写一个完成的Demo了。方法和套路懂了,自行就能写出测试验证Demo。

2.1 Nacos服务注册说明

统一启动服务通过使用 SpringApplicationBuilder 启动了多个独立的 Spring Boot 应用实例,但由于它们共享了同一个 JVM 环境,而Nacos 客户端使用全局配置单例,这样会导致多个服务实例在同一进程中运行时,它们共享相同的 Nacos 客户端,后面的服务实例在注册时会覆盖前面的注册信息,从而导致只注册了一个服务。为了解决这个问题需要在 SpringApplicationBuilder 中为每个实例设置不同的 Nacos 配置前缀来隔离它们的注册信息,这样它们在 Nacos 中会被识别为独立的服务实例,从而避免了注册信息的覆盖问题。同样的子服务中的代码如果使用了@Value(“${spring.application.name}”)的方式获取服务名,统一聚合启动时同样有覆盖问题。

2.2 子服务约束

  1. 为避免统一集成部署时jar包冲突问题,要求所有子服务相关依赖的版本必须使用根POM中统一定义的版本(当前我们各个子服务就是这样做的);// 之前各个子服务独立运行肯定不会有问题,当所集成到一个进程下运行之后,如果某些依赖的版本不一致,那么就会出现jar包冲突问题。
  2. 所有子服务的根包名相对统一,例如,都是com.china.xxx(这一点一般来说都满足,毕竟依赖管理中的groupId是重要标识);所有子服务的SpringBootApplication启动类增加一个自定义注解用于进行子服务启动类的发现。 // 这条是可选的,只我不想在统一启动服务中去做一个配置,于是选择了用反射扫描的方式去进行子服务启动类发现的方式。
  3. 所有子服务必须打原包,不能使用SpringBoot 的"FAT JAR"方式打包(skip掉spring-boot-maven-plugin的repackage即可;);spring-boot-maven-plugin 是 Spring Boot 提供的一个插件,用于简化 Maven 项目中的构建、打包和运行过程。它默认会执行一个名为 repackage 的任务,将项目的 JAR 重新打包成一个包含所有依赖的可执行 JAR(也称为 Fat JAR 或 Uber JAR)。Fat JAR中的SpringBootApplication启动类不太好直接拿到; // 反正我们的子项目都用了,因此要增加这条约束。
  4. 代码中禁止通过@Value(“${spring.application.name}”)获取服务名(解决方案:服务名都是固定的,定义一个常量即可;); --可以去掉这条规则,详见:5.2 各子服务Spring配置隔离

2.3 统一启动服务实现思路

  1. 引用所有子服务的JAR包;
  2. 使用Reflections.getTypesAnnotatedWith的扫描方式获取所有子服务的SpringBootApplication启动类;同时使用VM参数支持子服务的In和Out的配置。
  3. 使用SpringApplicationBuilder分别启动各个子服务,同时为每个子服务创建的一个新的 ConfigurableApplicationContext 实例,以确保每个服务都在独立的上下文中运行;
  4. 统一启动服务通过使用 SpringApplicationBuilder 启动了多个独立的 Spring Boot 应用实例,但由于它们共享了同一个 JVM 环境,而Nacos 客户端使用全局配置单例,这样会导致多个服务实例在同一进程中运行时,它们共享相同的 Nacos 客户端,后面的服务实例在注册时会覆盖前面的注册信息,从而导致只注册了一个服务。为了解决这个问题需要在 SpringApplicationBuilder 中为每个实例设置不同的 Nacos 配置前缀来隔离它们的注册信息,这样它们在 Nacos 中会被识别为独立的服务实例,从而避免了注册信息的覆盖问题。
  5. 增加Shutdown钩子以确保创建的ApplicationContext 实例可以被优雅的关闭;

2. 主要实现

2.1 项目结构

项目结构大致如下,一共3个项目:bw-server-all是统一启动服务项目,bw-job和bw-ai-app是2个独立的子服务项目;

--统一启动服务:bw-server-all 
├── /bw-server-all 
│   ├── /src
│   │   ├── /main
│   │   │   ├── /java
│   │   │   │   └── /com
│   │   │   │       └── beam
│   │   │   │           └── work
│   │   │   │               └── server
│   │   │   │                   └── all
│   │   │   │                       └── boot
│   │   │   │                           └── Bootstrap.java  // 统一启动类
│   │   │   └── /resources
│   │   │       └── application.yml
│   │   └── /test
│   │       └── ...
│   └── pom.xml

--子服务1:bw-job
├── /bw-job
│   ├── /src
│   │   ├── /main
│   │   │   └── /java
│   │   │       └── /com
│   │   │           └── beam
│   │   │               └── job
│   │   │                   └── provider
│   │   │                       └── JobApplicationRun.java  // 启动类
│   │   └── /resources
│   │       └── application.yml
│   └── pom.xml

--子服务2:bw-ai-app
└── /bw-ai-app
    ├── /src
    │   ├── /main
    │   │   └── /java
    │   │       └── /com
    │   │           └── beam
    │   │               └── ai
    │   │                   └── app
    │   │                       └── provider
    │   │                           └── ApplicationBootstrap.java  // 启动类
    │   └── /resources
    │       └── application.yml
    └── pom.xml 

2.2 启动类扫描自定义注解

/**
 * BwApplication
 *
 * @author chenx
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BwApplication {

    /**
     * service name
     */
    String name() default "";

    /**
     * contextId:default: name + "-context"
     */
    String contextId() default "";
}

2.3 子服务启动类示例

在这里插入图片描述

2.4 子服务打包示例

在这里插入图片描述

2.4 统一启动服务实现

2.4.1 引用所有子服务的JAR包

在这里插入图片描述

2.4.2 BootstrapHelper实现

package com.beam.work.server.all.boot;

import com.umbrella.work.common.annotation.BwApplication;
import com.umbrella.work.common.exception.BeemRuntimeException;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.reflections.Reflections;
import org.reflections.scanners.Scanners;
import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.core.env.StandardEnvironment;

import java.io.PrintStream;
import java.util.*;

/**
 * BootstrapHelper
 *
 * @author chenx
 */
public class BootstrapHelper {

    private static final String[] SCAN_BASE_PACKAGES = {"com.beam", "com.umbrella.work", "com.beem"};

    private BootstrapHelper() {
        // do nothing
    }

    /**
     * getApplications
     *
     * @param appIn
     * @param appOut
     * @param nacosGroup
     * @return
     */
    public static Map<String, SpringApplicationBuilder> getApplications(String appIn, String appOut, String nacosGroup) {
        if (StringUtils.isEmpty(nacosGroup)) {
            throw new BeemRuntimeException("nacosGroup is empty!");
        }

        Set<String> in = getAppSet(appIn);
        Set<String> out = getAppSet(appOut);

        Reflections reflections = new Reflections(new ConfigurationBuilder()
                .forPackages(SCAN_BASE_PACKAGES)
                .addScanners(Scanners.TypesAnnotated));
        Set<Class<?>> annotatedClasses = reflections.getTypesAnnotatedWith(BwApplication.class);

        Map<String, SpringApplicationBuilder> map = new HashMap<>(annotatedClasses.size());
        for (Class<?> clazz : annotatedClasses) {
            BwApplication annotation = clazz.getAnnotation(BwApplication.class);
            String appName = annotation.name().toLowerCase();
            if (!isLoadApplication(appName, in, out)) {
                continue;
            }

            String contextId = StringUtils.isEmpty(annotation.contextId()) ? appName + "-context" : annotation.contextId();
            SpringApplicationBuilder builder = new SpringApplicationBuilder();
            builder.sources(clazz)
                    .environment(new StandardEnvironment())
                    .properties("spring.application.name=" + appName
                            , "spring.main.application-context-id=" + contextId
                            , "spring.main.allow-bean-definition-overriding=true"
                            , "spring.cloud.nacos.discovery.group=" + nacosGroup
                            , "spring.cloud.nacos.discovery.service=" + appName
                    )
                    .web(WebApplicationType.SERVLET);

            map.putIfAbsent(appName, builder);
        }

        return map;
    }

    /**
     * printSeparatedLog
     *
     * @param logger
     * @param info
     */
    public static void printSeparatedLog(Logger logger, String info) {
        if (Objects.isNull(logger)) {
            return;
        }

        String separator = getSeparator(info);
        logger.info(separator);
        logger.info(info);
        logger.info(separator);
    }

    /**
     * getSeparator
     *
     * @param info
     * @return
     */
    private static String getSeparator(String info) {
        if (StringUtils.isEmpty(info)) {
            return "";
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < info.length(); i++) {
            sb.append("=");
        }

        return sb.toString();
    }

    /**
     * getAppSet
     */
    private static Set<String> getAppSet(String apps) {
        if (StringUtils.isEmpty(apps)) {
            return Collections.emptySet();
        }

        Set<String> set = new HashSet<>();
        String[] array = apps.split(",");
        for (String entry : array) {
            String appName = entry.toLowerCase();
            if (StringUtils.isEmpty(appName) || set.contains(appName)) {
                continue;
            }

            set.add(appName);
        }

        return set;
    }

    /**
     * isLoadApplication
     */
    private static boolean isLoadApplication(String appName, Set<String> in, Set<String> out) {
        if (StringUtils.isEmpty(appName)) {
            return false;
        }

        if (CollectionUtils.isEmpty(in) && CollectionUtils.isEmpty(out)) {
            return true;
        }

        // APP_IN优先
        if (!CollectionUtils.isEmpty(in)) {
            return in.contains(appName.toLowerCase());
        }

        if (!CollectionUtils.isEmpty(out)) {
            return !out.contains(appName.toLowerCase());
        }

        return true;
    }

    /**
     * BootstrapBanners
     */
    public enum BootstrapBanners {

        START(new String[]{
                " ######  ########    ###    ########  ######## ",
                "##    ##    ##      ## ##   ##     ##    ##    ",
                "##          ##     ##   ##  ##     ##    ##    ",
                " ######     ##    ##     ## ########     ##    ",
                "      ##    ##    ######### ##   ##      ##    ",
                "##    ##    ##    ##     ## ##    ##     ##    ",
                " ######     ##    ##     ## ##     ##    ##    "}),
        FAIL(new String[]{
                " _______    ___       __   __      ",
                "|   ____|  /   \\     |  | |  |     ",
                "|  |__    /  ^  \\    |  | |  |     ",
                "|   __|  /  /_\\  \\   |  | |  |     ",
                "|  |    /  _____  \\  |  | |  `----.",
                "|__|   /__/     \\__\\ |__| |_______|"}),
        ;

        private final String[] banner;

        BootstrapBanners(String[] banner) {
            this.banner = banner;
        }

        /**
         * printBanner
         *
         * @param logger
         */
        public void printBanner(Logger logger) {
            if (this.banner != null) {
                for (String line : this.banner) {
                    logger.warn(line);
                }
            }
        }

        /**
         * printBanner
         *
         * @param out
         */
        public void printBanner(PrintStream out) {
            if (this.banner != null) {
                for (String line : this.banner) {
                    out.println(line);
                }
            }
        }
    }
}

2.4.3 Bootstrap实现

package com.beam.work.server.all.boot;

import com.umbrella.work.common.exception.BeemRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.HashMap;
import java.util.Map;

/**
 * Bootstrap
 *
 * @author chenx
 */
@Slf4j
public class Bootstrap {

    private static final String PROPERTY_KEY_APP_IN = "APP_IN";
    private static final String PROPERTY_KEY_APP_OUT = "APP_OUT";
    private static final String PROPERTY_KEY_NACOS_GROUP = "NACOS_GROUP";

    /**
     * VM参数说明:
     * 1. APP_IN(可选): 要启动的服务(多个服务逗号分割);
     * 2. APP_OUT(可选): 不要启动的服务(多个服务逗号分割);
     * 3. NACOS_GROUP(必须):Nacos组名,对应根POM中profile.nacos.group(由于统一启动服务不是Springboot项目因此这里走VM参数配置);
     * 4. 某个服务同时存在于APP_IN和APP_OUT时APP_IN优先;
     * 5. 示例:-DAPP_IN=bw-job,bw-ai-app -DNACOS_GROUP=bw-dev
     * <p>
     * 服务发现机制:
     * 1. 扫SCAN_BASE_PACKAGES下所有含有@BwApplication注解的Springboot启动类;
     * 2. BwApplication.name():服务名称(为空则忽略该服务启动类);
     * 3. BwApplication.contextId():每个子服务的启动都使用 SpringApplicationBuilder 创建的一个新的 ConfigurableApplicationContext 实例,以确保每个服务都在独立的上下文中运行;
     * <p>
     * Nacos服务注册说明:
     * 统一启动服务通过使用 SpringApplicationBuilder 启动了多个独立的 Spring Boot 应用实例,但由于它们共享了同一个 JVM 环境,
     * 而Nacos 客户端使用全局配置单例,这样会导致多个服务实例在同一进程中运行时,它们共享相同的 Nacos 客户端,后面的服务实例在注册时会覆盖前面的注册信息,从而导致只注册了一个服务。
     * 为了解决这个问题需要在 SpringApplicationBuilder 中为每个实例设置不同的 Nacos 配置前缀来隔离它们的注册信息,这样它们在 Nacos 中会被识别为独立的服务实例,从而避免了注册信息的覆盖问题。
     *
     * <p>
     * 支持聚合启动子服务开发规范:
     * 1. SpringBootApplication启动类增加@BwApplication注解;
     * 2. 为避免jar包冲突,所有依赖版本必须使用根POM中统一定义的版本;
     * 3. 服务根包名为:"com.beam", "com.umbrella.work", "com.beem" 范围之一;
     * 4. 服务必须打原包,不能使用SpringBoot 的"FAT JAR"方式打包(skip掉spring-boot-maven-plugin的repackage即可);
     * 5. 代码中禁止通过@Value("${spring.application.name}")获取服务名(原因:统一聚合启动时同样有覆盖问题;解决方案:服务名都是固定的,定义一个常量即可;);
     */
    public static void main(String[] args) {
        try {
            // system properties
            System.setProperty("java.net.preferIPv4Stack", "true");
            String appIn = System.getProperty(PROPERTY_KEY_APP_IN);
            String appOut = System.getProperty(PROPERTY_KEY_APP_OUT);
            String nacosGroup = System.getProperty(PROPERTY_KEY_NACOS_GROUP);
            if (StringUtils.isEmpty(nacosGroup)) {
                throw new BeemRuntimeException("Missing VM-Args NACOS_GROUP!");
            }

            // scan
            long begin = System.currentTimeMillis();
            Map<String, SpringApplicationBuilder> appMap = BootstrapHelper.getApplications(appIn, appOut, nacosGroup);
            if (MapUtils.isEmpty(appMap)) {
                BootstrapHelper.printSeparatedLog(log, "No Valid Application Need to Bootstrap!");
                return;
            }

            // startup
            Map<String, ConfigurableApplicationContext> appContextMap = new HashMap<>();
            for (Map.Entry<String, SpringApplicationBuilder> entry : appMap.entrySet()) {
                String appName = entry.getKey();
                SpringApplicationBuilder builder = entry.getValue();
                ConfigurableApplicationContext applicationContext = builder.run(args);
                appContextMap.putIfAbsent(appName, applicationContext);

                String logInfo = appName + " Startup Done";
                BootstrapHelper.printSeparatedLog(log, logInfo);
            }

            long time = System.currentTimeMillis() - begin;
            BootstrapHelper.BootstrapBanners.START.printBanner(log);
            log.info("All Application Startup Complete. time: {}", time);

            // shutdown
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                for (Map.Entry<String, ConfigurableApplicationContext> entry : appContextMap.entrySet()) {
                    String appName = entry.getKey();
                    ConfigurableApplicationContext appContext = entry.getValue();
                    appContext.close();

                    String logInfo = appName + " Shutdown Done";
                    BootstrapHelper.printSeparatedLog(log, logInfo);
                }
                log.info("All Application Shutdown Complete.");
            }));
        } catch (Exception ex) {
            BootstrapHelper.BootstrapBanners.FAIL.printBanner(log);
            log.error("All Application Startup Error!", ex);
        }
    }
}

3. 验证

3.1 启动验证

在这里插入图片描述
日志太长了,一屏幕截不下;
在这里插入图片描述

3.2 Nacos服务注册验证

1、bw-ai-app服务
在这里插入图片描述
2、bw-job服务
在这里插入图片描述
从nacos中可以看到每个服务都按照预期进行了注册,这样前面的SpringCloud Gateway服务之前配置好个各种routes都不用更改;
在这里插入图片描述

3.3 打包执行验证

3.3.1 打包执行

统一启动服务打包方式需要使用spring-boot-maven-plugin以保障mvn package后 Springboot的相关文件不丢失,例如:META-INF/spring.factories,修改后的POM示例如下:

...
    <dependencies>
        <dependency>
            <groupId>com.umbrella.work</groupId>
            <artifactId>bw-job-provider</artifactId>
        </dependency>
        <dependency>
            <groupId>com.umbrella.work</groupId>
            <artifactId>bw-ai-app-provider</artifactId>
        </dependency>
    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-deploy-plugin</artifactId>
                <version>2.8.2</version>
                <configuration>
                    <skip>true</skip>
                </configuration>
            </plugin>
        </plugins>
    </build>
...

实测打包启动成功:
在这里插入图片描述
在这里插入图片描述

3.3.2 执行内存占用对比

由于当前的方案为在1个进程下启动了多个tomcat应用程序实例,因此一定会因为多个tomcat应用程序实例导致更多的内存占用,对此我首先查了一下,结论是1个tomcat应用程序实例内存占用<50MB。这里我实测的结果为:
启动2个服务内存占用:
在这里插入图片描述
启动1个服务内存占用:
在这里插入图片描述
从测试结果可以看出在linux下多1个tomcat应用程序实例多出的内存不到10MB,因此只要服务不是太多这部分的内存损失还是可以接受的,毕竟如果全部合成1个应用程序就会存在开篇里说的各种事情和问题。

3.3.3 windows打包和运行编码指定

1.确保 Maven 使用 UTF-8 编码来打包项目。在 pom.xml 中添加以下配置来强制编码为 UTF-8:

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

2.设置环境变量
打开系统设置。进入 “系统属性” -> “环境变量”。新建一个系统变量:
变量名:JAVA_TOOL_OPTIONS
变量值:-Dfile.encoding=UTF-8
3. 在运行命令中指定编码
如果环境变量生效的话直接运行 java -jar xxx.jar即可,并且会提示:Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8;
如果环境变量不生效:java -Dfile.encoding=UTF-8 -jar xxx.jar 去指定也可以;idea中的Terminal中需要加双引号:java “-Dfile.encoding=UTF-8” -jar xxx.jar
在这里插入图片描述

4. 结束语

上面就是说了:“出于时间问题,我就不写一个完成的Demo了。方法和套路懂了,自行就能写出测试验证Demo。”,现在回过头来看,这玩意真的不难,不过如果没有想到又或者没有参考的话,好像很多人还是真不知道咋弄。毕竟SpringApplicationBuilder不常被用到,毕竟SpringApplication.run(XXX.class, args)写法上太简单,执行却又太重了,里面的1234567好像不去专门背几个八股文,又真的能有几个人能条理清晰的说出其要点呢(SpringBoot不就是一个希望大家使用简单的开发框架吗,TMD,面试却卷的要死)?所以我觉得就算是偷懒也得给出个实例参考(毕竟在我看来,不给出一个完整的Demo好像并不是那么的对读者负责,因此我也只能有图有真相了)。

在这里插入图片描述

封面由微软AI生成,一塌糊涂。。。

5. 后续

5.1 多线程启动

上面的方式都能满足开篇背景中的预期:一个进程下启动多个Springboot项目,并且在nacos和gateway层面看起来仍然是多个服务;并且服务的运行其实也在多个tomcat实例下隔离的。一切看起来都很好,不过本着做事做完美的角度来说最好更近一步:

  • 多线程启动各个子服务;
  • 各个子服务日志隔离;

在又经历了一些研究和思考之后终于得以解决,这里启动3个服务时间跟启动1个服务的时间基本相同(QuickBootstrap就是多线程启动入口),不过解决方案这里我卖个关子,不过可以明确的告诉大家肯定可以实现并且改动不大;主要的核心问题在于:1)如何实现各个子服务的日志分离(方案有几种);2)如果碰到日志的“Collisions detected” 问题又该如何规避;单线线程串行启动3个子服务和多线程并行启动3个子服务对比如下:

  • 多线程启动效果
    在这里插入图片描述
  • 单线程启动效果
    在这里插入图片描述

5.2 各子服务Spring配置隔离

在之前的子服务开发规范定义了1条规则:代码中禁止通过@Value(“${spring.application.name}”)获取服务名(解决方案:服务名都是固定的,定义一个常量即可;)。其实这是个霸王条款,并且是本绕过问题而不是解决问题的方式给出的。在后续随着更多的服务集成到统一执行和部署方式之后发现这个问题不能绕过,因此这里补充给出解决方案:

在构造SpringApplicationBuilder时,为每个子服务单独指定配置文件Path即可:

String springConfigPath = "classpath:" + appName + "-bootstrap.yml";
SpringApplicationBuilder builder = new SpringApplicationBuilder();
            builder.sources(clazz)
                    .environment(new StandardEnvironment())
                    .properties("spring.application.name=" + appName
                            ...
                            , "spring.config.location=" + springConfigPath)
                    .web(WebApplicationType.SERVLET)

5.3 启动提速

关于启动提速最开始想到的简单暴力的方法就是多线程启动,不过在实践的过程中发现即使为每个服务单独指定了logback-spring.xml,仍然会出现日志“串”的情况,所以最后还是放弃了。毕竟服务多了,单线程一个一个的串行启动其时间肯定不会好,为了降低单线程方式的服务启动时间,我做了几种尝试:1、设置父ApplicationContext(可以把Springboot的ApplicationContext理解一个容器,并且可以向上不断继承);2、设置共享Bean。

最后发现最为稳妥的方式是:手工去设置共享Bean,因为设置父ApplicationContext一旦控制不好就很容易出现各种问题,例如:某些服务启动失败、某些东西又“串”了等。不过主要还是我偷懒,对于父ApplicationContext的构造,我用了一个加载了我们自己公共组件的空服务去构造,并且不使用SERVLET)。

对于手工去设置共享Bean的方式,当前我只是把:RedissonClient、DruidDataSource、ClockDiffService(自定义的一个从DB里取当前时间戳)这3个东西改为了共享Bean的方式,20多个服务的总启动时间就可以减少100多秒(毕竟光一个初始化DB连接就得几秒);
下面我简单说下这2种方式的大致实现。

5.3.1 设置父ApplicationContext

伪代码如下:

// 根据需要构造parentContext
ConfigurableApplicationContext parentContext = getParentContext(...);

// 为每个服务设置parentContext
SpringApplicationBuilder builder = new SpringApplicationBuilder();
            builder.sources(clazz)
                   ...
                    .web(WebApplicationType.SERVLET)
                    .parent(parentContext)
                    ...
                    ;

5.3.2 设置共享Bean

主要代码如下:

/**
     * startup
     *
     * @param appMap
     * @param startedAppCounter
     * @param log
     * @return
     */
    public static Map<BwApplication, ConfigurableApplicationContext> startup(Map<BwApplication, SpringApplicationBuilder> appMap,
                                                                             AtomicInteger startedAppCounter,
                                                                             Logger log) {
        Map<BwApplication, ConfigurableApplicationContext> appContextMap = new HashMap<>();
        if (MapUtils.isEmpty(appMap)) {
            log.warn("No SpringApplication Need To Startup!");
            return appContextMap;
        }

        int appMapSize = appMap.size();
        SharedBeanInitializer initializer = null;
        for (Map.Entry<BwApplication, SpringApplicationBuilder> entry : appMap.entrySet()) {
            long begin = System.currentTimeMillis();
            String appName = entry.getKey().name();
            SpringApplicationBuilder builder = entry.getValue();
            log.info("[{}] Startup Begin.", appName);

            if (Objects.nonNull(initializer)) {
                builder.initializers(initializer);
            }

            ConfigurableApplicationContext applicationContext = builder.run(ArrayUtils.EMPTY_STRING_ARRAY);

            // init shared beans:从第一个启动服务的context中提取需要共享的bean
            if (Objects.isNull(initializer)) {
                RedissonClient redissonClient = applicationContext.getBean(RedissonClient.class);
                DruidDataSource druidDataSource = applicationContext.getBean(DruidDataSource.class);
                ClockDiffService clockDiffService = applicationContext.getBean(ClockDiffService.class);
                initializer = new SharedBeanInitializer(redissonClient, druidDataSource, clockDiffService);
                log.info("{} Init done.", SharedBeanInitializer.class.getName());
            }

            appContextMap.putIfAbsent(entry.getKey(), applicationContext);
            int index = startedAppCounter.incrementAndGet();
            String logInfo = "[" + appName + "] Startup Done, Time: [" + (System.currentTimeMillis() - begin) + "], index: [" + index + "/" + appMapSize + "]";
            BootstrapHelper.printSeparatedLog(log, logInfo);
        }

        return appContextMap;
    }

SharedBeanInitializer

import com.alibaba.druid.pool.DruidDataSource;
import com.beam.work.web.base.service.ClockDiffService;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;

import java.beans.Introspector;

/**
 * SharedBeanInitializer
 *
 * @author chenx
 */
public class SharedBeanInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    private final RedissonClient redissonClient;
    private final DruidDataSource druidDataSource;
    private final ClockDiffService clockDiffService;

    public SharedBeanInitializer(RedissonClient redissonClient, DruidDataSource druidDataSource, ClockDiffService clockDiffService) {
        this.redissonClient = redissonClient;
        this.druidDataSource = druidDataSource;
        this.clockDiffService = clockDiffService;
    }

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
        beanFactory.registerSingleton(this.getSpringBeanName(this.redissonClient), this.redissonClient);
        beanFactory.registerSingleton(this.getSpringBeanName(this.druidDataSource), this.druidDataSource);
        beanFactory.registerSingleton(this.getSpringBeanName(this.clockDiffService), this.clockDiffService);
    }

    /**
     * getSpringBeanName(Spring 的默认命名规则就是使用java.beans.Introspector处理)
     *
     * @param bean
     * @return
     */
    private String getSpringBeanName(Object bean) {
        return Introspector.decapitalize(bean.getClass().getSimpleName());
    }
}

本文章已经生成可运行项目
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BossFriday

原创不易,请给作者打赏或点赞!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值