# 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 基类子类 │ └─ 用户通过 显式声明 └─ 收集所有入口接口 → className[] 阶段2:可达性分析(类级别,ASM) ├─ 以入口接口为根,BFS 遍历引用图 │ ├─ 接口继承链(super interfaces) │ ├─ 方法签名中的类型(参数类型 + 返回类型 + 异常类型) │ │ └─ 递归解析泛型参数 │ ├─ 注解类型引用(类注解 + 方法注解 + 字段注解) │ │ └─ 保留序列化相关注解:Jackson、Protobuf、Hessian、Fastjson 等 │ └─ 父类中的字段类型 └─ 输出:必需类集合 Set 阶段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(可选,根据配置) │ └─ 业务注解(用户自定义) └─ 通过 配置 ``` --- ### 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 ← Analyzer 追加类 ← 用户配置 排除类 ← 用户配置 流程: 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`: ```xml org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter provided true ``` #### 3.2.4 配置模型 ```xml com.example api-trim-maven-plugin 1.0.0 com.example.api.UserService com.example.api.OrderService DUBBO com.example.MyBusinessAnnotation com.example.common.dto.** com.example.common.enums.** com.example.**.impl.** com.example.**.config.** REWRITE_SCOPE mysql:mysql-connector-java org.mybatis:* true true ``` --- ### 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 ← api-trim-maven-plugin ├── service-module ← 实现模块,正常打包 ├── gateway-bridge ← 网关桥,依赖 api-module(会拿到精简版 JAR) └── pom.xml ``` API 模块只需在 `pom.xml` 中声明该插件即可,其下游模块(gateway-bridge)通过普通 Maven 依赖机制拿到精简后的 JAR: ```xml com.example api-module ${project.version} ``` --- ### 3.4 安全保障与验证机制 #### 3.4.1 编译时校验 ``` 校验项1:字节码引用完整性 ├─ 读取精简后的 classes 集合 ├─ 检查每个类的方法描述符中的所有类型引用 ├─ 若引用到未包含在 JAR 中的非标准库类 → ERROR └─ 区分:缺少的类在调用方 classpath 中 → WARN(需用户确认) 校验项2:序列化完整性 ├─ 对每个 DTO 类: │ ├─ 遍历所有字段 getter/setter │ ├─ 检查序列化注解是否被意外移除 │ └─ 检查枚举值是否完整 └─ 生成 SerializationTest.java 模板 ``` #### 3.4.2 运行时验证(可选,需用户配置测试类) ``` 验证方式:API 接口契约对比 ├─ 原始 JAR → 反射扫描所有 RPC 接口的方法签名 → Set ├─ 精简 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 可达性分析(核心算法) ```java public class ReachabilityAnalyzer { private final Set retained = new HashSet<>(); private final Queue queue = new LinkedList<>(); public Set analyze(Set 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 parseReferences(byte[] bytecode) { // 使用 ASM ClassReader // 提取:super class, interfaces, field types, // method param/return types, annotation types, // generic signatures // CRITICAL: 递归解析泛型签名中的类型 // 如 List> → OrderDetailDTO 必须保留 List 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 模块 ```xml com.example api-trim-maven-plugin DUBBO ``` 效果: - 移除所有 `*ServiceImpl`、`*ManagerImpl` 实现类 - 移除 MyBatis、Spring Boot AutoConfigure、JDBC Driver - 保留 RPC 接口、DTO、枚举、自定义异常 - JAR 从 45MB → ~3MB ### 场景2:Feign API 模块 ```xml com.example api-trim-maven-plugin FEIGN ``` --- ## 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/通用混淆 |