共享代码不是共享风险——公共库解耦的三种进化路径

背景与痛点

在一家互联网公司中,最初团队为了代码复用,将所有业务通用逻辑打包成一个共享 JAR(公共库),并被多个业务线 A、B、C 引用。

随着业务的发展,不同线上的需求出现差异:A 线需要的特性被硬塞进公共库,同样的改动也带给了 B、C。虽然 A 线的 RD 修改后,A 的 QA 进行了验证并上线,但 QA 只关注 A 线场景,无视对 B、C 的影响,最终导致"一个业务上线,多个业务宕机"的连锁故障。

核心问题是:公共库承载了过多个性化逻辑,反而毁掉了最初的通用性,造成多业务深度耦合,测试覆盖不完整,风险高且事故频发。


现代解耦思路

总体目标:将稳定、核心 的共性逻辑与易变、个性逻辑彻底分离,做到低耦合、高内聚,安全可控。下面引入三种更现代化且可视化的方案。

方案一:模块化单体(Modular Monolith)

  • 思路:在同一个仓库与部署单元内,通过分域(Domain)与模块(Module)划分------模块内代码可自由改动,模块间仅通过明确接口交互。

  • 实现方式

    1. 基于 领域驱动设计 (DDD),为 A、B、C 各自建立 Domain Module
    2. 公共基础设施(工具、公共 DTO、通用工具类)放在 Core Module
    3. Release 时,通过构建工具(Maven/Gradle)按模块聚合产出单体应用。
bash 复制代码
modular-monolith/
├── pom.xml                          ← 父工程(Aggregator POM)
├── core/                            ← Core Module(公共协议、DTO、工具)
│   └── pom.xml
│   └── src/main/java/...
│       └── com/example/core/
│           ├── dto/                 ← 通用 DTO
│           ├── util/                ← 通用工具类
│           └── config/              ← 公共配置(如 Swagger、拦截器等)
├── module-a/                        ← A-Module(领域 A 逻辑)
│   └── pom.xml
│   └── src/main/java/...
│       └── com/example/a/
│           ├── api/                 ← A 模块对外接口(Service 接口)
│           ├── application/         ← 用例层(Use Case)
│           ├── domain/              ← 领域层(Entity / VO / Repository 接口)
│           └── infrastructure/      ← 基础设施层(Repository 实现、FeignClient 等)
├── module-b/  ...                      ← B-Module(同理)
└── module-c/  ...                      ← C-Module(同理)
%% 模块化单体示意 flowchart LR subgraph 单体应用 Core[Core Module 工具公共协议] A[A-Module 业务A逻辑] B[B-Module 业务B逻辑] C[C-Module 业务C逻辑] end Core --> A Core --> B Core --> C A --> DB1((DB-A)) B --> DB2((DB-B)) C --> DB3((DB-C)) style Core fill:#f0f8ff,stroke:#0066cc,stroke-width:2px style A fill:#e6ffe6,stroke:#009933 style B fill:#fff5e6,stroke:#ff9933 style C fill:#ffe6f2,stroke:#cc0066
  • 优点:部署统一、维护简单;模块边界清晰;易于在单体基础上渐进拆分。
  • 适用场景:团队规模中等,对服务化拆分成本较高时。常用作从公共库走向微服务的过渡。

方案二:插件化架构(Plug-in Architecture)

  • 思路 :核心框架提供可扩展的 插件加载机制,每个业务个性化逻辑通过插件动态加载,无需改动核心框架。

  • 实现方式

    1. 定义插件接口(Plugin API),约定初始化、执行、销毁等生命周期方法。
    2. 各业务将定制逻辑封装为独立插件包,按需部署到插件目录。
    3. 启动时,核心框架扫描并加载所有插件,实现运行时隔离。
%% 插件化架构示意 flowchart TD subgraph 核心框架 Core[Core Service] Loader(Plugin Loader) end subgraph 插件目录 P1[Plugin A] P2[Plugin B] P3[Plugin C] end Core --> Loader Loader --> P1 & P2 & P3 Core --> DB((数据库)) style Core fill:#e8f4ff,stroke:#2a6fdb style P1 fill:#d9f7d9,stroke:#33aa33 style P2 fill:#fff0d9,stroke:#ff9e33 style P3 fill:#f9d9ff,stroke:#cc33aa
  • 优点:核心库稳定不变,业务个性可独立迭代;插件隔离减少互相影响。
  • 适用场景:需要支持多租户、可插拔特性,或者框架级项目(如支付、消息中间件)。
  • 下面给出一个基于 Maven 的 插件化架构(Plug‑in Architecture)示例项目,展示如何在核心框架中定义插件接口并动态加载各业务插件。
bash 复制代码
plugin-architecture/
├── pom.xml                            ← 父工程(Aggregator POM)
├── core/                              ← Core 模块(包含 Plugin API 与加载器)
│   └── pom.xml
│   └── src/main/java/...
│       └── com/example/core/
│           ├── plugin/               ← 插件接口定义
│           ├── loader/               ← 插件加载器实现
│           └── app/                  ← 核心应用入口
└── plugins/                           ← 插件项目(可独立部署、打包为 JAR)
    ├── plugin-a/
    │   └── pom.xml
    │   └── src/main/java/...
    │       └── com/example/plugin/a/
    │           └── PluginAImpl.java
    ├── plugin-b/...                     ← Plugin B 同理
    └── plugin-c/...                     ← Plugin C 同理

1. 父工程 pom.xml

xml 复制代码
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>plugin-architecture</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <modules>
    <module>core</module>
    <module>plugins/plugin-a</module>
    <module>plugins/plugin-b</module>
    <module>plugins/plugin-c</module>
  </modules>

  <properties>
    <java.version>11</java.version>
  </properties>
</project>

2. Core 模块(core/pom.xml

xml 复制代码
<project ...>
  <parent>
    <groupId>com.example</groupId>
    <artifactId>plugin-architecture</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>
  <artifactId>core</artifactId>
  <packaging>jar</packaging>

  <dependencies>
    <!-- 用于扫描插件目录的 Apache Commons IO -->
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.11.0</version>
    </dependency>
  </dependencies>
</project>
2.1 插件接口定义(Plugin API
java 复制代码
// src/main/java/com/example/core/plugin/Plugin.java
package com.example.core.plugin;

/**
 * 插件基础接口,定义生命周期方法
 */
public interface Plugin {
    /**
     * 插件初始化(例如读取配置)
     */
    void init();

    /**
     * 执行业务逻辑,返回执行结果
     */
    String execute(String input);

    /**
     * 插件卸载/销毁
     */
    void destroy();
}
2.2 插件加载器(PluginLoader
java 复制代码
// src/main/java/com/example/core/loader/PluginLoader.java
package com.example.core.loader;

import com.example.core.plugin.Plugin;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;

/**
 * 简单的插件加载器:扫描指定目录下所有 JAR 并实例化 Plugin
 */
public class PluginLoader {
    private final File pluginsDir;

    public PluginLoader(String pluginsDirPath) {
        this.pluginsDir = new File(pluginsDirPath);
    }

    public void loadAndRunAll() throws Exception {
        if (!pluginsDir.exists() || !pluginsDir.isDirectory()) {
            throw new IllegalArgumentException("插件目录不存在: " + pluginsDir.getAbsolutePath());
        }

        // 扫描所有 .jar 文件
        Collection<File> jars = FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false);
        for (File jar : jars) {
            // 每个 JAR 用单独的 ClassLoader 加载
            URL jarUrl = jar.toURI().toURL();
            try (URLClassLoader cl = new URLClassLoader(new URL[]{jarUrl}, this.getClass().getClassLoader())) {
                // 约定:插件 JAR 内含 resources/plugin.properties,声明插件主类
                String propsPath = "plugin.properties";
                URL propUrl = cl.findResource(propsPath);
                if (propUrl == null) continue;

                java.util.Properties props = new java.util.Properties();
                props.load(propUrl.openStream());
                String pluginClass = props.getProperty("plugin.class");
                Class<?> cls = cl.loadClass(pluginClass);
                Plugin plugin = (Plugin) cls.getDeclaredConstructor().newInstance();

                plugin.init();
                String result = plugin.execute("TestInput");
                System.out.println("[" + jar.getName() + "] execute result: " + result);
                plugin.destroy();
            }
        }
    }
}
2.3 核心应用入口
java 复制代码
// src/main/java/com/example/core/app/CoreApp.java
package com.example.core.app;

import com.example.core.loader.PluginLoader;

public class CoreApp {
    public static void main(String[] args) throws Exception {
        String pluginsDir = args.length > 0 ? args[0] : "./plugins";
        System.out.println("Loading plugins from: " + pluginsDir);
        new PluginLoader(pluginsDir).loadAndRunAll();
    }
}

3. 插件示例:Plugin A(plugins/plugin-a/pom.xml

xml 复制代码
<project ...>
  <parent>
    <groupId>com.example</groupId>
    <artifactId>plugin-architecture</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>
  <artifactId>plugin-a</artifactId>
  <packaging>jar</packaging>

  <dependencies>
    <!-- 依赖 Plugin 接口 -->
    <dependency>
      <groupId>com.example</groupId>
      <artifactId>core</artifactId>
      <version>${project.version}</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
</project>
3.1 插件实现
java 复制代码
// src/main/java/com/example/plugin/a/PluginAImpl.java
package com.example.plugin.a;

import com.example.core.plugin.Plugin;

public class PluginAImpl implements Plugin {
    @Override
    public void init() {
        System.out.println("Plugin A 初始化");
    }

    @Override
    public String execute(String input) {
        return "Plugin A processed: " + input.toUpperCase();
    }

    @Override
    public void destroy() {
        System.out.println("Plugin A 销毁");
    }
}
3.2 插件描述文件

src/main/resources/plugin.properties 中添加:

properties 复制代码
# 声明此插件的主类
plugin.class=com.example.plugin.a.PluginAImpl

4. Plugin B / Plugin C

B、C 插件项目结构、plugin.properties、实现类都与 A 类似,只需改包名和业务逻辑。


5. 构建与运行

  1. 构建所有模块

    bash 复制代码
    mvn clean package
    • 会在 core/target 生成核心 JAR
    • 在各 plugins/plugin-*/target 生成各自的插件 JAR
  2. 准备插件目录

    bash 复制代码
    mkdir app-plugins
    cp plugins/plugin-a/target/plugin-a-1.0.0-SNAPSHOT.jar app-plugins/
    cp plugins/plugin-b/target/plugin-b-1.0.0-SNAPSHOT.jar app-plugins/
  3. 启动核心应用

    bash 复制代码
    java -jar core/target/core-1.0.0-SNAPSHOT.jar ./app-plugins

    控制台会依次输出:

    bash 复制代码
    Loading plugins from: ./app-plugins
    Plugin A 初始化
    [plugin-a-1.0.0-SNAPSHOT.jar] execute result: Plugin A processed: TestInput
    Plugin A 销毁
    Plugin B 初始化
    ...

这样就实现了一个 插件化架构

  • 核心框架(core)只负责定义标准 API 和加载机制,
  • 具体业务以插件 JAR 的形式部署到指定目录,
  • 启动时动态扫描、隔离加载并执行,互不干扰。

方案三:微服务化(Serviceization)

  • 思路:对于真正共性的、可复用的业务能力,下沉为独立微服务,暴露契约清晰的 HTTP/RPC 接口。

  • 实现方式

    1. 将公共逻辑部署为 biz-service。接口需遵守 兼容性策略(向后兼容、接口版本管理)。
    2. 使用 API Gateway 承载路由、限流、鉴权等横切关注点。
    3. 各业务侧通过 SDK 或 API 调用方式集成。
%% 微服务化示意 flowchart LR subgraph 业务侧 A[Service A] B[Service B] C[Service C] end D[API Gateway] E[biz-service] subgraph 数据存储 DB[(数据库)] Cache((缓存)) end A & B & C --> D D --> E E --> DB & Cache style A fill:#e6ffe6,stroke:#009933 style B fill:#fff5e6,stroke:#ff9933 style C fill:#ffe6f2,stroke:#cc0066 style D fill:#f0f0f0,stroke:#666666,stroke-dasharray:4 2 style E fill:#eef7ff,stroke:#2a6fdb
  • 优点:真正解耦;可独立伸缩、独立发布;不同团队可并行开发;框架升级不影响业务侧。
  • 适用场景:团队成熟、服务调用频繁、对稳定性、扩展性要求高的项目。

总结:分离关注点

  • 垂直拆分(模块化/插件化) :将业务个性化逻辑从公共库剥离,放到模块或插件中,核心只保留稳定基础。
  • 水平拆分(微服务化) :真正的共性能力下沉为服务,以接口契约维系耦合。
  • 物理隔离(过渡手段) :在拆分初期,模块化单体或插件化能快速止血,待成熟后再逐步演进。
相关推荐
Hellyc23 分钟前
基于模板设计模式开发优惠券推送功能以及对过期优惠卷进行定时清理
java·数据库·设计模式·rocketmq
lifallen26 分钟前
Paimon LSM Tree Compaction 策略
java·大数据·数据结构·数据库·算法·lsm-tree
hdsoft_huge1 小时前
SpringBoot 与 JPA 整合全解析:架构优势、应用场景、集成指南与最佳实践
java·spring boot·架构
Livingbody1 小时前
基于【ERNIE-4.5-VL-28B-A3B】模型的图片内容分析系统
后端
百锦再1 小时前
详细解析 .NET 依赖注入的三种生命周期模式
java·开发语言·.net·di·注入·模式·依赖
程序员的世界你不懂2 小时前
基于Java+Maven+Testng+Selenium+Log4j+Allure+Jenkins搭建一个WebUI自动化框架(2)对框架加入业务逻辑层
java·selenium·maven
你的人类朋友3 小时前
🍃Kubernetes(k8s)核心概念一览
前端·后端·自动化运维
有没有没有重复的名字3 小时前
线程安全的单例模式与读者写者问题
java·开发语言·单例模式
追逐时光者4 小时前
面试第一步,先准备一份简洁、优雅的简历模板!
后端·面试
DoraBigHead4 小时前
你写前端按钮,他们扛服务器压力:搞懂后端那些“黑话”!
前端·javascript·架构