Files
workutils/docs/api-trim-maven-plugin-design.md
T
2026-06-25 19:09:19 +08:00

19 KiB
Raw Blame History

api-trim-maven-plugin 架构设计

1. 问题定义

API 模块旨在为网关桥应用提供 RPC 接口调用能力,理想的 JAR 依赖范围应限于:

  • RPC 接口定义
  • 入参/出参 DTO 模型
  • 序列化注解与类型

实际产生的冗余:

冗余来源 示例
实现类 *ServiceImpl.java、*ManagerImpl.java
内部工具体系 *Utils.java、*Helper.java、*Converter.java
Spring 配置/自动装配 *Config.java、*AutoConfiguration.java
数据层依赖 MyBatis Mapper、JDBC Driver、DataSource
传递依赖 由实现类引入的 spring-boot-starter-*、数据库连接池、缓存等

2. 整体架构

┌────────────────────────────────────────────────────────────┐
│                    api-trim-maven-plugin                    │
├────────────────────────────────────────────────────────────┤
│  Goal: api-trim:trim    (默认绑定 package 阶段)           │
│         api-trim:analyze(仅分析,不修改产物)              │
├────────────────────────────────────────────────────────────┤
│                      ┌──────────────┐                      │
│  ① analyze          │  依赖分析器   │  ASM 字节码扫描       │
│                     │  Analyzer     │  Maven 依赖树解析      │
│                      └──────┬───────┘                      │
│                             │                               │
│                             ▼                               │
│                      ┌──────────────┐                      │
│  ② trim              │  精简执行器   │  类/资源白名单策略    │
│                     │  Trimmer      │  依赖重写 或 shade     │
│                      └──────┬───────┘                      │
│                             │                               │
│                             ▼                               │
│                      ┌──────────────┐                      │
│  ③ verify            │  安全校验器   │  接口契约 diff        │
│                     │  Verifier     │  字节码引用完整性      │
│                      └──────────────┘                      │
└────────────────────────────────────────────────────────────┘

3. 核心组件详细设计

3.1 依赖分析器(Analyzer)

分析器负责区分「RPC 必需的 API 表面」与「冗余实现及传递依赖」。

3.1.1 三阶段分析

阶段1:入口类识别
  ├─ 扫描目标模块 compiled classes
  ├─ 按注解识别 RPC 服务接口
  │   ├─ @DubboService / @Service(interfaceClass=...)
  │   ├─ @FeignClient
  │   ├─ @RestController / @RequestMapping(若 API 模块含 HTTP 接口)
  │   ├─ gRPC Service 基类子类
  │   └─ 用户通过 <apiInterfaces> 显式声明
  └─ 收集所有入口接口 → className[]

阶段2:可达性分析(类级别,ASM)
  ├─ 以入口接口为根,BFS 遍历引用图
  │   ├─ 接口继承链(super interfaces)
  │   ├─ 方法签名中的类型(参数类型 + 返回类型 + 异常类型)
  │   │   └─ 递归解析泛型参数
  │   ├─ 注解类型引用(类注解 + 方法注解 + 字段注解)
  │   │   └─ 保留序列化相关注解:Jackson、Protobuf、Hessian、Fastjson 等
  │   └─ 父类中的字段类型
  └─ 输出:必需类集合 Set<ClassName>

阶段3:依赖坐标分析(Maven,可选)
  ├─ 解析 dependency:tree
  ├─ 对每个传递依赖:
  │   ├─ 若所含类均在「必需类集合」外 → 标记为可移除
  │   └─ 若所含类部分必需 → 保留(后续通过类级过滤处理)
  └─ 输出:可移除的 Maven 坐标列表

3.1.2 引用图遍历规则

BFS 起点:所有入口接口
遍历策略:
┌─ 遇到类/接口 → 加入必需集 → 展开
│   ├─ 父类 / 父接口 → 加入队列
│   ├─ 泛型参数中引用的类 → 加入队列
│   ├─ 方法参数/返回类型 → 加入队列
│   ├─ 注解中引用的类 → 加入队列
│   └─ 字段类型 → 加入队列(仅对 DTO 类)
│
├─ 遇到标准库类(java.* / javax.*) → 终止(不展开)
├─ 遇到第三方库类 → 加入必需集但标记为 external(不影响 JAR 精简,仅用于依赖分析)
└─ 遇到非目标模块类 → 记录为「外部依赖」,用于验证阶段

3.1.3 注解保留策略

强制保留的注解分类:
├─ 序列化注解
│   ├─ Jackson: @JsonProperty, @JsonIgnore, @JsonFormat, @JsonTypeInfo...
│   ├─ Protobuf: @Protobuf, @ProtoField...
│   ├─ Hessian: java.io.Serializable(接口继承)
│   └─ Fastjson: @JSONField
│
├─ 校验注解(RPC 调用方可能使用)
│   └─ JSR-303: @NotNull, @Valid, @Size, @Pattern...
│
├─ API 文档注解
│   └─ Swagger: @ApiModel, @ApiModelProperty(可选,根据配置)
│
└─ 业务注解(用户自定义)
    └─ 通过 <retainedAnnotations> 配置

3.2 精简执行器(Trimmer)

3.2.1 策略对比与选择

策略 方案 优点 缺点
A: 类级过滤 重新打包 JAR,仅保留分析器输出的必需类 极致精简,JAR 最小 需要处理 MANIFEST、资源文件
B: POM 重写 修改依赖声明,exclude 非必需传递依赖 Maven 原生支持 粒度粗,一个 JAR 内可能只有几个类需要
C: Shade + 过滤 maven-shade-plugin + filters + minimizeJar 成熟稳定 配置复杂,对 RPC 语义不感知

推荐:策略 A(类级过滤),原因:

  • RPC API 模块依赖中,一个三方 JAR 可能包含 200 个类但仅 3 个 DTO 需要——B 粒度太粗
  • 策略 C 的 minimizeJar 对反射/序列化场景不够友好,可能有遗漏
  • 策略 A 基于可达性分析,天然感知 RPC 语义

3.2.2 类级过滤执行流程

输入:原始 JAR ← maven-jar-plugin 产物
     必需类集合 Set<ClassName> ← Analyzer
       <includes> 追加类 ← 用户配置
       <excludes> 排除类 ← 用户配置

流程:
1. 创建临时目录 temp/
2. 遍历原始 JAR 中所有条目:
   ├─ *.class → 判断类全限定名是否在「必需集合 + includes」
   │   ├─ YES → 解压到 temp/
   │   └─ NO  → 跳过
   │
   ├─ META-INF/MANIFEST.MF → 保留
   ├─ META-INF/spring.factories → 跳过(不含自动装配)
   ├─ *.xml / *.properties(Spring 配置)→ 跳过
   ├─ *.proto / *.avsc → 按需保留(若入口是 gRPC/Protobuf)
   └─ 其他资源 → 跳过

3. 重新打包 temp/ → ${project.build.finalName}-api.jar
4. 可选:若启用 deps 重写,重写 POM 中依赖 scope

3.2.3 依赖 scope 重写

对于传递给下游的 POM,将非必需依赖标记为 provided:

<!-- 原始 POM -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

<!-- 精简后 POM -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <scope>provided</scope>  <!-- 调用方自行提供 -->
    <optional>true</optional>
</dependency>

3.2.4 配置模型

<plugin>
    <groupId>com.example</groupId>
    <artifactId>api-trim-maven-plugin</artifactId>
    <version>1.0.0</version>
    <configuration>

        <!-- ===== 入口指定 ===== -->
        <!-- 自动扫描 RPC 注解类 + 手动指定 -->
        <apiInterfaces>
            <apiInterface>com.example.api.UserService</apiInterface>
            <apiInterface>com.example.api.OrderService</apiInterface>
        </apiInterfaces>

        <!-- RPC 框架标识,影响注解扫描策略 -->
        <rpcFramework>DUBBO</rpcFramework>  <!-- DUBBO | FEIGN | GRPC | SPRING_CLOUD | GENERIC -->

        <!-- ===== 类过滤规则 ===== -->
        <retainedAnnotations>
            <annotation>com.example.MyBusinessAnnotation</annotation>
        </retainedAnnotations>

        <retainedPackages>
            <package>com.example.common.dto.**</package>
            <package>com.example.common.enums.**</package>
        </retainedPackages>

        <excludedPackages>
            <package>com.example.**.impl.**</package>
            <package>com.example.**.config.**</package>
        </excludedPackages>

        <!-- ===== 依赖处理 ===== -->
        <dependencyStrategy>REWRITE_SCOPE</dependencyStrategy>
        <!-- PROVIDED_ONLY: 标记为 provided
             OPTIONAL_ONLY: 标记为 optional
             REMOVE: 从 POM 中完全移除 -->

        <excludedDependencies>  <!-- 指定 POM 中排除的传递依赖 -->
            <dependency>mysql:mysql-connector-java</dependency>
            <dependency>org.mybatis:*</dependency>
        </excludedDependencies>

        <!-- ===== 安全 ===== -->
        <failOnBrokenReference>true</failOnBrokenReference>
        <outputReport>true</outputReport>
    </configuration>
</plugin>

3.3 Maven 生命周期集成

3.3.1 Goal 绑定

Goal: trim(默认)
├─ 绑定阶段: package
├─ 执行顺序: compile → jar → trim → [install/deploy]
├─ requiresDependencyResolution: compile+runtime
└─ 输出产物: ${project.build.finalName}-api.jar(替换默认 JAR)

Goal: analyze(诊断用)
├─ 绑定阶段: verify(或手动执行 api-trim:analyze)
├─ 输出: target/api-trim/report.txt
└─ 可选: target/api-trim/dependency-graph.dot

3.3.2 执行时序

mvn clean package

phase: compile     → javac 编译
phase: process-classes
phase: prepare-package
                      ┌─────────────────┐
phase: package ──────▶│ api-trim:trim    │
                      │                 │
                      │ 1. 解析入口接口   │
                      │ 2. 字节码可达性   │
                      │ 3. 构建白名单     │
                      │ 4. JAR 重打包     │
                      │ 5. POM rewrite   │
                      │ 6. 安全校验       │
                      └────────┬────────┘
                               │
phase: verify   ←──────────────┘ (report)
phase: install  → 安装精简后的 JAR 到本地仓库
phase: deploy   → 发布精简后的 JAR 到远程仓库

3.3.3 多模块项目集成

parent
├── api-module        ← 在此模块配置 api-trim:trim
│   └── pom.xml      ← <plugin>api-trim-maven-plugin</plugin>
├── service-module    ← 实现模块,正常打包
├── gateway-bridge    ← 网关桥,依赖 api-module(会拿到精简版 JAR)
└── pom.xml

API 模块只需在 pom.xml 中声明该插件即可,其下游模块(gateway-bridge)通过普通 Maven 依赖机制拿到精简后的 JAR:

<!-- gateway-bridge/pom.xml -->
<dependency>
    <groupId>com.example</groupId>
    <artifactId>api-module</artifactId>
    <version>${project.version}</version>
</dependency>
<!-- 无需任何额外配置,直接引用即可 -->

3.4 安全保障与验证机制

3.4.1 编译时校验

校验项1:字节码引用完整性
├─ 读取精简后的 classes 集合
├─ 检查每个类的方法描述符中的所有类型引用
├─ 若引用到未包含在 JAR 中的非标准库类 → ERROR
└─ 区分:缺少的类在调用方 classpath 中 → WARN(需用户确认)

校验项2:序列化完整性
├─ 对每个 DTO 类:
│   ├─ 遍历所有字段 getter/setter
│   ├─ 检查序列化注解是否被意外移除
│   └─ 检查枚举值是否完整
└─ 生成 SerializationTest.java 模板

3.4.2 运行时验证(可选,需用户配置测试类)

验证方式:API 接口契约对比
├─ 原始 JAR  → 反射扫描所有 RPC 接口的方法签名 → Set<MethodSignature>
├─ 精简 JAR → 同上
├─ 对比两者:
│   ├─ 方法数量一致
│   ├─ 方法签名完全匹配(名称 + 参数 + 返回类型 + 异常)
│   └─ 泛型签名一致
└─ 不一致 → BUILD FAILURE

验证方式:序列化往返测试(自动生成测试代码)
├─ 为每个 DTO 生成:
│   public void test_${ClassName}_serialization() {
│       ${ClassName} obj = new ${ClassName}();
│       // ... populate random data
│       byte[] data = serialize(obj);
│       ${ClassName} result = deserialize(data);
│       assertThat(result).isEqualTo(obj);
│   }
└─ 建议用户将此测试加入 CI 流程

3.4.3 报告输出

target/api-trim/
├── report.txt             # 精简摘要
│   ├── 原始 JAR 大小: 45.2 MB
│   ├── 精简后 JAR 大小: 3.8 MB (↓ 91.6%)
│   ├── 原始 class 数量: 8,342
│   ├── 保留 class 数量: 312
│   ├── 移除的传递依赖: 23 个
│   └── 耗时: 1.2s
│
├── retained-classes.txt   # 保留的类清单
├── removed-classes.txt    # 移除的类清单
└── dependency-graph.dot   # 依赖图(Graphviz)

4. 关键实现细节

4.1 可达性分析(核心算法)

public class ReachabilityAnalyzer {
    private final Set<String> retained = new HashSet<>();
    private final Queue<String> queue = new LinkedList<>();

    public Set<String> analyze(Set<String> entryPoints, Path classesDir) {
        // Step 1: 注册入口
        queue.addAll(entryPoints);
        retained.addAll(entryPoints);

        // Step 2: BFS
        while (!queue.isEmpty()) {
            String className = queue.poll();
            byte[] bytecode = readClass(classesDir, className);
            if (bytecode == null) continue; // 外部类,跳过

            // 解析类引用
            for (String ref : parseReferences(bytecode)) {
                if (isStandardLibrary(ref)) continue;
                if (retained.add(ref)) {
                    queue.add(ref); // 新发现,入队
                }
            }
        }
        return retained;
    }

    private List<String> parseReferences(byte[] bytecode) {
        // 使用 ASM ClassReader
        // 提取:super class, interfaces, field types,
        //       method param/return types, annotation types,
        //       generic signatures

        // CRITICAL: 递归解析泛型签名中的类型
        // 如 List<Map<String, OrderDetailDTO>> → OrderDetailDTO 必须保留
        List<String> refs = new ArrayList<>();
        // ... ASM visitor logic
        return refs;
    }
}

4.2 ASM Visitor 设计

ApiSurfaceClassVisitor extends ClassVisitor
├─ visit(version, access, name, signature, superName, interfaces)
│   └─ 记录 superName + interfaces + signature(泛型)
├─ visitAnnotation(desc, visible)
│   └─ 记录注解类型(即使不在保留白名单,也记录引用)
├─ visitField(access, name, desc, signature, value)
│   └─ 记录: desc 中类型 + signature 中泛型 + 字段注解类型
│   └─ 注意: desc="Lcom/example/UserDTO;" → 提取 UserDTO
├─ visitMethod(access, name, desc, signature, exceptions)
│   └─ 记录: 参数类型 + 返回类型 + 异常类型 + signature
│   └─ desc="(ILcom/example/ReqDTO;)Lcom/example/RespDTO;"
│        → 提取 ReqDTO, RespDTO
└─ visitEnd()

4.3 类名规范化

字节码内部名 → 全限定类名转换:
  "com/example/UserService"              → "com.example.UserService"
  "[Lcom/example/DTO;"                   → "com.example.DTO[]"(保留数组标记)
  "Lcom/example/Inner$Nested;"           → "com.example.Inner$Nested"
  
跳过规则:
  java/*  javax/*  sun/*  jdk/*          → skip(JDK 标准库)
  [ZBSCIJFD                               → skip(8大基本类型 + void)

5. 典型使用场景

场景1:Dubbo API 模块

<!-- API 模块 POM -->
<plugin>
    <groupId>com.example</groupId>
    <artifactId>api-trim-maven-plugin</artifactId>
    <configuration>
        <rpcFramework>DUBBO</rpcFramework>
        <!-- 自动扫描 @Service(interfaceClass=...) 找到所有 RPC 接口 -->
    </configuration>
</plugin>

效果:

  • 移除所有 *ServiceImpl、*ManagerImpl 实现类
  • 移除 MyBatis、Spring Boot AutoConfigure、JDBC Driver
  • 保留 RPC 接口、DTO、枚举、自定义异常
  • JAR 从 45MB → ~3MB

场景2:Feign API 模块

<plugin>
    <groupId>com.example</groupId>
    <artifactId>api-trim-maven-plugin</artifactId>
    <configuration>
        <rpcFramework>FEIGN</rpcFramework>
        <!-- 自动扫描 @FeignClient 注解 -->
    </configuration>
</plugin>

6. 优先级与技术可行性

模块 可行性 复杂度 备注
入口扫描(注解识别) ✅ 成熟 低 ASM 读注解
类级可达性分析 ✅ 成熟 中 BFS + ASM,已有类似工具(如 ProGuard)
JAR 重打包 ✅ 成熟 低 Java NIO + JarOutputStream
POM 依赖重写 ✅ 成熟 低 maven-model 库直接修改
字节码引用完整性校验 ✅ 可行 中 ASM 二次扫描精简后 JAR
序列化验证 ✅ 可行 中 需框架特定处理(Jackson/Protobuf/Hessian)
泛型签名解析 ⚠️ 有坑 中-高 signature 字段可能包含复杂泛型,需递归解析

7. 与其他方案对比

方案 JAR 大小 配置复杂度 RPC 安全性 适用场景
api-trim-plugin ★★★★★ (3MB) 中 高(ASM 可达性确保) RPC API 模块专用
maven-shade minimizeJar ★★★★ (5MB) 高 中(反射不感知) 通用 JAR 精简
maven-dependency exclude ★★ (20MB+) 低 高 粗粒度排除
手工拆分模块 ★★★★ 很高 高 架构重构
ProGuard/R8 ★★★★★ 高 中(可能破坏序列化) Android/通用混淆