提交UI自动化插件

This commit is contained in:
张家训
2026-07-03 16:50:26 +08:00
parent bd605c47c7
commit 3883323f93
6 changed files with 0 additions and 3203 deletions
-509
View File
@@ -1,509 +0,0 @@
# 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/通用混淆 |
-7
View File
@@ -41,13 +41,6 @@
<dependencies>
<!-- ASM 字节码分析(API Trim 依赖) -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.6</version>
</dependency>
<!-- MyBatis Generator Core -->
<dependency>
<groupId>org.mybatis.generator</groupId>
@@ -1,97 +0,0 @@
package com.example.workutils.biz.apitrimmer;
import com.example.workutils.biz.apitrimmer.service.ApiTrimmerService;
import com.example.workutils.system.config.WorkUtilsProperties;
import com.example.workutils.system.plugin.Plugin;
import com.example.workutils.system.plugin.PluginDescriptor;
import com.example.workutils.system.plugin.VisualPluginFactory;
import com.example.workutils.system.support.JfxBootstrap;
import javafx.scene.Parent;
import javafx.scene.layout.Pane;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
/**
* API Trim 插件:精简 API JAR 包,移除冗余实现类与传递依赖。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiTrimmerPlugin implements Plugin {
private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder()
.id("api-trim")
.name("API Trim")
.version("1.0.0")
.description("精简API模块JAR包,移除冗余实现类与传递依赖")
.order(65)
.icon("✂️")
.build();
private static final String[] MENU_OPTIONS = {
"1. UI可视化操作(选择JAR → 分析 → 精简)",
"0. 返回上级菜单"
};
private final ApiTrimmerService service;
private final WorkUtilsProperties properties;
@Override public PluginDescriptor getDescriptor() { return DESCRIPTOR; }
@Override public String[] getMenuOptions() { return MENU_OPTIONS; }
@Override
public VisualPluginFactory getVisualFactory() {
return new VisualPluginFactory() {
private ApiTrimmerWindow window;
private Parent embeddedRoot;
private CountDownLatch latch;
@Override
public Parent getEmbeddedPane() {
if (embeddedRoot != null) return embeddedRoot;
latch = new CountDownLatch(1);
window = new ApiTrimmerWindow(service, properties, latch);
embeddedRoot = window.getContentPane();
window.getStage().getScene().setRoot(new Pane());
return embeddedRoot;
}
@Override
public void open(Runnable onClosed) {
if (window == null) {
latch = new CountDownLatch(1);
window = new ApiTrimmerWindow(service, properties, latch);
}
if (embeddedRoot != null && window.getStage().getScene().getRoot() != embeddedRoot) {
window.getStage().getScene().setRoot(embeddedRoot);
}
window.show();
new Thread(() -> {
try { latch.await(); } catch (InterruptedException ignored) {}
onClosed.run();
}, "apitrim-close").start();
}
};
}
@Override
public void handleChoice(int choice, Scanner scanner) throws Exception {
if (choice == 1) {
System.out.println("正在打开 API Trim 窗口...");
JfxBootstrap.await();
if (JfxBootstrap.isFailed()) {
System.err.println("JavaFX 初始化失败");
return;
}
CountDownLatch closeLatch = new CountDownLatch(1);
javafx.application.Platform.runLater(() ->
new ApiTrimmerWindow(service, properties, closeLatch).show());
closeLatch.await();
System.out.println("API Trim 窗口已关闭");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,5 +0,0 @@
# API Trim 自动勾选目录名配置(每行一个关键词,忽略大小写,匹配即勾选)
# 修改后保存,点击「刷新目录」即可生效
lombok
ttf
class50