当前位置: 首页 > news >正文

多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}”)获取服务名(解决方案:服务名都是固定的,定义一个常量即可;);

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 propertiesSystem.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!");}// scanlong 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;}// startupMap<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);// shutdownRuntime.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都不用更改;
在这里插入图片描述

4. 结束语

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

在这里插入图片描述

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


http://www.mrgr.cn/news/63529.html

相关文章:

  • 在网络营销中你犯过的最大的错误是什么?
  • GBDT 算法的原理推导
  • windows@命令行中获取环境变量取值不展开取值(原值)
  • 【制造业&盒子】食品物品检测系统源码&数据集全套:改进yolo11-MultiSEAMHead
  • 树莓派基本设置--2. raspi-config工具介绍
  • 使用Java内存级方式 和 Redis Lua 脚本方式实现滑动窗口限流
  • 华为认证HCIE通过需要考到多少分?_博睿谷·博睿慕课
  • layui 自定义验证单选框必填
  • Raspberry Pi 树莓派产品系列说明
  • Django入门教程——用户管理实现
  • 【C语言学习笔记】
  • 现货白银实时行情怎么看?首先搞清楚这个原则
  • @Async(“asyncTaskExecutor“) 注解介绍
  • 链栈的引用
  • C# 两个不同文件路径的同步
  • Latex中Reference的卷号加粗的问题
  • 指令系统 II(程序的机器级代码表示、CISC 和 RISC)
  • 写一个小日历
  • 中电金信:GienTech动态|丰收之秋,公司多项目获得荣誉
  • 如何解决docker镜像下载失败问题
  • (9)位运算
  • 用友U8采购入库单与旺店通·企业奇门集成方案解析
  • [CSP篇] CSP2024 游记(下)
  • 机器学习:我们能用机器学习来建立投资模型吗
  • C++模拟实现list
  • 第5章第6章 Servlet技术