前言
当下在设计大型系统或网站时,为了满足系统的灵活性、扩展性、模块化、松耦合、高可用等特性,在技术架构选择时往往会选用微服务架构。独立服务的拆分会增加部署时机器资源的消耗。在轻量化部署场景的催化下,需要考虑中间件的缩减以及微服务应用的合并部署,已达到降低对服务器资源的依赖。
项目结构
我们的项目工程结构如下所示,其中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);}@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引入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全局的,可能会涉及到部分代码调整。