将多个SpringBoot / 微服务应用合并成一个SpringBoot应用

news/2024/9/25 14:28:15

前言

当下在设计大型系统或网站时,为了满足系统的灵活性、扩展性、模块化、松耦合、高可用等特性,在技术架构选择时往往会选用微服务架构。独立服务的拆分会增加部署时机器资源的消耗。在轻量化部署场景的催化下,需要考虑中间件的缩减以及微服务应用的合并部署,已达到降低对服务器资源的依赖。

项目结构

我们的项目工程结构如下所示,其中xxx代表一个独立的微服务 ,整个工程由多个独立的微服务模块组成,这里只举例说明,没有列举完整的项目结构,api-xxx模块表示某个独立的微服务的后台管理能力,provider-xxx模块表示某个独立微服务对其它服务提供能力的模块。

- project-parent- api-xxx- provider-xxx

应用合并需要考虑的问题

因为系统整体基于微服务构建,在进行应用合并实现资源减配时,主要考虑将apiprovider应用进行合并,遇到的主要问题如下:

  1. apiprovider从业务角度属于同一个,所以重名的类较多,因此会导致Spring容器中的beanName重复
  2. ORM框架用的是JPAHibernate中的实体只有类名没有包路径,类名重复会导致JPA中的实体重复
  3. SpringMVC中注册的接口请求路径重复的问题
  4. apiprovider合并为一个服务后,其它应用通过RPC调用provider服务的服务名需要调整
  5. 其它一些由业务和技术特性决定的不具备普遍性的问题,这里不加赘述
    面临上面的问题,如果在一个SpringBoot模块中,直接通过Mavenapi-xxx模块provider-xxx模块引入后启动肯定会报错的。

应用合并合并

基于以上问题,理想状态是在一个JVM里面启动两个Spring容器,分别对应apiprovider,减少对服务器资源需求的同时最大程度保留原有的技术架构。
支持多个应用同时启动的容器类,这是一个抽象类,需要由具体启动的应用继承后设置应用名称和SpringBoot的Application类:

public abstract class MultipleServiceRunner {private ConfigurableApplicationContext applicationContext;private final String applicationName;private final Class<?>[] applicationClasses;private String[] args;private final static Object lock = new Object();private Boolean wait = Boolean.FALSE;public MultipleServiceRunner(String applicationName, Class<?>... applicationClasses) {this.applicationName = applicationName;this.applicationClasses = applicationClasses;}public void setArgs(String[] args) {this.args = args;}public void run() {if(applicationContext != null) {throw new IllegalStateException("AppContext must be null to run this backend");}runBackendInThread();waitUntilBackendIsStarted();}private void waitUntilBackendIsStarted() {try {synchronized (lock) {if(wait) {lock.wait();}}} catch (InterruptedException e) {throw new IllegalStateException(e);}}private void runBackendInThread() {final Thread runnerThread = new ApplicationRunner(applicationName);wait = Boolean.TRUE;runnerThread.setContextClassLoader(applicationClasses[0].getClassLoader());runnerThread.start();}public void stop() {if (Optional.ofNullable(applicationContext).isPresent()) {SpringApplication.exit(applicationContext);applicationContext = null;}}protected class ApplicationRunner extends Thread {public ApplicationRunner(String name) {super(name);}@Overridepublic void run() {applicationContext = SpringApplication.run(applicationClasses, args);synchronized (lock) {wait = Boolean.FALSE;lock.notify();}}}}

扫描MultipleServiceRunner的子类,启动SpringBoot容器:

public class MultipleServiceStarter {private final static List<Container> containers = new ArrayList<>(4);private final static String RUNNER_PACKAGE = "com.xxx";protected static Set<Class<?>> scan() throws IOException, ClassNotFoundException {Set<Class<?>> classes = new HashSet<>();ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +ClassUtils.convertClassNameToResourcePath(RUNNER_PACKAGE) + "/**/*.class";Resource[] resources = resourcePatternResolver.getResources(pattern);//MetadataReader 的工厂类MetadataReaderFactory readerfactory = new CachingMetadataReaderFactory(resourcePatternResolver);for (Resource resource : resources) {//用于读取类信息MetadataReader reader = readerfactory.getMetadataReader(resource);//扫描到的classString classname = reader.getClassMetadata().getClassName();Class<?> clazz = Class.forName(classname);if (MultipleServiceRunner.class.isAssignableFrom(clazz) && !Objects.equals(MultipleServiceRunner.class, clazz)) {classes.add(clazz);}}return classes;}public static void start(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {Set<Class<?>> runnerClasses = scan();for (Class<?> runnerClass : runnerClasses) {MultipleServiceRunner runnerInstance = (MultipleServiceRunner) runnerClass.newInstance();containers.add(new Container(runnerClass, runnerInstance));runnerInstance.setArgs(args);runnerInstance.run();}}public static void stop() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {for (Container container : containers) {container.runnerInstance.stop();}}protected static class Container {private Class<?> runnerClass;private MultipleServiceRunner runnerInstance;public Container(Class<?> runnerClass, MultipleServiceRunner runnerInstance) {this.runnerClass = runnerClass;this.runnerInstance = runnerInstance;}}}

主程序启动类:

public class LiteLauncherApplication {public static void main(String[] args) throws IOException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {MultipleServiceStarter.start(args);Runtime.getRuntime().addShutdownHook(new Thread(() -> {try {MultipleServiceStarter.stop();} catch (Exception e) {e.printStackTrace();}}));}}

微服务改造

新增lite-xxx模块,Maven引入apiprovider模块,修改打包插件,指定程序入口,由于公司安全政策原因已对敏感信息进行脱敏:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.xxx</groupId><artifactId>parent</artifactId><version>4.8.0-SNAPSHOT</version></parent><groupId>com.xxx</groupId><artifactId>lite-xxx</artifactId><properties><java.version>1.8</java.version><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>com.xxx</groupId><artifactId>service-xxx</artifactId><version>${xxx.version}</version></dependency><dependency><groupId>com.xxx</groupId><artifactId>provider-xxx</artifactId><version>${xxx.version}</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><executions><execution><id>unpack-some-artifact</id><phase>prepare-package</phase><goals><goal>unpack</goal></goals><configuration><artifactItems><artifactItem><groupId>com.xxx</groupId><artifactId>service-xxx</artifactId><type>jar</type><overWrite>true</overWrite><outputDirectory>${project.build.directory}/classes</outputDirectory><includes>**/*</includes><excludes>*.properties,logback-spring.xml</excludes></artifactItem><artifactItem><groupId>com.xxx</groupId><artifactId>provider-xxx</artifactId><type>jar</type><overWrite>true</overWrite><outputDirectory>${project.build.directory}/classes</outputDirectory><includes>**/*</includes><excludes>*.properties,logback-spring.xml</excludes></artifactItem></artifactItems></configuration></execution></executions></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><mainClass>com.xxx.LiteLauncherApplication</mainClass></configuration><executions><execution><goals><goal>repackage</goal><!--可以把依赖的包都打包到生成的Jar包中--></goals></execution></executions></plugin></plugins></build></project>

api模块容器类:

public class ApiContainerRunner extends MultipleServiceRunner {public ApiContainerRunner() {super("api-xxx", ApiApplication.class);System.setProperty("spring.profiles.active", "release");System.setProperty("spring.application.name", "xxx");System.setProperty("spring.cloud.nacos.config.group", "xxx");System.setProperty("spring.datasource.master.jpa.packageToScan", "com.xxx.servicexxx.bean,com.xxx.servicexxx.bean");}
}

api模块Application类,保留关键注解,一是引入配置文件,二是Spring扫描bean时排除掉provider模块下的类否则还是会出现beanName重复:

@SpringBootApplication
@PropertySource(value = {"classpath:bootstrap-release.properties"})
@ComponentScan(nameGenerator = VersionAnnotationBeanNameGenerator.class, basePackages="com.xxx.*",excludeFilters = {@ComponentScan.Filter(type = FilterType.REGEX, pattern = {"com.xxx.providerxxx.*","com.xxx.servicedxxx.*"})}
)
public class ApiApplication {public static void main(String[] args) {SpringApplication.run(ApiApplication.class, args);}}

provider模块的Run和Application参考实现即可。
通过com.xxx.LiteLauncherApplication类启动服务,会看到apiprovider模块依次启动成功,至此应用合并完成

注意事项

应用合并后,大家要理解本质是在同一个JVM中启动了两个Spring容器/Spring Context,如果有些代码实现是JVM全局的,可能会涉及到部分代码调整。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/64574.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

一万字全面解析CRM的定义、分类与核心价值

1、CRM定义与分类 1.1CRM的定义 CRM,英文Customer Relationship Management的缩写,中文全称为客户关系管理。通常情况下,人们通常用CRM直接表达客户关系管理软件系统——一个以客户为中心的专门用于管理与客户关系的软件工具,以确保与客户在营销、销售、服务的每一环节上都…

串口属性中的BM延时计时器问题

天道酬勤 循序渐进 技压群雄

MUX VLAN

MUX VLAN 作用在二层,通过动态调整VLAN的划分和分配,实现了更高效的网络管理 主VLAN 扮演核心角色,负责承载主要的网络流量和服务 所有的从VLAN都与主VLAN相关联,通过主VLAN进行通信和数据传输 从VLAN 互通型从VLAN(Group VLAN) 这些VLAN内的设备可以相互通信 隔离型从VL…

module collections has no attribute Hashable PyDocx 库报错

### 项目背景在测试PyDocx代码时```python from pydocx import PyDocXhtml = PyDocX.to_html("test.docx") with项目背景 在测试PyDocx代码时 ```python from pydocx import PyDocX html = PyDocX.to_html("test.docx") with open("test.html", …

SimpleAIAgent:使用免费的glm-4-flash即可开始构建简单的AI Agent应用FI

合集 - C#(80)1.使用C#将几个Excel文件合并去重分类2023-11-152.C#使用SqlSugar操作MySQL数据库实现简单的增删改查2023-11-163.C#中的类和继承2023-11-174.C#中的virtual和override关键字2023-11-175.C#中的属性2023-11-206.C#winform中使用SQLite数据库2023-11-237.C#简化工作…

VLAN原理和配置

VLAN原理和配置 VLAN:虚拟局域网,将一个物理的局域网在逻辑上划分成多个广播域 华为交换机默认4094个VLAN 在交换机上配置VLAN,同一个VLAN内的用户可以进行二层互访,而不同VLAN 间的用户被二层隔离 VLAN帧格式 Tag用于区分不同的VLAN 没有携带Tag的帧DMAC SMAC Type Data F…

ddsadasdasd

目录理论部分 Ceph的诞生主要是为了解决以下问题: 操作部分 第一部分(虚拟机配置) 一、修改主机名 二、修改防火墙、SELinux状态 三、修改hosts文件 四、验证网络环境(请参阅 第一步、第四步) 五、配置 ceph 源 六、开始执行yum安装 七、创建目录 第二部分(部署ceph) 1…

.net 到底行不行!2000 人在线的客服系统真实屏录演示(附技术详解)

时常有朋友问我性能方面的问题,正好有一个真实客户,在线的访客数量达到了 2000 人。在争得客户同意后,我录了一个视频。升讯威在线客服系统可以在极低配置的服务器环境下,轻松应对这种情况,依然可以做到消息毫秒级送达,操作毫秒级响应。业余时间用 .net 写了一个免费的在…