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

前言

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

项目结构

我们的项目工程结构如下所示,其中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);
        }

        @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引入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全局的,可能会涉及到部分代码调整。

请登录后发表评论

    没有回复内容