万籁俱寂,万字将成。
刘耀文
Stay hungry. Stay foolish.
© 2024-2026
Powered by Mix Space&
余白 / Yohaku
.
正在被0人看爆
关于
关于本站关于我
更多
时间线友链
联系
写留言发邮件 ↗
刘耀文
Stay hungry. Stay foolish.
链接
关于本站·关于我·时间线·友链·写留言·发邮件
© 2024-2026 Powered by Mix Space&
余白 / Yohaku
.
正在被0人看爆
赣ICP备2024031666号
RSS 订阅·站点地图·
··|
RSS 订阅·站点地图·|··|赣ICP备2024031666号
稍候片刻,月出文自明。

Koupleless 合并部署完整教程

4
AI·GEN

关键洞察

Koupleless 合并部署完整教程

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • 目录

    1. Koupleless 简介
    2. 合并部署概述
    3. 环境准备
    4. 基座改造
    5. 模块改造
    6. 模块瘦身
    7. 部署与验证
    8. 常见问题与解决方案
    9. 最佳实践
    10. 生产环境部署

    Koupleless 简介

    Koupleless 是一种模块化的 Serverless 技术解决方案,它能让普通应用以较低的代价演进为 Serverless 研发模式,让代码与资源解耦,轻松独立维护,同时支持秒级构建部署、合并部署、动态伸缩等能力,为用户提供极致的研发运维体验。

    适用场景

    1. 应用构建发布慢或 SDK 升级繁琐:传统应用构建发布需要 6-10 分钟,使用 Koupless 可降至 10 秒级
    2. 长尾应用资源浪费:企业 80% 的应用 CPU 使用率低于 10%,合并部署可显著降低资源成本
    3. 研发协作效率低:多人开发一个应用时,需要统一时间窗口发布, Koupless 支持模块独立迭代
    4. 中台应用难以沉淀业务资产:通过基座沉淀公共能力,模块实现具体业务逻辑
    5. 微服务演进成本高:支持应用在单体、模块化、微服务架构间平滑过渡

    合并部署概述

    什么是合并部署

    合并部署是指将多个独立的应用(在 Koupless 中称为"模块")部署到同一个 JVM 进程中,共享基座的资源和依赖,但保持代码和运行时的隔离。

    合并部署的优势

    • 节省资源:多个模块共享基座的内存(Metaspace 和 Heap),CPU 使用率有效提升
    • 快速启动:模块构建产物从数百 MB 瘦身到几十 MB,启动时间大幅缩短
    • 简化运维:统一管理多个模块,降低维护成本
    • 灵活扩展:支持动态添加、移除模块,无需重启基座

    架构原理

    Koupleless 基于 SOFAArk 框架实现类隔离和合并部署:

    • 基座(Base):提供公共依赖和基础能力,如 SpringBoot 框架、中间件 SDK 等
    • 模块(Biz):业务功能模块,依赖基座提供的公共能力
    • 类加载机制:模块优先从自己的 ClassLoader 查找类,找不到再委托给基座 ClassLoader

    环境准备

    前置条件

    • JDK 8 或更高版本
    • Maven 3.6+
    • SpringBoot 2.x 或 3.x
    • 可用的 IDE(IntelliJ IDEA、Eclipse 等)

    版本选择

    当前 Koupleless 主要版本:

    CodeBlock Loading...

    工具准备

    下载 Arkctl 工具(用于模块部署):

    CodeBlock Loading...

    基座改造

    1. 添加依赖

    在基座的 pom.xml 中添加 Koupleless 基座依赖:

    CodeBlock Loading...

    2. 配置应用名

    在 application.properties 或 application.yml 中配置应用名:

    CodeBlock Loading...

    3. 配置基座构建插件

    在 pom.xml 的 build 部分添加基座构建插件:

    CodeBlock Loading...

    4. 生成依赖 Starter

    执行以下命令生成基座的依赖 starter:

    CodeBlock Loading...

    这个命令会生成一个 ${baseAppName}-dependencies-starter 的依赖包,模块项目可以将其作为 parent 来继承基座的所有依赖。

    5. 启动基座

    完成上述配置后,可以直接通过 IDE 或命令行启动基座:

    CodeBlock Loading...

    模块改造

    1. 创建模块项目

    模块可以是现有的 SpringBoot 应用,也可以是新创建的项目。建议使用 Maven 多模块结构管理多个模块。

    2. 配置模块依赖

    在模块的 pom.xml 中添加模块依赖和打包插件:

    CodeBlock Loading...

    3. 配置打包插件

    在模块的 pom.xml 中添加 SOFAArk 打包插件:

    CodeBlock Loading...

    4. 配置模块瘦身

    下载自动排包配置文件 rules.txt,放在模块的 conf/ark/ 目录下:

    CodeBlock Loading...

    rules.txt 内容示例:

    CodeBlock Loading...

    5. 开发模块代码

    创建模块的业务代码,例如 REST Controller:

    CodeBlock Loading...

    6. 配置应用名

    在模块的 application.properties 中配置应用名:

    CodeBlock Loading...

    模块瘦身

    为什么要模块瘦身

    模块瘦身可以:

    • 减小模块包大小:从数百 MB 降低到几十 MB
    • 减少内存占用:降低 Metaspace 占用,基座可以安装更多模块
    • 加快启动速度:模块启动时间从分钟级降低到秒级
    • 提高部署效率:小体积包更容易传输和部署

    瘦身原理

    模块瘦身的核心思想是:移除模块中与基座重复的依赖,让模块复用基座的类

    通过以下方式实现:

    1. 继承基座依赖 starter:模块以 ${baseAppName}-dependencies-starter 作为 parent
    2. 自动排包:使用 rules.txt 配置文件排除不需要的依赖
    3. 依赖委托:模块依赖设置为 provided 作用域,委托给基座加载

    配置方法

    方法一:继承基座依赖 starter(推荐)

    在模块的 pom.xml 中配置:

    CodeBlock Loading...

    这样模块会自动继承基座的所有依赖,无需手动配置版本。

    方法二:手动配置依赖作用域

    在模块的 pom.xml 中,将基座已有的依赖设置为 provided 作用域:

    CodeBlock Loading...

    方法三:使用排除配置

    在 sofa-ark-maven-plugin 中配置排除项:

    CodeBlock Loading...

    验证瘦身效果

    构建模块并查看包大小:

    CodeBlock Loading...

    可以看到,瘦身后的 ark-biz.jar 从 180M 降低到 15M,效果显著。


    部署与验证

    1. 启动基座

    首先启动基座应用:

    CodeBlock Loading...

    或使用 IDE 启动主类。

    2. 构建模块

    在模块项目中执行构建:

    CodeBlock Loading...

    构建成功后会生成两个 jar 包:

    • module1-1.0.0.jar:普通 SpringBoot fat jar,可独立运行
    • module1-1.0.0-ark-biz.jar:Koupleless 模块包,用于合并部署

    3. 使用 Arkctl 部署模块

    使用 Arkctl 工具将模块部署到基座:

    CodeBlock Loading...

    4. 验证部署

    部署成功后,可以通过以下方式验证:

    查看模块状态

    CodeBlock Loading...

    输出示例:

    CodeBlock Loading...

    访问模块接口

    CodeBlock Loading...

    查看日志

    在基座控制台可以看到模块启动日志:

    CodeBlock Loading...

    5. 部署多个模块

    重复上述步骤部署 module2:

    CodeBlock Loading...

    验证多模块部署:

    CodeBlock Loading...

    6. 卸载模块

    CodeBlock Loading...

    常见问题与解决方案

    1. 类冲突问题

    问题描述:模块和基座存在相同类名的类,导致类冲突。

    解决方案:

    • 使用类隔离机制,模块优先加载自己的类
    • 公共类下沉到基座,模块通过依赖委托复用
    • 避免在模块和基座中定义相同包名和类名的类

    2. 静态字段共享问题

    问题描述:多个模块共享静态字段,导致数据混乱。

    解决方案:

    使用 StaticFieldMapWrapper 适配静态字段:

    CodeBlock Loading...

    3. Dubbo 兼容性问题

    问题描述:模块中使用 Dubbo 泛化调用时出现问题。

    解决方案:

    • 确保 Dubbo 相关依赖下沉到基座
    • 为 Dubbo 配置正确的 ClassLoader 切换
    • 参考官方 adapter 仓库的解决方案

    4. Nacos 线程过多

    问题描述:每个模块启动时创建 Nacos 客户端,导致线程数过多。

    解决方案:

    在基座中缓存 Nacos 客户端:

    CodeBlock Loading...

    5. 模块启动顺序问题

    问题描述:模块之间有依赖关系,需要按顺序启动。

    解决方案:

    • 在基座中配置模块启动顺序
    • 使用依赖关系控制启动顺序
    • 避免循环依赖

    6. 内存泄漏

    问题描述:模块卸载后,内存没有释放,导致 Metaspace 持续增长。

    解决方案:

    • 确保模块卸载时清理所有资源(线程池、连接等)
    • 使用弱引用管理缓存
    • 定期重启基座或进行内存整理

    7. Spring 上下文冲突

    问题描述:模块和基座的 Spring Bean 冲突。

    解决方案:

    • 使用不同的 Bean 名称
    • 模块中使用 @Primary 注解明确优先级
    • 避免模块和基座定义相同的 Bean ---

    生产环境部署

    1. 部署架构

    生产环境推荐部署架构:

    CodeBlock Loading...

    2. K8s 部署配置

    基座 Deployment

    CodeBlock Loading...

    模块发布流水线

    CodeBlock Loading...

    3. 模块动态部署

    方案一:OSS 监听模式

    基座监听 OSS 目录变化,自动部署新模块:

    CodeBlock Loading...

    方案二:配置中心模式

    通过配置中心控制模块部署:

    CodeBlock Loading...

    基座监听配置变化:

    CodeBlock Loading...

    下面把教程里没展开的两件事一次讲透:

    1. 静态合并部署(Static Packaged Deployment)——一次性把基座 + N 个模块打成一个可执行 Fat Jar,启动即“全员就位”;
    2. 端口隔离——让每个模块真的监听自己的端口,实现“进程内多 WebServer”。

    ------------------------------------------------

    方案三、静态合并部署(Static Packaged Deployment)

    适用场景

    • 上线流程极简,不允许运维侧再敲 arkctl deploy;
    • 容器镜像只启一个进程,K8s liveness/readiness 探针直接探测基座即可;
    • 模块版本固定,不需要热卸载/热加载。
    1. 打包思路
      基座 pom 里把各模块的 ark-biz.jar 当资源一起打进 BOOT-INF/lib 目录;
      基座启动时借助 SOFAArk 的 “static-biz” 机制,让这些 ark-biz.jar 随基座生命周期一起 install + start。

    2. 实操步骤
      step-1 模块端正常 mvn package 得到 moduleX-1.0.0-ark-biz.jar
      step-2 基座 pom 增加 profile(只在打静态包时激活)

    CodeBlock Loading...

    step-3 基座启动类加 @StaticBiz 注解(0.5.6+ 支持),告诉 SOFAArk 把 classpath 下所有 ark-biz.jar 静态安装:

    CodeBlock Loading...

    step-4 打静态包

    CodeBlock Loading...

    生成的 base-application-1.0.0-exec.jar 里同时包含 module1-ark-biz.jar & module2-ark-biz.jar;
    java -jar base-application-1.0.0-exec.jar 启动后,arkctl status 能看到两个模块已经是 ACTIVATED,无需再 deploy。

    让模块监听自己的端口

    默认行为

    • 所有模块与基座共用同一个嵌入式 Tomcat(端口 8080),仅靠 webContextPath 区分;
    • 好处:节省线程、节省内存;
    • 坏处:无法“端口级”隔离,也做不到“模块单独暴露管理口”。

    Koupleless 0.5.6 开始支持 “多 WebServer 模式”,即每个模块可以起独立的 Netty/Tomcat,真正 bind 自己的端口。

    1. 模块端增加依赖
    CodeBlock Loading...
    1. 配置端口 & 上下文
    CodeBlock Loading...
    1. 打包插件声明
    CodeBlock Loading...
    1. 部署验证
      基座仍跑 8080,module1 会额外监听 8081,module2 可再配 8082,互不影响;
      curl http://localhost:8081/ 直接返回模块 1 的响应,无需再加前缀。

    注意

    • 端口多占 n 个,内存/线程也会相应增加,按需权衡;
    • 生产环境需在 K8s Service 里把 8081、8082… 也声明出来,或走 Istio 多端口即可。

    参考资源

    • 官方文档:https://koupleless.io/docs/
    • GitHub 仓库:https://github.com/koupleless/koupleless
    • 示例代码:https://github.com/koupleless/samples
    • 社区支持:https://github.com/koupleless/koupleless/issues
    XML
    <koupleless.runtime.version>0.5.6</koupleless.runtime.version>
    <sofa.ark.version>2.2.14</sofa.ark.version>
    
    # Mac/Linux
    wget https://github.com/koupleless/koupleless/releases/download/arkctl-release-0.2.0/arkctl-darwin-amd64
    mv arkctl-darwin-amd64 /usr/local/bin/arkctl
    chmod +x /usr/local/bin/arkctl
    
    # Linux
    wget https://github.com/koupleless/koupleless/releases/download/arkctl-release-0.2.0/arkctl-linux-amd64
    mv arkctl-linux-amd64 /usr/local/bin/arkctl
    chmod +x /usr/local/bin/arkctl
    
    XML
    <properties>
        <koupleless.runtime.version>0.5.6</koupleless.runtime.version>
        <sofa.ark.version>2.2.14</sofa.ark.version>
    </properties>
    
    <dependencies>
        <!-- Koupleless 基座启动器 -->
        <dependency>
            <groupId>com.alipay.sofa.koupleless</groupId>
            <artifactId>koupleless-base-starter</artifactId>
            <version>${koupleless.runtime.version}</version>
        </dependency>
        
        <!-- Web 插件(支持多 Web 模块合并部署) -->
        <dependency>
            <groupId>com.alipay.sofa</groupId>
            <artifactId>web-ark-plugin</artifactId>
        </dependency>
    </dependencies>
    
    PROPERTIES
    # application.properties
    spring.application.name=base-application
    
    XML
    <build>
        <plugins>
            <!-- Koupleless 基座构建插件 -->
            <plugin>
                <groupId>com.alipay.sofa.koupleless</groupId>
                <artifactId>koupleless-base-build-plugin</artifactId>
                <version>${koupleless.runtime.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>add-patch</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <!-- 生成 starter 的 artifactId,需要替换为实际的应用名 -->
                    <dependencyArtifactId>${baseAppName}-dependencies-starter</dependencyArtifactId>
                    <!-- 生成 jar 的版本号 -->
                    <dependencyVersion>0.0.1-SNAPSHOT</dependencyVersion>
                </configuration>
            </plugin>
            
            <!-- SpringBoot 打包插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    
    mvn com.alipay.sofa.koupleless:koupleless-base-build-plugin::packageDependency -f pom.xml
    
    mvn spring-boot:run
    
    XML
    <properties>
        <koupleless.runtime.version>0.5.6</koupleless.runtime.version>
        <sofa.ark.version>2.2.14</sofa.ark.version>
    </properties>
    
    <dependencies>
        <!-- Koupleless 模块启动器(provided 作用域) -->
        <dependency>
            <groupId>com.alipay.sofa.koupleless</groupId>
            <artifactId>koupleless-app-starter</artifactId>
            <version>${koupleless.runtime.version}</version>
            <scope>provided</scope>
        </dependency>
        
        <!-- 可选:继承基座依赖 starter -->
        <parent>
            <groupId>com.example</groupId>
            <artifactId>base-application-dependencies-starter</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </parent>
    </dependencies>
    
    XML
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- SOFAArk 打包插件 -->
            <plugin>
                <groupId>com.alipay.sofa</groupId>
                <artifactId>sofa-ark-maven-plugin</artifactId>
                <version>${sofa.ark.version}</version>
                <executions>
                    <execution>
                        <id>default-cli</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <!-- 不生成可执行 jar -->
                    <skipArkExecutable>true</skipArkExecutable>
                    <!-- 输出目录 -->
                    <outputDirectory>./target</outputDirectory>
                    <!-- 模块名称(唯一标识) -->
                    <bizName>module1</bizName>
                    <!-- Web 上下文路径(需要与其他模块不同) -->
                    <webContextPath>/module1</webContextPath>
                    <!-- 开启声明模式 -->
                    <declaredMode>true</declaredMode>
                    <!-- 模块瘦身配置文件 -->
                    <packExcludesConfig>rules.txt</packExcludesConfig>
                </configuration>
            </plugin>
            
            <!-- SpringBoot 打包插件(可选,用于独立部署) -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    
    # 创建目录
    mkdir -p conf/ark
    
    # 下载配置文件
    wget https://github.com/koupleless/samples/raw/master/springboot-samples/slimming/log4j2/biz1/conf/ark/rules.txt -O conf/ark/rules.txt
    
    # 排除的依赖(groupId:artifactId:version)
    excludes=org.springframework.boot:spring-boot-starter-logging,commons-logging:commons-logging
    
    # 排除的 groupId
    excludeGroupIds=org.slf4j
    
    # 排除的 artifactId
    excludeArtifactIds=logback-classic
    
    JAVA
    package com.example.module1.controller;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.context.ApplicationContext;
    
    @RestController
    @RequestMapping("/")
    public class SampleController {
        
        private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
        
        @Autowired
        private ApplicationContext applicationContext;
        
        @GetMapping("/")
        public String hello() {
            String appName = applicationContext.getApplicationName();
            LOGGER.info("{} web test: into sample controller", appName);
            return String.format("hello to %s deploy", appName);
        }
        
        @GetMapping("/health")
        public String health() {
            return "OK";
        }
    }
    
    PROPERTIES
    spring.application.name=module1
    
    XML
    <parent>
        <groupId>com.example</groupId>
        <artifactId>base-application-dependencies-starter</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    
    XML
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <scope>provided</scope>
    </dependency>
    
    XML
    <configuration>
        <excludeGroupIds>org.springframework,org.slf4j</excludeGroupIds>
        <excludeArtifactIds>spring-boot-starter-logging</excludeArtifactIds>
    </configuration>
    
    # 构建模块
    mvn clean package
    
    # 查看构建产物
    ls -lh target/*.jar
    
    # 瘦身前
    -rw-r--r--  1 user  staff   250M  1 15 10:00 module1-1.0.0.jar
    -rw-r--r--  1 user  staff   180M  1 15 10:00 module1-1.0.0-ark-biz.jar
    
    # 瘦身后
    -rw-r--r--  1 user  staff   250M  1 15 10:30 module1-1.0.0.jar
    -rw-r--r--  1 user  staff    15M  1 15 10:30 module1-1.0.0-ark-biz.jar
    
    cd base-application
    mvn spring-boot:run
    
    cd module1
    mvn clean package
    
    # 部署模块
    arkctl deploy target/module1-1.0.0-ark-biz.jar
    
    # 或者使用 curl 命令
    curl -X POST http://localhost:1238/installBiz \
      -H "Content-Type: application/json" \
      -d '{"bizName":"module1","bizVersion":"1.0.0","bizUrl":"file:///path/to/module1-1.0.0-ark-biz.jar"}'
    
    arkctl status
    
    Biz count: 1
    bizName: module1
    bizVersion: 1.0.0
    bizState: ACTIVATED
    mainClass: com.example.module1.Module1Application
    webContextPath: /module1
    
    # 访问模块1的接口
    curl http://localhost:8080/module1/
    # 输出: hello to module1 deploy
    
    curl http://localhost:8080/module1/health
    # 输出: OK
    
    [INFO] Install biz: module1, version: 1.0.0, state: ACTIVATED
    [INFO] Module module1 started successfully
    
    cd module2
    mvn clean package
    arkctl deploy target/module2-1.0.0-ark-biz.jar
    
    # 查看所有模块
    arkctl status
    
    # 访问不同模块
    curl http://localhost:8080/module1/
    curl http://localhost:8080/module2/
    
    # 卸载指定模块
    arkctl uninstall --bizName=module1 --bizVersion=1.0.0
    
    # 或者使用 curl
    curl -X POST http://localhost:1238/uninstallBiz \
      -H "Content-Type: application/json" \
      -d '{"bizName":"module1","bizVersion":"1.0.0"}'
    
    JAVA
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.function.Supplier;
    
    public class StaticFieldMapWrapper<T> {
        private final ConcurrentHashMap<ClassLoader, T> classLoaderTMap = new ConcurrentHashMap<>();
        private Supplier<T> getDefaultMethod;
        
        public StaticFieldMapWrapper(Supplier<T> getDefaultMethod) {
            this.getDefaultMethod = getDefaultMethod;
        }
        
        public T getOrPutDefault() {
            T t = classLoaderTMap.get(Thread.currentThread().getContextClassLoader());
            if (t == null && getDefaultMethod != null) {
                t = getDefaultMethod.get();
                classLoaderTMap.put(Thread.currentThread().getContextClassLoader(), t);
            }
            return t;
        }
    }
    
    JAVA
    private static final Map<Properties, ConfigService> CONFIG_SERVICE_MAP = new ConcurrentHashMap<>();
    
    public static ConfigService getConfigService(Properties properties) {
        return CONFIG_SERVICE_MAP.computeIfAbsent(properties, k -> {
            try {
                return NacosFactory.createConfigService(k);
            } catch (NacosException e) {
                throw new RuntimeException(e);
            }
        });
    }
    
    用户流量 -> Nginx/SLB -> 基座应用(K8s Pod)
                                        |
                                        +-> 模块1
                                        +-> 模块2
                                        +-> 模块3
    
    YAML
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: base-application
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: base-application
      template:
        metadata:
          labels:
            app: base-application
        spec:
          containers:
          - name: base-application
            image: registry.example.com/base-application:1.0.0
            ports:
            - containerPort: 8080
            env:
            - name: MODULE_AUTO_INSTALL
              value: "true"
            - name: MODULE_OSS_ENDPOINT
              value: "oss-cn-region.aliyuncs.com"
            - name: MODULE_OSS_BUCKET
              value: "koupleless-modules"
            livenessProbe:
              httpGet:
                path: /health
                port: 8080
              initialDelaySeconds: 30
              periodSeconds: 10
            readinessProbe:
              httpGet:
                path: /ready
                port: 8080
              initialDelaySeconds: 30
              periodSeconds: 5
            volumeMounts:
            - name: module-storage
              mountPath: /opt/modules
          volumes:
          - name: module-storage
            persistentVolumeClaim:
              claimName: module-pvc
    
    YAML
    # 模块 CI/CD Pipeline
    stages:
      - build
      - test
      - package
      - deploy
    
    build_module:
      stage: build
      script:
        - mvn clean compile
    
    test_module:
      stage: test
      script:
        - mvn test
    
    package_module:
      stage: package
      script:
        - mvn clean package
        - cp target/*.ark-biz.jar module.ark-biz.jar
      artifacts:
        paths:
          - module.ark-biz.jar
    
    deploy_module:
      stage: deploy
      script:
        # 上传模块到 OSS
        - ossutil cp module.ark-biz.jar oss://koupleless-modules/module1/${CI_COMMIT_SHA}.ark-biz.jar
        
        # 通知基座更新模块
        - curl -X POST http://base-service:8080/install \
            -H "Content-Type: application/json" \
            -d '{"module":"module1","version":"'${CI_COMMIT_SHA}'"}'
    
    JAVA
    @Service
    public class ModuleDeploymentService {
        
        @Autowired
        private OssClient ossClient;
        
        @Scheduled(fixedDelay = 5000)
        public void checkModuleUpdates() {
            // 检查 OSS 上的模块版本
            List<String> modules = getModuleList();
            
            for (String module : modules) {
                String latestVersion = getLatestVersion(module);
                String currentVersion = getCurrentVersion(module);
                
                if (!latestVersion.equals(currentVersion)) {
                    // 下载新模块
                    downloadModule(module, latestVersion);
                    
                    // 卸载旧模块
                    uninstallModule(module, currentVersion);
                    
                    // 安装新模块
                    installModule(module, latestVersion);
                }
            }
        }
    }
    
    YAML
    # 配置中心配置
    modules:
      module1:
        version: 1.0.0
        enabled: true
        healthCheck: /health
      module2:
        version: 2.0.0
        enabled: true
        healthCheck: /health
    
    JAVA
    @NacosConfigListener(dataId = "modules.json")
    public void onModuleConfigChange(String config) {
        ModulesConfig modulesConfig = JSON.parseObject(config, ModulesConfig.class);
        
        // 对比配置差异
        List<ModuleChange> changes = compareModuleConfig(modulesConfig);
        
        // 应用变更
        for (ModuleChange change : changes) {
            switch (change.getType()) {
                case INSTALL:
                    installModule(change.getModule());
                    break;
                case UNINSTALL:
                    uninstallModule(change.getModule());
                    break;
                case UPDATE:
                    updateModule(change.getModule());
                    break;
            }
        }
    }
    
    XML
    <!-- 基座 pom.xml -->
    <profiles>
      <profile>
        <id>static-pack</id>
        <dependencies>
          <!-- 把模块 ark-biz 当 jar 依赖进来 -->
          <dependency>
            <groupId>com.example</groupId>
            <artifactId>module1</artifactId>
            <version>1.0.0</version>
            <classifier>ark-biz</classifier>
            <type>jar</type>
          </dependency>
          <dependency>
            <groupId>com.example</groupId>
            <artifactId>module2</artifactId>
            <version>1.0.0</version>
            <classifier>ark-biz</classifier>
            <type>jar</type>
          </dependency>
        </dependencies>
      </profile>
    </profiles>
    
    JAVA
    @SpringBootApplication
    @StaticBiz   // 关键
    public class BaseApplication {
        public static void main(String[] args) {
            SpringApplication.run(BaseApplication.class, args);
        }
    }
    
    mvn clean package -Pstatic-pack
    
    XML
    <!-- 模块 pom.xml -->
    <dependency>
        <groupId>com.alipay.sofa</groupId>
        <artifactId>web-ark-plugin</artifactId>
        <classifier>multi-web</classifier>   <!-- 关键 classifier -->
        <scope>provided</scope>
    </dependency>
    
    YAML
    # module1/application.yml
    server:
      port: 8081          # 模块独占
      servlet:
        context-path: /   # 可以为 /
    spring:
      application:
        name: module1
    
    XML
    <plugin>
      <groupId>com.alipay.sofa</groupId>
      <artifactId>sofa-ark-maven-plugin</artifactId>
      <configuration>
        <bizName>module1</bizName>
        <webContextPath>/</webContextPath>
        <!-- 开启多 web 模式 -->
        <webMultiFlag>true</webMultiFlag>
      </configuration>
    </plugin>