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

510 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`:
```xml
<!-- 原始 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 配置模型
```xml
<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:
```xml
<!-- 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 可达性分析(核心算法)
```java
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 模块
```xml
<!-- 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 模块
```xml
<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/通用混淆 |