JAR 瘦身插件
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
# 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/通用混淆 |
|
||||
@@ -41,6 +41,13 @@
|
||||
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
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 窗口已关闭");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
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.ui.StyledButton;
|
||||
import com.example.workutils.system.ui.Theme;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.CheckBoxTableCell;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.*;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
/**
|
||||
* API Trim 可视化窗口。
|
||||
* <p>
|
||||
* 功能:选择 API JAR → 查看目录结构并勾选整目录删除 → 分析冗余 → 执行精简。
|
||||
*/
|
||||
public class ApiTrimmerWindow {
|
||||
|
||||
private final Stage stage;
|
||||
private final CountDownLatch closeLatch;
|
||||
private final ApiTrimmerService service;
|
||||
private final WorkUtilsProperties properties;
|
||||
|
||||
// ---- UI 组件 ----
|
||||
private TextField jarPathField;
|
||||
private Label autoCheckLabel; // 显示当前配置的关键词
|
||||
// ---- 目录表格 ----
|
||||
private TableView<DirRow> dirTable;
|
||||
private final ObservableList<DirRow> dirRows = FXCollections.observableArrayList();
|
||||
private Label dirTableLabel;
|
||||
// ----
|
||||
private VBox resultBox;
|
||||
private Button trimBtn;
|
||||
private Button cancelBtn;
|
||||
|
||||
// ---- 状态 ----
|
||||
private ApiTrimmerService.TrimAnalysis analysis;
|
||||
/** 自动勾选关键词集合,从配置文件加载 */
|
||||
private Set<String> autoCheckPatterns = new LinkedHashSet<>();
|
||||
/** data 目录下的配置文件路径 */
|
||||
private Path configFile;
|
||||
|
||||
// ==================== 配置加载 ====================
|
||||
|
||||
private static final String PATTERNS_FILE = "api-trimmer-patterns.txt";
|
||||
private static final String DATA_DIR = "data";
|
||||
|
||||
/** 初始化配置文件:若 data 目录下不存在,则从 classpath 模板复制 */
|
||||
private void initConfigFile() {
|
||||
Path dataDir = Path.of(DATA_DIR);
|
||||
configFile = dataDir.resolve(PATTERNS_FILE);
|
||||
try {
|
||||
if (!Files.exists(dataDir)) {
|
||||
Files.createDirectories(dataDir);
|
||||
}
|
||||
if (!Files.exists(configFile)) {
|
||||
// 从 classpath 模板复制
|
||||
try (InputStream is = getClass().getClassLoader()
|
||||
.getResourceAsStream(PATTERNS_FILE)) {
|
||||
if (is != null) {
|
||||
Files.copy(is, configFile);
|
||||
} else {
|
||||
// fallback
|
||||
Files.writeString(configFile, "lombok\nttf\nclass50\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 兜底
|
||||
configFile = dataDir.resolve(PATTERNS_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
/** 从配置文件读取关键词到 autoCheckPatterns */
|
||||
private void loadPatterns() {
|
||||
autoCheckPatterns.clear();
|
||||
if (configFile == null || !Files.exists(configFile)) {
|
||||
autoCheckPatterns.addAll(List.of("lombok", "ttf", "class50"));
|
||||
return;
|
||||
}
|
||||
try (BufferedReader br = Files.newBufferedReader(configFile, StandardCharsets.UTF_8)) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
line = line.trim().toLowerCase();
|
||||
if (!line.isEmpty() && !line.startsWith("#")) {
|
||||
autoCheckPatterns.add(line);
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
if (autoCheckPatterns.isEmpty()) {
|
||||
autoCheckPatterns.addAll(List.of("lombok", "ttf", "class50"));
|
||||
}
|
||||
}
|
||||
|
||||
/** 用系统默认编辑器打开配置文件 */
|
||||
private void onEditConfig() {
|
||||
if (configFile == null) return;
|
||||
try {
|
||||
if (!Files.exists(configFile)) {
|
||||
Files.createDirectories(configFile.getParent());
|
||||
Files.writeString(configFile, "# API Trim 自动勾选关键词\nlombok\nttf\nclass50\n");
|
||||
}
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
Desktop.getDesktop().edit(configFile.toFile());
|
||||
} else {
|
||||
// fallback: 用 notepad 打开
|
||||
Runtime.getRuntime().exec(new String[]{"notepad.exe", configFile.toAbsolutePath().toString()});
|
||||
}
|
||||
} catch (IOException e) {
|
||||
showAlert("无法打开配置文件: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新配置显示标签 */
|
||||
private String patternsDisplay() {
|
||||
return String.join(", ", autoCheckPatterns);
|
||||
}
|
||||
|
||||
// ==================== TableView 数据行 ====================
|
||||
|
||||
public static class DirRow {
|
||||
private final ApiTrimmerService.DirInfo info;
|
||||
private final BooleanProperty selected;
|
||||
private final StringProperty name;
|
||||
private final IntegerProperty fileCount;
|
||||
private final IntegerProperty classCount;
|
||||
private final StringProperty size;
|
||||
|
||||
DirRow(ApiTrimmerService.DirInfo info, boolean selected) {
|
||||
this.info = info;
|
||||
this.selected = new SimpleBooleanProperty(selected);
|
||||
this.name = new SimpleStringProperty(info.getName());
|
||||
this.fileCount = new SimpleIntegerProperty(info.getFileCount());
|
||||
this.classCount = new SimpleIntegerProperty(info.getClassCount());
|
||||
this.size = new SimpleStringProperty(formatSize(info.getSizeBytes()));
|
||||
}
|
||||
|
||||
public ApiTrimmerService.DirInfo getInfo() { return info; }
|
||||
|
||||
public BooleanProperty selectedProperty() { return selected; }
|
||||
public boolean isSelected() { return selected.get(); }
|
||||
public void setSelected(boolean v) { selected.set(v); }
|
||||
|
||||
public StringProperty nameProperty() { return name; }
|
||||
public String getName() { return name.get(); }
|
||||
|
||||
public IntegerProperty fileCountProperty() { return fileCount; }
|
||||
public int getFileCount() { return fileCount.get(); }
|
||||
|
||||
public IntegerProperty classCountProperty() { return classCount; }
|
||||
public int getClassCount() { return classCount.get(); }
|
||||
|
||||
public StringProperty sizeProperty() { return size; }
|
||||
public String getSize() { return size.get(); }
|
||||
|
||||
private static String formatSize(long bytes) {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1_048_576) return String.format("%.1f KB", bytes / 1024.0);
|
||||
return String.format("%.1f MB", bytes / 1_048_576.0);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 构造 ====================
|
||||
|
||||
public ApiTrimmerWindow(ApiTrimmerService service, WorkUtilsProperties properties,
|
||||
CountDownLatch closeLatch) {
|
||||
this.service = service;
|
||||
this.properties = properties;
|
||||
this.closeLatch = closeLatch;
|
||||
this.stage = new Stage();
|
||||
initConfigFile();
|
||||
loadPatterns();
|
||||
buildUI();
|
||||
}
|
||||
|
||||
private int fsLabel() { return properties.getFontFieldLabel(); }
|
||||
private int fsButton() { return properties.getFontButton(); }
|
||||
private int fsBody() { return properties.getFontBody(); }
|
||||
|
||||
public void show() { stage.show(); }
|
||||
|
||||
public Parent getContentPane() {
|
||||
return stage.getScene().getRoot();
|
||||
}
|
||||
|
||||
Stage getStage() { return stage; }
|
||||
|
||||
// ==================== UI 构建 ====================
|
||||
|
||||
private void buildUI() {
|
||||
stage.setTitle("API Trim - 精简API JAR");
|
||||
stage.initModality(Modality.NONE);
|
||||
stage.setOnCloseRequest(e -> close());
|
||||
|
||||
VBox root = new VBox(Theme.S3);
|
||||
root.setPadding(new Insets(Theme.S4));
|
||||
root.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_BODY) + ";");
|
||||
|
||||
Label title = new Label("API Trim - 精简API JAR包");
|
||||
title.setFont(Theme.heading(properties.getFontSectionTitle() + 2));
|
||||
title.setTextFill(Theme.TEXT_PRIMARY);
|
||||
|
||||
VBox inputSection = createInputSection();
|
||||
|
||||
// 结果区
|
||||
resultBox = new VBox(Theme.S2);
|
||||
resultBox.setPadding(new Insets(Theme.S2, Theme.S3, Theme.S2, Theme.S3));
|
||||
resultBox.setStyle("-fx-background-color: white; -fx-background-radius: "
|
||||
+ Theme.RADIUS_SM + "; -fx-border-color: " + Theme.toWeb(Theme.BORDER)
|
||||
+ "; -fx-border-width: 1; -fx-border-radius: " + Theme.RADIUS_SM + ";");
|
||||
resultBox.setVisible(false);
|
||||
resultBox.setManaged(false);
|
||||
ScrollPane resultScroll = new ScrollPane(resultBox);
|
||||
resultScroll.setFitToWidth(true);
|
||||
resultScroll.setPrefHeight(180);
|
||||
resultScroll.setStyle("-fx-background-color: transparent;");
|
||||
|
||||
HBox btnBar = createButtonBar();
|
||||
|
||||
root.getChildren().addAll(title, inputSection, resultScroll, btnBar);
|
||||
VBox.setVgrow(resultScroll, Priority.ALWAYS);
|
||||
|
||||
Scene scene = new Scene(root, 920, 780);
|
||||
stage.setScene(scene);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private VBox createInputSection() {
|
||||
VBox section = new VBox(Theme.S2);
|
||||
section.setPadding(new Insets(Theme.S3));
|
||||
section.setStyle("-fx-background-color: white; -fx-background-radius: "
|
||||
+ Theme.RADIUS_MD + "; -fx-border-color: " + Theme.toWeb(Theme.BORDER)
|
||||
+ "; -fx-border-width: 1; -fx-border-radius: " + Theme.RADIUS_MD + ";");
|
||||
|
||||
// ---- JAR 选择行 ----
|
||||
Label jarLabel = new Label("API JAR 文件:");
|
||||
jarLabel.setFont(Font.font("Microsoft YaHei", fsLabel()));
|
||||
jarPathField = new TextField();
|
||||
jarPathField.setPromptText("选择 api.jar...");
|
||||
jarPathField.getStyleClass().add(Theme.CSS_TEXT_FIELD);
|
||||
HBox.setHgrow(jarPathField, Priority.ALWAYS);
|
||||
|
||||
Button browseBtn = StyledButton.small("浏览...",
|
||||
Font.font("Microsoft YaHei", fsBody() - 1));
|
||||
browseBtn.setOnAction(e -> onBrowseJar());
|
||||
|
||||
Button refreshBtn = StyledButton.small("刷新目录",
|
||||
Font.font("Microsoft YaHei", fsBody() - 1));
|
||||
refreshBtn.setOnAction(e -> onRefreshDirs());
|
||||
|
||||
HBox jarRow = new HBox(Theme.S2, jarLabel, jarPathField, browseBtn, refreshBtn);
|
||||
jarRow.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
// ---- 自动勾选配置信息行 ----
|
||||
autoCheckLabel = new Label("自动勾选: " + patternsDisplay());
|
||||
autoCheckLabel.setFont(Font.font("Microsoft YaHei", fsBody() - 1));
|
||||
autoCheckLabel.setTextFill(Theme.TEXT_MUTED);
|
||||
|
||||
Button editConfigBtn = StyledButton.small("编辑",
|
||||
Font.font("Microsoft YaHei", fsBody() - 2));
|
||||
editConfigBtn.setOnAction(e -> onEditConfig());
|
||||
|
||||
HBox configRow = new HBox(Theme.S2, autoCheckLabel, editConfigBtn);
|
||||
configRow.setAlignment(Pos.CENTER_LEFT);
|
||||
configRow.setPadding(new Insets(Theme.S1, 0, Theme.S1, 0));
|
||||
|
||||
// ---- 目录表格 (7-Zip 风格) ----
|
||||
dirTableLabel = new Label("JAR 内目录(勾选 = 整目录删除):");
|
||||
dirTableLabel.setFont(Font.font("Microsoft YaHei", fsLabel()));
|
||||
|
||||
dirTable = new TableView<>(dirRows);
|
||||
dirTable.setEditable(true);
|
||||
dirTable.setPrefHeight(320);
|
||||
dirTable.setPlaceholder(new Label("(选择 JAR 文件后自动加载目录)"));
|
||||
|
||||
// 列1:复选框
|
||||
TableColumn<DirRow, Boolean> selCol = new TableColumn<>("");
|
||||
selCol.setPrefWidth(36);
|
||||
selCol.setMinWidth(36);
|
||||
selCol.setMaxWidth(36);
|
||||
selCol.setSortable(false);
|
||||
selCol.setCellValueFactory(cdf -> cdf.getValue().selectedProperty());
|
||||
selCol.setCellFactory(CheckBoxTableCell.forTableColumn(selCol));
|
||||
// 点击复选框区域即可切换,同时支持表头全选/全不选
|
||||
selCol.setCellFactory(tc -> {
|
||||
CheckBoxTableCell<DirRow, Boolean> cell = new CheckBoxTableCell<>();
|
||||
return cell;
|
||||
});
|
||||
|
||||
// 列2:目录名
|
||||
TableColumn<DirRow, String> nameCol = new TableColumn<>("目录");
|
||||
nameCol.setCellValueFactory(cdf -> cdf.getValue().nameProperty());
|
||||
nameCol.setPrefWidth(280);
|
||||
|
||||
// 列3:文件数
|
||||
TableColumn<DirRow, Number> fileCol = new TableColumn<>("文件数");
|
||||
fileCol.setCellValueFactory(cdf -> cdf.getValue().fileCountProperty());
|
||||
fileCol.setPrefWidth(80);
|
||||
fileCol.setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
|
||||
// 列4:类文件
|
||||
TableColumn<DirRow, Number> classCol = new TableColumn<>("类文件");
|
||||
classCol.setCellValueFactory(cdf -> cdf.getValue().classCountProperty());
|
||||
classCol.setPrefWidth(80);
|
||||
classCol.setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
|
||||
// 列5:大小
|
||||
TableColumn<DirRow, String> sizeCol = new TableColumn<>("大小");
|
||||
sizeCol.setCellValueFactory(cdf -> cdf.getValue().sizeProperty());
|
||||
sizeCol.setPrefWidth(100);
|
||||
sizeCol.setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
|
||||
dirTable.getColumns().setAll(selCol, nameCol, fileCol, classCol, sizeCol);
|
||||
|
||||
// 表格样式
|
||||
dirTable.setStyle(
|
||||
"-fx-font-family: 'Microsoft YaHei';" +
|
||||
"-fx-font-size: " + (fsBody() - 1) + "px;" +
|
||||
"-fx-border-color: " + Theme.toWeb(Theme.BORDER) + ";" +
|
||||
"-fx-border-width: 0 0 1 0;");
|
||||
|
||||
section.getChildren().addAll(jarRow, configRow, dirTableLabel, dirTable);
|
||||
VBox.setVgrow(dirTable, Priority.ALWAYS);
|
||||
return section;
|
||||
}
|
||||
|
||||
private HBox createButtonBar() {
|
||||
HBox bar = new HBox(Theme.S4);
|
||||
bar.setAlignment(Pos.CENTER_RIGHT);
|
||||
bar.setPadding(new Insets(Theme.S2, 0, 0, 0));
|
||||
|
||||
trimBtn = StyledButton.primary("执行精简",
|
||||
Font.font("Microsoft YaHei", fsButton()));
|
||||
trimBtn.setPrefWidth(110);
|
||||
trimBtn.setOnAction(e -> onTrim());
|
||||
|
||||
cancelBtn = StyledButton.secondary("取消",
|
||||
Font.font("Microsoft YaHei", fsButton()));
|
||||
cancelBtn.setPrefWidth(80);
|
||||
cancelBtn.setOnAction(e -> close());
|
||||
|
||||
bar.getChildren().addAll(trimBtn, cancelBtn);
|
||||
return bar;
|
||||
}
|
||||
|
||||
// ==================== TrimConfig 构建 ====================
|
||||
|
||||
private ApiTrimmerService.TrimConfig getTrimConfig() {
|
||||
ApiTrimmerService.TrimConfig config = new ApiTrimmerService.TrimConfig();
|
||||
config.skipLombok = false;
|
||||
config.skipFonts = false;
|
||||
config.skipIdeAnnotations = false;
|
||||
config.skipShadeRepack = false;
|
||||
config.skipNativeLibs = false;
|
||||
config.skipSpringMeta = false;
|
||||
for (DirRow row : dirRows) {
|
||||
if (row.isSelected()) {
|
||||
config.excludedDirs.add(row.getInfo().getPath());
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
private void onBrowseJar() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle("选择 API JAR 文件");
|
||||
chooser.getExtensionFilters().add(
|
||||
new FileChooser.ExtensionFilter("JAR 文件", "*.jar"));
|
||||
File file = chooser.showOpenDialog(stage);
|
||||
if (file != null) {
|
||||
jarPathField.setText(file.getAbsolutePath());
|
||||
refreshDirList(Path.of(file.getAbsolutePath()));
|
||||
}
|
||||
}
|
||||
|
||||
private void onRefreshDirs() {
|
||||
String path = jarPathField.getText().trim();
|
||||
if (path.isEmpty()) return;
|
||||
Path jarPath = Path.of(path);
|
||||
if (Files.exists(jarPath)) {
|
||||
loadPatterns();
|
||||
autoCheckLabel.setText("自动勾选: " + patternsDisplay());
|
||||
refreshDirList(jarPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshDirList(Path jarPath) {
|
||||
dirRows.clear();
|
||||
try {
|
||||
List<ApiTrimmerService.DirInfo> dirs = service.scanJarDirectories(jarPath);
|
||||
for (ApiTrimmerService.DirInfo d : dirs) {
|
||||
boolean preSelect = matchesAnyPattern(d.getName());
|
||||
dirRows.add(new DirRow(d, preSelect));
|
||||
}
|
||||
dirTableLabel.setText("JAR 内目录(勾选 = 整目录删除):");
|
||||
} catch (IOException e) {
|
||||
dirTableLabel.setText("目录扫描失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchesAnyPattern(String dirName) {
|
||||
String lower = dirName.toLowerCase();
|
||||
for (String p : autoCheckPatterns) {
|
||||
if (lower.contains(p)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void onTrim() {
|
||||
String path = jarPathField.getText().trim();
|
||||
if (path.isEmpty()) {
|
||||
showAlert("请先选择 API JAR 文件");
|
||||
return;
|
||||
}
|
||||
Path jarPath = Path.of(path);
|
||||
if (!Files.exists(jarPath)) {
|
||||
showAlert("文件不存在: " + path);
|
||||
return;
|
||||
}
|
||||
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle("保存精简后的 JAR");
|
||||
chooser.getExtensionFilters().add(
|
||||
new FileChooser.ExtensionFilter("JAR 文件", "*.jar"));
|
||||
String srcName = jarPath.getFileName().toString();
|
||||
chooser.setInitialFileName(srcName.replace(".jar", "-trimmed.jar"));
|
||||
File outputFile = chooser.showSaveDialog(stage);
|
||||
if (outputFile == null) return;
|
||||
Path outputPath = outputFile.toPath();
|
||||
|
||||
trimBtn.setDisable(true);
|
||||
trimBtn.setText("精简中...");
|
||||
|
||||
ApiTrimmerService.TrimConfig config = getTrimConfig();
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
analysis = service.analyze(jarPath, List.of(), config);
|
||||
Platform.runLater(this::showResult);
|
||||
|
||||
service.execute(jarPath, analysis, outputPath, config);
|
||||
long newSize = Files.size(outputPath);
|
||||
double reduction = 100.0 * (1.0 - (double) newSize / analysis.getOriginalSizeBytes());
|
||||
Platform.runLater(() -> {
|
||||
appendOutputLine("精简完成!");
|
||||
appendOutputLine("输出: " + outputPath.toAbsolutePath());
|
||||
appendOutputLine(String.format("新大小: %.1f MB ( %.1f%%)",
|
||||
newSize / 1_048_576.0, reduction));
|
||||
showInfo("精简完成",
|
||||
"输出文件:\n" + outputPath.toAbsolutePath() + "\n\n"
|
||||
+ String.format("原始: %.1f MB 精简: %.1f MB ( %.1f%%)",
|
||||
analysis.getOriginalSizeBytes() / 1_048_576.0,
|
||||
newSize / 1_048_576.0,
|
||||
reduction));
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Platform.runLater(() -> showAlert("精简失败: " + e.getMessage()));
|
||||
} finally {
|
||||
Platform.runLater(() -> {
|
||||
trimBtn.setDisable(false);
|
||||
trimBtn.setText("执行精简");
|
||||
});
|
||||
}
|
||||
}, "api-trim-execute").start();
|
||||
}
|
||||
|
||||
// ==================== 结果显示 ====================
|
||||
|
||||
private void showResult() {
|
||||
resultBox.getChildren().clear();
|
||||
resultBox.setVisible(true);
|
||||
resultBox.setManaged(true);
|
||||
|
||||
addStatRow("原始类数:", String.valueOf(analysis.getTotalClassCount()));
|
||||
addStatRow("保留类数:", analysis.getRetainedClassCount() + " (" +
|
||||
String.format("%.1f%%", 100.0 - analysis.getReductionPercent()) + ")");
|
||||
addStatRow("移除类数:", analysis.getRemovedClassCount() + " (" +
|
||||
String.format("%.1f%%", analysis.getReductionPercent()) + ")");
|
||||
addStatRow("入口类数:", String.valueOf(analysis.getEntryPoints().size()));
|
||||
addStatRow("原始大小:", String.format("%.1f MB", analysis.getOriginalSizeBytes() / 1_048_576.0));
|
||||
|
||||
if (!analysis.getEntryPoints().isEmpty()) {
|
||||
Label entryLabel = new Label("入口类:");
|
||||
entryLabel.setFont(Font.font("Microsoft YaHei", fsBody()));
|
||||
entryLabel.setStyle("-fx-font-weight: bold;");
|
||||
resultBox.getChildren().add(entryLabel);
|
||||
TextFlow flow = new TextFlow();
|
||||
flow.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_BODY)
|
||||
+ "; -fx-padding: 4 8 4 8; -fx-background-radius: " + Theme.RADIUS_SM + ";");
|
||||
for (int i = 0; i < Math.min(analysis.getEntryPoints().size(), 20); i++) {
|
||||
Text t = new Text(analysis.getEntryPoints().get(i));
|
||||
t.setFont(Font.font("Consolas", fsBody() - 2));
|
||||
t.setFill(Theme.TEXT_PRIMARY);
|
||||
flow.getChildren().add(t);
|
||||
if (i < Math.min(analysis.getEntryPoints().size(), 20) - 1) {
|
||||
flow.getChildren().add(new Text("\n"));
|
||||
}
|
||||
}
|
||||
if (analysis.getEntryPoints().size() > 20) {
|
||||
flow.getChildren().add(new Text(
|
||||
"\n...还有 " + (analysis.getEntryPoints().size() - 20) + " 个"));
|
||||
}
|
||||
resultBox.getChildren().add(flow);
|
||||
}
|
||||
}
|
||||
|
||||
private void addStatRow(String label, String value) {
|
||||
HBox row = new HBox(Theme.S2);
|
||||
row.setAlignment(Pos.CENTER_LEFT);
|
||||
Label lbl = new Label(label);
|
||||
lbl.setFont(Font.font("Microsoft YaHei", fsBody()));
|
||||
lbl.setPrefWidth(100);
|
||||
lbl.setStyle("-fx-font-weight: bold;");
|
||||
Label val = new Label(value);
|
||||
val.setFont(Font.font("Consolas", fsBody()));
|
||||
val.setTextFill(Theme.PRIMARY);
|
||||
row.getChildren().addAll(lbl, val);
|
||||
resultBox.getChildren().add(row);
|
||||
}
|
||||
|
||||
private void appendOutputLine(String text) {
|
||||
Label line = new Label(text);
|
||||
line.setFont(Font.font("Consolas", fsBody() - 1));
|
||||
line.setTextFill(Theme.SUCCESS);
|
||||
Platform.runLater(() -> resultBox.getChildren().add(line));
|
||||
}
|
||||
|
||||
// ==================== 对话框 ====================
|
||||
|
||||
private void showAlert(String msg) {
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle("提示");
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(msg);
|
||||
alert.initOwner(stage);
|
||||
alert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
private void showInfo(String title, String msg) {
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(msg);
|
||||
alert.initOwner(stage);
|
||||
alert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
private void close() {
|
||||
closeLatch.countDown();
|
||||
stage.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
package com.example.workutils.biz.apitrimmer.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.objectweb.asm.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.jar.*;
|
||||
|
||||
/**
|
||||
* API JAR 精简服务:通过 ASM 字节码可达性分析,剔除实现类及冗余依赖。
|
||||
* <p>
|
||||
* 算法流程:
|
||||
* <ol>
|
||||
* <li>读取 JAR 中所有 .class → className→byte[] 字典</li>
|
||||
* <li>扫描 RPC 入口(接口 + RPC 注解类)</li>
|
||||
* <li>BFS 可达性分析:从入口出发遍历所有类型引用</li>
|
||||
* <li>JAR 重打包:只输出可达类</li>
|
||||
* </ol>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ApiTrimmerService {
|
||||
|
||||
/** 精简配置(UI 中的可选开关) */
|
||||
public static class TrimConfig {
|
||||
public boolean skipLombok = true;
|
||||
public boolean skipFonts = true;
|
||||
public boolean skipIdeAnnotations = true;
|
||||
public boolean skipShadeRepack = true;
|
||||
public boolean skipNativeLibs = true;
|
||||
public boolean skipSpringMeta = true;
|
||||
/** 直接整目录删除的路径前缀列表(如 "org/projectlombok/", "fonts/") */
|
||||
public Set<String> excludedDirs = new HashSet<>();
|
||||
}
|
||||
|
||||
/** JAR 内目录信息 */
|
||||
public static class DirInfo {
|
||||
private final String path; // 如 "org/projectlombok/"
|
||||
private final String name; // 如 "org/projectlombok"
|
||||
private final int classCount;
|
||||
private final int fileCount;
|
||||
private final long sizeBytes;
|
||||
|
||||
public DirInfo(String path, String name, int classCount, int fileCount, long sizeBytes) {
|
||||
this.path = path;
|
||||
this.name = name;
|
||||
this.classCount = classCount;
|
||||
this.fileCount = fileCount;
|
||||
this.sizeBytes = sizeBytes;
|
||||
}
|
||||
public String getPath() { return path; }
|
||||
public String getName() { return name; }
|
||||
public int getClassCount() { return classCount; }
|
||||
public int getFileCount() { return fileCount; }
|
||||
public long getSizeBytes() { return sizeBytes; }
|
||||
}
|
||||
|
||||
/** 分析结果数据对象 */
|
||||
public static class TrimAnalysis {
|
||||
private final int totalClassCount;
|
||||
private final int retainedClassCount;
|
||||
private final Set<String> retainedClasses;
|
||||
private final long originalSizeBytes;
|
||||
private final List<String> entryPoints;
|
||||
|
||||
public TrimAnalysis(int totalClassCount, int retainedClassCount,
|
||||
Set<String> retainedClasses, long originalSizeBytes,
|
||||
List<String> entryPoints) {
|
||||
this.totalClassCount = totalClassCount;
|
||||
this.retainedClassCount = retainedClassCount;
|
||||
this.retainedClasses = Collections.unmodifiableSet(retainedClasses);
|
||||
this.originalSizeBytes = originalSizeBytes;
|
||||
this.entryPoints = Collections.unmodifiableList(entryPoints);
|
||||
}
|
||||
|
||||
public int getTotalClassCount() { return totalClassCount; }
|
||||
public int getRetainedClassCount() { return retainedClassCount; }
|
||||
public int getRemovedClassCount() { return totalClassCount - retainedClassCount; }
|
||||
public Set<String> getRetainedClasses() { return retainedClasses; }
|
||||
public long getOriginalSizeBytes() { return originalSizeBytes; }
|
||||
public List<String> getEntryPoints() { return entryPoints; }
|
||||
|
||||
public double getReductionPercent() {
|
||||
return totalClassCount == 0 ? 0 :
|
||||
100.0 * getRemovedClassCount() / totalClassCount;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 目录扫描(供 UI 展示选择) ====================
|
||||
|
||||
/**
|
||||
* 扫描 JAR 内所有顶层目录,返回目录信息列表(按大小降序)。
|
||||
*/
|
||||
public List<DirInfo> scanJarDirectories(Path jarPath) throws IOException {
|
||||
Map<String, Integer> classCounts = new HashMap<>();
|
||||
Map<String, Long> sizeMap = new HashMap<>();
|
||||
Map<String, Integer> fileCounts = new HashMap<>();
|
||||
|
||||
try (JarInputStream jis = new JarInputStream(Files.newInputStream(jarPath))) {
|
||||
JarEntry entry;
|
||||
while ((entry = jis.getNextJarEntry()) != null) {
|
||||
String name = entry.getName();
|
||||
if (name.equals("META-INF/") || name.equals("module-info.class")) continue;
|
||||
|
||||
// 提取顶层目录名
|
||||
int slash = name.indexOf('/');
|
||||
if (slash <= 0) continue;
|
||||
String topDir = name.substring(0, slash + 1); // "com/" "org/" "fonts/"
|
||||
|
||||
fileCounts.merge(topDir, 1, Integer::sum);
|
||||
sizeMap.merge(topDir, entry.getCompressedSize(), Long::sum);
|
||||
if (name.endsWith(".class")) {
|
||||
classCounts.merge(topDir, 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sizeMap.entrySet().stream()
|
||||
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
|
||||
.map(e -> new DirInfo(
|
||||
e.getKey(),
|
||||
e.getKey().substring(0, e.getKey().length() - 1),
|
||||
classCounts.getOrDefault(e.getKey(), 0),
|
||||
fileCounts.getOrDefault(e.getKey(), 0),
|
||||
e.getValue()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ==================== 分析入口 ====================
|
||||
|
||||
/**
|
||||
* 分析 JAR 文件,返回精简方案。
|
||||
*
|
||||
* @param jarPath 原始 JAR 路径
|
||||
* @param extraPackages 额外保留的包名模式(如 com.example.common.dto.**)
|
||||
* @param config 排除规则开关
|
||||
*/
|
||||
public TrimAnalysis analyze(Path jarPath, List<String> extraPackages, TrimConfig config) throws IOException {
|
||||
long originalSize = Files.size(jarPath);
|
||||
|
||||
// Step 1: 读取所有类(排除已选 junk 类)
|
||||
Map<String, byte[]> allClasses = loadAllClasses(jarPath, config);
|
||||
log.info("分析 JAR: {} 个类, 大小 {}MB", allClasses.size(),
|
||||
String.format("%.1f", originalSize / 1_048_576.0));
|
||||
|
||||
// Step 2: 扫描入口
|
||||
Set<String> entryPoints = findEntryPoints(allClasses);
|
||||
log.info("找到 {} 个入口类", entryPoints.size());
|
||||
|
||||
// Step 3: BFS 可达性分析
|
||||
Set<String> reachable = computeReachable(allClasses, entryPoints);
|
||||
|
||||
// Step 4: 追加用户指定的额外包
|
||||
if (extraPackages != null) {
|
||||
int before = reachable.size();
|
||||
for (String pkg : extraPackages) {
|
||||
for (String cls : allClasses.keySet()) {
|
||||
if (matchPackage(cls, pkg)) {
|
||||
reachable.add(cls);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reachable.size() > before) {
|
||||
log.info("额外包追加了 {} 个类", reachable.size() - before);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: 对 reachable 再做一轮闭包(新加入的类可能引用其他类)
|
||||
Set<String> closure = computeReachable(allClasses, reachable);
|
||||
if (closure.size() > reachable.size()) {
|
||||
log.info("闭包追加了 {} 个类", closure.size() - reachable.size());
|
||||
}
|
||||
|
||||
return new TrimAnalysis(
|
||||
allClasses.size(),
|
||||
closure.size(),
|
||||
closure,
|
||||
originalSize,
|
||||
new ArrayList<>(entryPoints)
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 执行精简 ====================
|
||||
|
||||
/**
|
||||
* 根据分析结果重新打包 JAR,只保留白名单中的类。
|
||||
*/
|
||||
public Path execute(Path jarPath, TrimAnalysis analysis, Path outputPath, TrimConfig config) throws IOException {
|
||||
Set<String> retained = analysis.getRetainedClasses();
|
||||
int written = 0;
|
||||
|
||||
try (JarInputStream jis = new JarInputStream(Files.newInputStream(jarPath));
|
||||
JarOutputStream jos = new JarOutputStream(Files.newOutputStream(outputPath))) {
|
||||
|
||||
JarEntry entry;
|
||||
while ((entry = jis.getNextJarEntry()) != null) {
|
||||
String name = entry.getName();
|
||||
|
||||
// 排除用户选择的整目录
|
||||
if (isUnderExcludedDir(name, config)) continue;
|
||||
|
||||
// 保留非 class 文件(manifest、资源等)
|
||||
if (!name.endsWith(".class")) {
|
||||
if (shouldSkipResource(name, config)) continue;
|
||||
jos.putNextEntry(new JarEntry(name));
|
||||
jis.transferTo(jos);
|
||||
jos.closeEntry();
|
||||
continue;
|
||||
}
|
||||
|
||||
// class 文件 → 判断是否在白名单
|
||||
String className = jarEntryToClassName(name);
|
||||
if (retained.contains(className)) {
|
||||
jos.putNextEntry(new JarEntry(name));
|
||||
jis.transferTo(jos);
|
||||
jos.closeEntry();
|
||||
written++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("精简完成: {} 个类 → {}", analysis.getTotalClassCount(), written);
|
||||
log.info("输出: {} ({}MB)", outputPath,
|
||||
String.format("%.1f", Files.size(outputPath) / 1_048_576.0));
|
||||
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
// ==================== 内部:JAR 读取 ====================
|
||||
|
||||
private Map<String, byte[]> loadAllClasses(Path jarPath, TrimConfig config) throws IOException {
|
||||
Map<String, byte[]> classes = new LinkedHashMap<>();
|
||||
try (JarInputStream jis = new JarInputStream(Files.newInputStream(jarPath))) {
|
||||
JarEntry entry;
|
||||
while ((entry = jis.getNextJarEntry()) != null) {
|
||||
if (entry.getName().endsWith(".class") && !entry.getName().equals("module-info.class")) {
|
||||
String className = jarEntryToClassName(entry.getName());
|
||||
if (isJunkClass(className, config)) continue;
|
||||
if (isUnderExcludedDir(entry.getName(), config)) continue;
|
||||
classes.put(className, jis.readAllBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
/** 判断 entry 是否位于用户选择的排除目录下 */
|
||||
private static boolean isUnderExcludedDir(String entryName, TrimConfig config) {
|
||||
if (config.excludedDirs.isEmpty()) return false;
|
||||
for (String dir : config.excludedDirs) {
|
||||
if (entryName.startsWith(dir)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 判断是否跳过冗余类:根据 config 开关 */
|
||||
private static boolean isJunkClass(String className, TrimConfig config) {
|
||||
if (config.skipLombok &&
|
||||
(className.startsWith("org.projectlombok.") || className.startsWith("lombok."))) {
|
||||
return true;
|
||||
}
|
||||
if (config.skipIdeAnnotations &&
|
||||
(className.startsWith("org.intellij.lang.annotations.")
|
||||
|| className.startsWith("org.jetbrains.annotations.")
|
||||
|| className.startsWith("javax.annotation.processing."))) {
|
||||
return true;
|
||||
}
|
||||
if (config.skipShadeRepack && className.contains(".shaded.")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 判断是否跳过非 API 必需的资源文件:根据 config 开关 */
|
||||
private static boolean shouldSkipResource(String entryName, TrimConfig config) {
|
||||
// Maven 元数据(始终跳过)
|
||||
if (entryName.startsWith("META-INF/maven/") || entryName.equals("META-INF/")) {
|
||||
return true;
|
||||
}
|
||||
// Spring 配置元数据
|
||||
if (config.skipSpringMeta) {
|
||||
if (entryName.startsWith("META-INF/spring-configuration-metadata.json")
|
||||
|| entryName.startsWith("META-INF/spring.factories")
|
||||
|| entryName.startsWith("META-INF/spring/")
|
||||
|| entryName.contains("application")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 字体文件
|
||||
if (config.skipFonts) {
|
||||
String lower = entryName.toLowerCase();
|
||||
if (lower.endsWith(".ttf") || lower.endsWith(".otf")
|
||||
|| lower.endsWith(".woff") || lower.endsWith(".woff2")
|
||||
|| entryName.contains("/fonts/")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 原生动态库
|
||||
if (config.skipNativeLibs) {
|
||||
String lower = entryName.toLowerCase();
|
||||
if (lower.endsWith(".dll") || lower.endsWith(".so")
|
||||
|| lower.endsWith(".dylib")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String jarEntryToClassName(String entryName) {
|
||||
return entryName.substring(0, entryName.length() - 6).replace('/', '.');
|
||||
}
|
||||
|
||||
// ==================== 内部:入口扫描 ====================
|
||||
|
||||
/**
|
||||
* 扫描入口类:所有接口 + 带 RPC 注解的类。
|
||||
*/
|
||||
private Set<String> findEntryPoints(Map<String, byte[]> allClasses) {
|
||||
Set<String> entries = new LinkedHashSet<>();
|
||||
for (Map.Entry<String, byte[]> e : allClasses.entrySet()) {
|
||||
String name = e.getKey();
|
||||
byte[] code = e.getValue();
|
||||
// 匿名内部类不作为入口
|
||||
if (name.contains("$")) continue;
|
||||
|
||||
try {
|
||||
ClassReader cr = new ClassReader(code);
|
||||
int access = cr.getAccess();
|
||||
// 接口是天然入口
|
||||
if ((access & Opcodes.ACC_INTERFACE) != 0) {
|
||||
entries.add(name);
|
||||
continue;
|
||||
}
|
||||
// 检查是否有 RPC 注解
|
||||
EntryDetector detector = new EntryDetector();
|
||||
cr.accept(detector, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
|
||||
if (detector.isRpcEntry) {
|
||||
entries.add(name);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
// ==================== 内部:BFS 可达性分析 ====================
|
||||
|
||||
private Set<String> computeReachable(Map<String, byte[]> allClasses,
|
||||
Set<String> seeds) {
|
||||
Set<String> reachable = new LinkedHashSet<>();
|
||||
Queue<String> queue = new ArrayDeque<>();
|
||||
|
||||
// 将 seeds 中存在于此 JAR 的类入队
|
||||
for (String seed : seeds) {
|
||||
if (allClasses.containsKey(seed)) {
|
||||
if (reachable.add(seed)) {
|
||||
queue.add(seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
String cls = queue.poll();
|
||||
byte[] code = allClasses.get(cls);
|
||||
if (code == null) continue;
|
||||
|
||||
try {
|
||||
// 提取所有类型引用
|
||||
Set<String> refs = extractTypeReferences(code);
|
||||
for (String ref : refs) {
|
||||
// 只关注 JAR 内的类
|
||||
if (!allClasses.containsKey(ref)) continue;
|
||||
if (reachable.add(ref)) {
|
||||
queue.add(ref);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("BFS 分析 {} 失败: {}", cls, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return reachable;
|
||||
}
|
||||
|
||||
private Set<String> extractTypeReferences(byte[] classBytes) {
|
||||
Set<String> refs = new LinkedHashSet<>();
|
||||
ClassReader cr = new ClassReader(classBytes);
|
||||
cr.accept(new ReferenceCollector(refs),
|
||||
ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
|
||||
return refs;
|
||||
}
|
||||
|
||||
// ==================== 内部:包名匹配 ====================
|
||||
|
||||
private static boolean matchPackage(String className, String pattern) {
|
||||
// 支持: com.example.dto.** 或 com.example.dto.* 或 com.example.dto
|
||||
if (pattern.endsWith(".**")) {
|
||||
return className.startsWith(pattern.substring(0, pattern.length() - 3));
|
||||
}
|
||||
if (pattern.endsWith(".*")) {
|
||||
String prefix = pattern.substring(0, pattern.length() - 2);
|
||||
if (!className.startsWith(prefix + ".")) return false;
|
||||
String suffix = className.substring(prefix.length() + 1);
|
||||
return !suffix.contains(".");
|
||||
}
|
||||
return className.startsWith(pattern);
|
||||
}
|
||||
|
||||
// ==================== ASM Visitors ====================
|
||||
|
||||
/** 检测类是否有 RPC 框架注解 */
|
||||
private static class EntryDetector extends ClassVisitor {
|
||||
boolean isRpcEntry = false;
|
||||
|
||||
EntryDetector() {
|
||||
super(Opcodes.ASM9);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
|
||||
String annotationName = Type.getType(desc).getClassName();
|
||||
if (annotationName.contains("DubboService")
|
||||
|| annotationName.contains("FeignClient")
|
||||
|| annotationName.contains("ThriftService")
|
||||
|| annotationName.contains("GrpcService")
|
||||
|| (annotationName.startsWith("org.apache.dubbo.config.annotation.")
|
||||
&& annotationName.contains("Service"))) {
|
||||
isRpcEntry = true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 收集类的所有类型引用 */
|
||||
private static class ReferenceCollector extends ClassVisitor {
|
||||
private final Set<String> refs;
|
||||
|
||||
ReferenceCollector(Set<String> refs) {
|
||||
super(Opcodes.ASM9);
|
||||
this.refs = refs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(int version, int access, String name,
|
||||
String signature, String superName, String[] interfaces) {
|
||||
addInternalName(superName);
|
||||
if (interfaces != null) {
|
||||
for (String itf : interfaces) addInternalName(itf);
|
||||
}
|
||||
// 泛型签名中可能包含类型引用
|
||||
parseSignature(signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FieldVisitor visitField(int access, String name, String desc,
|
||||
String signature, Object value) {
|
||||
addTypeDesc(desc);
|
||||
parseSignature(signature);
|
||||
return null; // 字段类型引用已从 desc/signature 提取
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String name, String desc,
|
||||
String signature, String[] exceptions) {
|
||||
for (Type type : Type.getArgumentTypes(desc)) {
|
||||
addType(type);
|
||||
}
|
||||
addType(Type.getReturnType(desc));
|
||||
if (exceptions != null) {
|
||||
for (String ex : exceptions) addInternalName(ex);
|
||||
}
|
||||
parseSignature(signature);
|
||||
return null; // 方法参数/返回类型已从 desc/signature 提取
|
||||
}
|
||||
|
||||
private void addInternalName(String internalName) {
|
||||
if (internalName == null) return;
|
||||
refs.add(internalName.replace('/', '.'));
|
||||
}
|
||||
|
||||
private void addTypeDesc(String desc) {
|
||||
addType(Type.getType(desc));
|
||||
}
|
||||
|
||||
private void addType(Type type) {
|
||||
if (type.getSort() == Type.OBJECT) {
|
||||
refs.add(type.getClassName());
|
||||
} else if (type.getSort() == Type.ARRAY) {
|
||||
addType(type.getElementType());
|
||||
}
|
||||
}
|
||||
|
||||
private void parseSignature(String signature) {
|
||||
if (signature == null) return;
|
||||
// 从泛型签名中提取所有 L...; 类型引用
|
||||
int pos = 0;
|
||||
while (pos < signature.length()) {
|
||||
int lt = signature.indexOf('L', pos);
|
||||
if (lt < 0) break;
|
||||
int semi = signature.indexOf(';', lt);
|
||||
if (semi < 0) break;
|
||||
String typeRef = signature.substring(lt + 1, semi).replace('/', '.');
|
||||
// 排除 JDK 标准库
|
||||
if (!typeRef.startsWith("java.") && !typeRef.startsWith("javax.")) {
|
||||
refs.add(typeRef);
|
||||
}
|
||||
pos = semi + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集注解中的类型引用(保留供未来扩展)。
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
private static class AnnotationRefCollector extends AnnotationVisitor {
|
||||
private final Set<String> refs;
|
||||
|
||||
AnnotationRefCollector(Set<String> refs) {
|
||||
super(Opcodes.ASM9);
|
||||
this.refs = refs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(String name, Object value) {
|
||||
if (value instanceof Type) {
|
||||
Type t = (Type) value;
|
||||
refs.add(t.getClassName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationVisitor visitAnnotation(String name, String desc) {
|
||||
refs.add(Type.getType(desc).getClassName());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationVisitor visitArray(String name) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitEnum(String name, String desc, String value) {
|
||||
refs.add(Type.getType(desc).getClassName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# API Trim 自动勾选目录名配置(每行一个关键词,忽略大小写,匹配即勾选)
|
||||
# 修改后保存,点击「刷新目录」即可生效
|
||||
lombok
|
||||
ttf
|
||||
class50
|
||||
Reference in New Issue
Block a user