将多个SpringBoot / 微服务应用合并成一个SpringBoot应用
前言
当下在设计大型系统或网站时,为了满足系统的灵活性、扩展性、模块化、松耦合、高可用等特性,在技术架构选择时往往会选用微服务架构。独立服务的拆分会增加部署时机器资源的消耗。在轻量化部署场景的催化下,需要考虑中间件的缩减以及微服务应用的合并部署,已达到降低对服务器资源的依赖。
项目结构
我们的项目工程结构如下所示,其中xxx
代表一个独立的微服务 ,整个工程由多个独立的微服务模块组成,这里只举例说明,没有列举完整的项目结构,api-xxx
模块表示某个独立的微服务的后台管理能力,provider-xxx
模块表示某个独立微服务对其它服务提供能力的模块。
- project-parent
- api-xxx
- provider-xxx
应用合并需要考虑的问题
因为系统整体基于微服务构建,在进行应用合并实现资源减配时,主要考虑将api
和provider
应用进行合并,遇到的主要问题如下:
api
和provider
从业务角度属于同一个,所以重名的类较多,因此会导致Spring
容器中的beanName
重复- ORM框架用的是
JPA
,Hibernate
中的实体只有类名没有包路径,类名重复会导致JPA
中的实体重复 SpringMVC
中注册的接口请求路径重复的问题- 将
api
和provider
合并为一个服务后,其它应用通过RPC调用provider
服务的服务名需要调整 - 其它一些由业务和技术特性决定的不具备普遍性的问题,这里不加赘述
面临上面的问题,如果在一个SpringBoot模块
中,直接通过Maven
将api-xxx模块
和provider-xxx模块
引入后启动肯定会报错的。
应用合并合并
基于以上问题,理想状态是在一个JVM里面启动两个Spring容器,分别对应api
和provider
,减少对服务器资源需求的同时最大程度保留原有的技术架构。
支持多个应用同时启动的容器类,这是一个抽象类,需要由具体启动的应用继承后设置应用名称和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);
}
@Override
public 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);
//扫描到的class
String 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引入api
和provider
模块,修改打包插件,指定程序入口,由于公司安全政策原因已对敏感信息进行脱敏:
<?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
类启动服务,会看到api
和provider
模块依次启动成功,至此应用合并完成
注意事项
应用合并后,大家要理解本质是在同一个JVM
中启动了两个Spring容器/Spring Context
,如果有些代码实现是JVM全局的,可能会涉及到部分代码调整。