diff --git a/docs/api-trim-maven-plugin-design.md b/docs/api-trim-maven-plugin-design.md deleted file mode 100644 index fed4e9e..0000000 --- a/docs/api-trim-maven-plugin-design.md +++ /dev/null @@ -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 基类子类 - │ └─ 用户通过 显式声明 - └─ 收集所有入口接口 → 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/通用混淆 | diff --git a/pom.xml b/pom.xml index ee8cbed..b5394f4 100644 --- a/pom.xml +++ b/pom.xml @@ -41,13 +41,6 @@ - - - org.ow2.asm - asm - 9.6 - - org.mybatis.generator diff --git a/src/main/java/com/example/workutils/biz/apitrimmer/ApiTrimmerPlugin.java b/src/main/java/com/example/workutils/biz/apitrimmer/ApiTrimmerPlugin.java deleted file mode 100644 index ac68c85..0000000 --- a/src/main/java/com/example/workutils/biz/apitrimmer/ApiTrimmerPlugin.java +++ /dev/null @@ -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 窗口已关闭"); - } - } -} diff --git a/src/main/java/com/example/workutils/biz/apitrimmer/ApiTrimmerWindow.java b/src/main/java/com/example/workutils/biz/apitrimmer/ApiTrimmerWindow.java deleted file mode 100644 index 7a2c463..0000000 --- a/src/main/java/com/example/workutils/biz/apitrimmer/ApiTrimmerWindow.java +++ /dev/null @@ -1,1406 +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.support.SystemOpenHelper; -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.concurrent.Task; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.control.*; -import javafx.scene.control.cell.CheckBoxTreeTableCell; -import javafx.scene.layout.*; -import javafx.scene.text.Font; -import javafx.stage.FileChooser; -import javafx.stage.Modality; -import javafx.stage.Stage; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CountDownLatch; - -public class ApiTrimmerWindow { - - private static final String PATTERNS_FILE = "api-trimmer-patterns.txt"; - private static final String DATA_DIR = "data"; - - private final Stage stage; - private final CountDownLatch closeLatch; - private final ApiTrimmerService service; - private final WorkUtilsProperties properties; - - private TextField jarPathField; - private Label autoCheckLabel; - private Label targetLabel; - private Label selectionLabel; - private Label riskLabel; - private Label statusLabel; - private ProgressIndicator progressIndicator; - - private static final int TREE_FONT_SIZE = 12; - private static final int TREE_ROW_HEIGHT = 26; - - private TreeTableView dirTree; - private TreeItem treeRoot; - private Stage candidatePopupStage; - private TreeTableView candidatePopupTree; - private TreeItem candidatePopupRoot; - private final ObservableList dirRows = FXCollections.observableArrayList(); - private Label dirTableLabel; - - private Button previewBtn; - private Button trimBtn; - private ApiTrimmerService.TrimPlan currentPlan; - private ApiTrimmerService.TrimAnalysis analysis; - private Set autoCheckPatterns = new LinkedHashSet<>(); - private Path configFile; - private boolean refreshingSelection = false; - - public ApiTrimmerWindow(ApiTrimmerService service, WorkUtilsProperties properties, - CountDownLatch closeLatch) { - this.service = service; - this.properties = properties; - this.closeLatch = closeLatch; - this.stage = new Stage(); - initConfigFile(); - loadPatterns(); - buildUI(); - } - - public void show() { - stage.show(); - } - - public Parent getContentPane() { - return stage.getScene().getRoot(); - } - - Stage getStage() { - return stage; - } - - private int fsLabel() { - return properties.getFontFieldLabel(); - } - - private int fsButton() { - return properties.getFontButton(); - } - - private int fsBody() { - return properties.getFontBody(); - } - - private int compactFont() { - return Math.max(11, Math.min(13, fsBody() - 2)); - } - - 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)) { - try (InputStream is = getClass().getClassLoader().getResourceAsStream(PATTERNS_FILE)) { - if (is != null) { - Files.copy(is, configFile); - } else { - Files.writeString(configFile, "lombok\nttf\nclass50\n", StandardCharsets.UTF_8); - } - } - } - } catch (IOException e) { - configFile = dataDir.resolve(PATTERNS_FILE); - } - } - - 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", - StandardCharsets.UTF_8); - } - SystemOpenHelper.editFile(configFile); - } catch (IOException e) { - showAlert("无法打开配置文件: " + e.getMessage()); - } - } - - private String patternsDisplay() { - return String.join(", ", autoCheckPatterns); - } - - private void buildUI() { - stage.setTitle("API Trim - 安全精简 API JAR"); - stage.initModality(Modality.NONE); - stage.setResizable(true); - stage.setMinWidth(1280); - stage.setMinHeight(760); - stage.setOnCloseRequest(e -> close()); - - VBox root = new VBox(Theme.S2); - root.setPadding(new Insets(Theme.S3)); - root.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_BODY) + ";"); - - Label title = new Label("API Trim - 安全精简 API JAR"); - title.setFont(Theme.heading(Math.min(properties.getFontSectionTitle(), 22))); - title.setTextFill(Theme.TEXT_PRIMARY); - - VBox inputSection = createInputSection(); - - HBox btnBar = createButtonBar(); - - root.getChildren().addAll(title, inputSection, btnBar); - VBox.setVgrow(inputSection, Priority.ALWAYS); - - Scene scene = new Scene(root, 1440, 900); - stage.setScene(scene); - stage.setMaximized(true); - updateSummary(); - } - - @SuppressWarnings("unchecked") - private VBox createInputSection() { - VBox section = new VBox(Theme.S3); - 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 + ";"); - - Label jarLabel = new Label("API JAR 文件:"); - jarLabel.setFont(Font.font("Microsoft YaHei", fsLabel())); - jarPathField = new TextField(); - jarPathField.setPromptText("选择 Maven 构建出的 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); - - GridPane summary = createSummaryPanel(); - - HBox selectRow = new HBox(Theme.S2); - selectRow.setAlignment(Pos.CENTER_RIGHT); - Button popupCandidatesBtn = StyledButton.small("弹出候选窗口", Font.font("Microsoft YaHei", compactFont())); - popupCandidatesBtn.setOnAction(e -> openCandidatePopup()); - selectRow.getChildren().addAll(popupCandidatesBtn); - - dirTableLabel = new Label("JAR 文件和目录(勾选 = 删除该文件或整目录):"); - dirTableLabel.setFont(Font.font("Microsoft YaHei", Math.max(12, fsLabel() - 2))); - - treeRoot = new TreeItem<>(); - treeRoot.setExpanded(true); - dirTree = createCandidateTree(treeRoot); - - section.getChildren().addAll(jarRow, configRow, summary, dirTableLabel, dirTree, selectRow); - VBox.setVgrow(dirTree, Priority.ALWAYS); - return section; - } - - private TreeTableView createCandidateTree(TreeItem rootItem) { - TreeTableView tree = new TreeTableView<>(rootItem); - tree.setShowRoot(false); - tree.setEditable(true); - tree.setPrefHeight(520); - tree.setFixedCellSize(TREE_ROW_HEIGHT); - tree.setPlaceholder(new Label("选择 JAR 文件后自动生成安全瘦身方案")); - tree.getStyleClass().add(Theme.CSS_TABLE); - tree.setStyle("-fx-font-size: " + TREE_FONT_SIZE + "px;"); - - TreeTableColumn selCol = new TreeTableColumn<>(""); - selCol.setPrefWidth(42); - selCol.setMinWidth(42); - selCol.setMaxWidth(42); - selCol.setSortable(false); - selCol.setEditable(true); - selCol.setCellValueFactory(cdf -> { - DirRow row = cdf.getValue().getValue(); - return row == null ? new SimpleBooleanProperty(false) : row.selectedProperty(); - }); - selCol.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(selCol)); - - TreeTableColumn nameCol = treeNameColumn("名称", 280); - TreeTableColumn fileTypeCol = treeTextColumn("类型", 80, DirRow::fileTypeProperty); - TreeTableColumn sizeCol = treeTextColumn("大小", 72, DirRow::sizeProperty); - sizeCol.setStyle("-fx-alignment: CENTER-RIGHT;"); - TreeTableColumn coverageCol = treeTextColumn("覆盖", 50, DirRow::coverageProperty); - TreeTableColumn categoryCol = treeTextColumn("来源", 96, DirRow::categoryProperty); - TreeTableColumn riskCol = treeTextColumn("风险", 55, DirRow::riskProperty); - - // 所有列可拖拽调整宽度 - for (TreeTableColumn c : List.of(nameCol, fileTypeCol, sizeCol, - coverageCol, categoryCol, riskCol)) { - c.setResizable(true); - } - - tree.getColumns().setAll(selCol, nameCol, fileTypeCol, sizeCol, coverageCol, - categoryCol, riskCol); - tree.setTreeColumn(nameCol); - tree.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); - tree.setRowFactory(table -> new TreeTableRow<>() { - @Override - protected void updateItem(DirRow item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { - setStyle(""); - return; - } - boolean sel = item.isSelected(); - boolean covered = !sel && !item.getCoverage().isEmpty(); - String border = sel ? "#3b82f6" : (covered ? "#93c5fd" : "transparent"); - String bg; - if (sel) { - switch (item.getRiskLevel()) { - case SAFE: bg = "#dbeafe"; break; - case DANGER: - case BLOCKED: bg = "#fecdd3"; break; - default: bg = "#eff6ff"; break; - } - } else if (covered) { - bg = getIndex() % 2 == 0 ? "#f0f9ff" : "#f8faff"; - } else { - switch (item.getRiskLevel()) { - case SAFE: bg = getIndex() % 2 == 0 ? "#ecfdf5" : "#f0fdf4"; break; - case DANGER: - case BLOCKED: bg = getIndex() % 2 == 0 ? "#fef2f2" : "#fff1f2"; break; - default: bg = getIndex() % 2 == 0 ? "#f8fafc" : "white"; - } - } - setStyle("-fx-background-color: " + bg - + "; -fx-border-color: transparent transparent transparent " + border - + "; -fx-border-width: 0 0 0 4;"); - } - }); - return tree; - } - - private TreeTableColumn treeNameColumn(String title, int width) { - TreeTableColumn col = new TreeTableColumn<>(title); - col.setPrefWidth(width); - col.setCellValueFactory(cdf -> { - DirRow row = cdf.getValue().getValue(); - return row == null ? new SimpleStringProperty("") : row.nameProperty(); - }); - col.setCellFactory(column -> new TreeTableCell<>() { - private final Tooltip tooltip = new Tooltip(); - - @Override - protected void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - DirRow row = getTreeTableRow() == null ? null : getTreeTableRow().getItem(); - if (empty || item == null || row == null) { - setText(null); - setGraphic(null); - setTooltip(null); - return; - } - setText(item); - setGraphic(createFileIcon(row)); - tooltip.setText(row.getInfo().getPath()); - setTooltip(tooltip); - } - }); - return col; - } - - private TreeTableColumn treeTextColumn(String title, int width, - java.util.function.Function valueFactory) { - TreeTableColumn col = new TreeTableColumn<>(title); - col.setPrefWidth(width); - col.setCellValueFactory(cdf -> { - DirRow row = cdf.getValue().getValue(); - return row == null ? new SimpleStringProperty("") : valueFactory.apply(row); - }); - col.setCellFactory(column -> new TreeTableCell<>() { - private final Tooltip tooltip = new Tooltip(); - - { - tooltip.setMaxWidth(480); - tooltip.setWrapText(true); - tooltip.setFont(Theme.base(TREE_FONT_SIZE)); - } - - @Override - protected void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null || item.isEmpty()) { - setText(null); - setTooltip(null); - } else { - setText(item); - DirRow row = getTreeTableRow() == null ? null : getTreeTableRow().getItem(); - String tip = "名称".equals(title) && row != null ? row.getInfo().getPath() : item; - tooltip.setText(tip); - setTooltip(tip.length() > 40 ? tooltip : null); - } - } - }); - return col; - } - - private Node createFileIcon(DirRow row) { - if (row.isDirectory()) { - StackPane icon = new StackPane(); - icon.setMinSize(16, 14); - icon.setPrefSize(16, 14); - icon.setMaxSize(16, 14); - - Region tab = new Region(); - tab.setPrefSize(7, 4); - tab.setStyle("-fx-background-color: #fbbf24; -fx-background-radius: 1 1 0 0;"); - StackPane.setAlignment(tab, Pos.TOP_LEFT); - StackPane.setMargin(tab, new Insets(1, 0, 0, 1)); - - Region body = new Region(); - body.setPrefSize(15, 10); - body.setStyle("-fx-background-color: #facc15; -fx-border-color: #d97706; " - + "-fx-border-width: 0.6; -fx-background-radius: 1.5; -fx-border-radius: 1.5;"); - StackPane.setAlignment(body, Pos.BOTTOM_LEFT); - StackPane.setMargin(body, new Insets(4, 0, 0, 0)); - - icon.getChildren().addAll(tab, body); - return icon; - } - - StackPane icon = new StackPane(); - icon.setMinSize(14, 16); - icon.setPrefSize(14, 16); - icon.setMaxSize(14, 16); - Region page = new Region(); - page.setPrefSize(12, 15); - page.setStyle("-fx-background-color: #ffffff; -fx-border-color: #94a3b8; " - + "-fx-border-width: 0.8; -fx-background-radius: 1.5; -fx-border-radius: 1.5;"); - Region corner = new Region(); - corner.setPrefSize(4, 4); - corner.setStyle("-fx-background-color: #e2e8f0; -fx-border-color: #94a3b8; " - + "-fx-border-width: 0 0 0.6 0.6;"); - StackPane.setAlignment(corner, Pos.TOP_RIGHT); - StackPane.setMargin(corner, new Insets(1, 1, 0, 0)); - icon.getChildren().addAll(page, corner); - return icon; - } - - private GridPane createSummaryPanel() { - GridPane grid = new GridPane(); - grid.setHgap(Theme.S4); - grid.setVgap(Theme.S2); - grid.setPadding(new Insets(Theme.S2, Theme.S3, Theme.S2, Theme.S3)); - grid.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_CARD) - + "; -fx-background-radius: " + Theme.RADIUS_SM + ";"); - - targetLabel = summaryValue(); - selectionLabel = summaryValue(); - riskLabel = summaryValue(); - statusLabel = summaryValue(); - progressIndicator = new ProgressIndicator(); - progressIndicator.setPrefSize(18, 18); - progressIndicator.setVisible(false); - progressIndicator.setManaged(false); - - grid.add(summaryName("目标体积"), 0, 0); - grid.add(targetLabel, 1, 0); - grid.add(summaryName("已选删除"), 2, 0); - grid.add(selectionLabel, 3, 0); - grid.add(summaryName("风险"), 0, 1); - grid.add(riskLabel, 1, 1); - grid.add(summaryName("状态"), 2, 1); - HBox statusBox = new HBox(Theme.S1, progressIndicator, statusLabel); - statusBox.setAlignment(Pos.CENTER_LEFT); - grid.add(statusBox, 3, 1); - return grid; - } - - private Label summaryName(String text) { - Label label = new Label(text + ":"); - label.setFont(Theme.bold(fsBody() - 1)); - label.setTextFill(Theme.TEXT_SECONDARY); - return label; - } - - private Label summaryValue() { - Label label = new Label("-"); - label.setFont(Theme.base(fsBody() - 1)); - label.setTextFill(Theme.TEXT_PRIMARY); - return label; - } - - private void showResultPopup(String title, List lines) { - Platform.runLater(() -> { - VBox content = new VBox(4); - content.setPadding(new Insets(Theme.S3)); - for (ResultLine line : lines) { - Label label = new Label(line.text); - label.setWrapText(true); - label.setFont(line.bold ? Theme.bold(fsBody()) : Theme.base(fsBody() - 1)); - label.setTextFill(line.color); - content.getChildren().add(label); - } - - ScrollPane scroll = new ScrollPane(content); - scroll.setFitToWidth(true); - scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); - - Stage popupStage = new Stage(); - popupStage.setTitle(title); - popupStage.initOwner(stage); - popupStage.initModality(Modality.NONE); - popupStage.setMinWidth(500); - popupStage.setMinHeight(300); - VBox root = new VBox(scroll); - root.setPadding(new Insets(Theme.S2)); - root.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_BODY) + ";"); - VBox.setVgrow(scroll, Priority.ALWAYS); - popupStage.setScene(new Scene(root, 600, 440)); - popupStage.show(); - }); - } - - private record ResultLine(String text, javafx.scene.paint.Color color, boolean bold) {} - - private HBox createButtonBar() { - HBox bar = new HBox(Theme.S3); - bar.setAlignment(Pos.CENTER_RIGHT); - bar.setPadding(new Insets(Theme.S2, 0, 0, 0)); - - previewBtn = StyledButton.secondary("分析预览", Font.font("Microsoft YaHei", fsButton())); - previewBtn.setPrefWidth(110); - previewBtn.setOnAction(e -> onPreview()); - - trimBtn = StyledButton.primary("执行精简", Font.font("Microsoft YaHei", fsButton())); - trimBtn.setPrefWidth(110); - trimBtn.setOnAction(e -> onTrim()); - - bar.getChildren().addAll(previewBtn, trimBtn); - return bar; - } - - 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.skipMavenMeta = false; - config.skipSpringMeta = false; - config.excludedDirs.addAll(selectedPrefixes()); - return config; - } - - private void onBrowseJar() { - FileChooser chooser = new FileChooser(); - chooser.setTitle("选择 API JAR 文件"); - chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("JAR 文件", "*.jar")); - java.io.File file = chooser.showOpenDialog(stage); - if (file != null) { - jarPathField.setText(file.getAbsolutePath()); - refreshDirList(Path.of(file.getAbsolutePath())); - } - } - - private void onRefreshDirs() { - Path jarPath = currentJarPath(); - if (jarPath == null) { - return; - } - loadPatterns(); - autoCheckLabel.setText("自动规则: " + patternsDisplay()); - refreshDirList(jarPath); - } - - private void refreshDirList(Path jarPath) { - Task task = new Task<>() { - @Override - protected ApiTrimmerService.TrimPlan call() throws Exception { - return service.buildTrimPlan(jarPath, Set.of()); - } - }; - task.setOnRunning(e -> setBusy(true, "正在扫描 JAR...")); - task.setOnSucceeded(e -> { - currentPlan = task.getValue(); - analysis = null; - fillRows(currentPlan); - showPlanWarnings(currentPlan); - setBusy(false, "扫描完成"); - updateSummary(); - }); - task.setOnFailed(e -> { - setBusy(false, "扫描失败"); - showAlert("目录扫描失败: " + task.getException().getMessage()); - }); - new Thread(task, "api-trim-scan").start(); - } - - private void fillRows(ApiTrimmerService.TrimPlan plan) { - dirRows.clear(); - refreshingSelection = true; - try { - for (ApiTrimmerService.TrimCandidate candidate : plan.getCandidates()) { - DirRow row = new DirRow(candidate); - row.selectedProperty().addListener((obs, oldVal, newVal) -> { - if (refreshingSelection) { - return; - } - if (newVal && row.getRiskLevel() == ApiTrimmerService.RiskLevel.BLOCKED) { - row.setSelected(false); - showAlert("该目录被标记为禁止删除: " + row.getName()); - return; - } - analysis = null; - updateCoverageMarkers(); - updateCandidateFilter(); - updateSummary(); - }); - dirRows.add(row); - } - clearCoveredSelections(); - } finally { - refreshingSelection = false; - } - updateCoverageMarkers(); - updateCandidateFilter(); - dirTableLabel.setText("JAR 文件和目录(安全项已默认勾选,文件/谨慎/高风险项需人工确认):"); - } - - private void onPreview() { - Path jarPath = currentJarPath(); - if (jarPath == null || !ensureHasRows()) { - return; - } - ApiTrimmerService.TrimConfig config = getTrimConfig(); - Task task = new Task<>() { - @Override - protected PreviewResult call() throws Exception { - ApiTrimmerService.TrimAnalysis a = service.analyze(jarPath, List.of(), config); - long estimatedSize = service.estimateOutputSize(jarPath, a, config); - return new PreviewResult(a, estimatedSize); - } - }; - task.setOnRunning(e -> setBusy(true, "正在分析接口可达类...")); - task.setOnSucceeded(e -> { - PreviewResult result = task.getValue(); - analysis = result.analysis; - showPreviewResult(result); - setBusy(false, "分析完成"); - }); - task.setOnFailed(e -> { - setBusy(false, "分析失败"); - showAlert("分析失败: " + task.getException().getMessage()); - }); - new Thread(task, "api-trim-preview").start(); - } - - private void onTrim() { - Path jarPath = currentJarPath(); - if (jarPath == null || !ensureHasRows() || !confirmRiskSelection()) { - 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")); - java.io.File outputFile = chooser.showSaveDialog(stage); - if (outputFile == null) { - return; - } - Path outputPath = outputFile.toPath(); - ApiTrimmerService.TrimConfig config = getTrimConfig(); - - Task task = new Task<>() { - @Override - protected ExecutionResult call() throws Exception { - ApiTrimmerService.TrimAnalysis a = service.analyze(jarPath, List.of(), config); - if (!a.getExcludedEntryPoints().isEmpty()) { - throw new IllegalStateException("选择的删除项覆盖了 RPC/API 入口类: " - + String.join(", ", a.getExcludedEntryPoints())); - } - service.execute(jarPath, a, outputPath, config); - ApiTrimmerService.VerificationResult verification = service.verifyOutput(outputPath, a); - long newSize = Files.size(outputPath); - return new ExecutionResult(a, outputPath, newSize, verification); - } - }; - task.setOnRunning(e -> setBusy(true, "正在精简并验证...")); - task.setOnSucceeded(e -> { - ExecutionResult result = task.getValue(); - analysis = result.analysis; - showExecutionResult(result); - setBusy(false, result.verification.isOk() ? "精简完成,验证通过" : "精简完成,验证未通过"); - }); - task.setOnFailed(e -> { - setBusy(false, "精简失败"); - Throwable ex = task.getException(); - showAlert("精简失败: " + (ex.getMessage() != null ? ex.getMessage() : ex.getClass().getSimpleName())); - }); - new Thread(task, "api-trim-execute").start(); - } - - private boolean confirmRiskSelection() { - Set effectiveSelected = selectedPrefixes(); - List blocked = dirRows.stream() - .filter(DirRow::isSelected) - .filter(row -> effectiveSelected.contains(row.getInfo().getPath())) - .filter(row -> row.getRiskLevel() == ApiTrimmerService.RiskLevel.BLOCKED) - .toList(); - if (!blocked.isEmpty()) { - showAlert("存在禁止删除项,请取消后再执行: " + blocked.get(0).getName()); - return false; - } - List dangerous = dirRows.stream() - .filter(DirRow::isSelected) - .filter(row -> effectiveSelected.contains(row.getInfo().getPath())) - .filter(row -> row.getRiskLevel() == ApiTrimmerService.RiskLevel.DANGER) - .toList(); - if (dangerous.isEmpty()) { - return true; - } - StringBuilder msg = new StringBuilder("你选择了高风险目录,可能影响 Dubbo 接口或序列化类型。\n\n"); - for (int i = 0; i < Math.min(5, dangerous.size()); i++) { - DirRow row = dangerous.get(i); - msg.append("- ").append(row.getName()).append(": ").append(row.getReason()).append("\n"); - } - msg.append("\n仍要继续吗?"); - Alert alert = new Alert(Alert.AlertType.CONFIRMATION); - alert.setTitle("确认高风险删除"); - alert.setHeaderText(null); - alert.setContentText(msg.toString()); - alert.initOwner(stage); - return alert.showAndWait().filter(ButtonType.OK::equals).isPresent(); - } - - private void updateSummary() { - if (targetLabel == null) { - return; - } - Path jarPath = currentJarPath(); - if (jarPath == null || dirRows.isEmpty()) { - targetLabel.setText("目标 < " + formatSize(ApiTrimmerService.TARGET_SIZE_BYTES)); - selectionLabel.setText("未选择 JAR"); - riskLabel.setText("-"); - statusLabel.setText("等待选择 API JAR"); - if (previewBtn != null) { - previewBtn.setDisable(true); - trimBtn.setDisable(true); - } - return; - } - updateCoverageMarkers(); - Set selected = selectedPrefixes(); - try { - ApiTrimmerService.SelectionSummary summary = service.summarizeSelection(jarPath, selected); - targetLabel.setText("目标 < " + formatSize(ApiTrimmerService.TARGET_SIZE_BYTES) - + ",估算 " + formatSize(summary.getEstimatedOutputBytes()) - + (summary.isTargetReached() ? "(已达标)" : "(未达标)")); - selectionLabel.setText(selected.size() + " 项 / " - + formatSize(summary.getExcludedBytes()) + " / " - + summary.getExcludedFiles() + " 文件 / " - + summary.getExcludedClasses() + " 类"); - } catch (IOException e) { - selectionLabel.setText("统计失败: " + e.getMessage()); - } - long safe = dirRows.stream().filter(DirRow::isSelected) - .filter(row -> row.getRiskLevel() == ApiTrimmerService.RiskLevel.SAFE).count(); - long caution = dirRows.stream().filter(DirRow::isSelected) - .filter(row -> row.getRiskLevel() == ApiTrimmerService.RiskLevel.CAUTION).count(); - long danger = dirRows.stream().filter(DirRow::isSelected) - .filter(row -> row.getRiskLevel() == ApiTrimmerService.RiskLevel.DANGER).count(); - riskLabel.setText("安全 " + safe + " / 谨慎 " + caution + " / 高风险 " + danger); - statusLabel.setText("已生成候选方案"); - previewBtn.setDisable(selected.isEmpty()); - trimBtn.setDisable(selected.isEmpty()); - } - - private void showPlanWarnings(ApiTrimmerService.TrimPlan plan) { - List lines = new ArrayList<>(); - lines.add(new ResultLine("扫描完成", Theme.TEXT_PRIMARY, true)); - lines.add(new ResultLine("原始大小: " + formatSize(plan.getOriginalSizeBytes()) - + ",默认安全方案估算: " + formatSize(plan.getEstimatedOutputBytes()) - + ",目标: < " + formatSize(plan.getTargetSizeBytes()), Theme.TEXT_PRIMARY, false)); - lines.add(new ResultLine("绿色安全项会说明为什么可删以及为什么不影响 Dubbo/RPC。" - + "如果默认方案仍超过 4M,请先点「分析预览」,再逐项确认谨慎项。", Theme.TEXT_SECONDARY, false)); - for (String warning : plan.getWarnings()) { - lines.add(new ResultLine("[提示] " + warning, Theme.WARNING, false)); - } - showResultPopup("扫描完成", lines); - } - - private void showPreviewResult(PreviewResult result) { - List lines = new ArrayList<>(); - lines.add(new ResultLine("分析预览", Theme.TEXT_PRIMARY, true)); - lines.add(new ResultLine("原始类数: " + result.analysis.getTotalClassCount() - + ",保留类数: " + result.analysis.getRetainedClassCount() - + ",移除类数: " + result.analysis.getRemovedClassCount(), Theme.TEXT_PRIMARY, false)); - lines.add(new ResultLine("识别入口类: " + result.analysis.getEntryPoints().size() - + ",估算输出: " + formatSize(result.estimatedSize) - + (result.estimatedSize <= ApiTrimmerService.TARGET_SIZE_BYTES ? "(预计达标)" : "(预计未达标)"), - result.estimatedSize <= ApiTrimmerService.TARGET_SIZE_BYTES ? Theme.SUCCESS : Theme.WARNING, false)); - if (!result.analysis.getExcludedEntryPoints().isEmpty()) { - lines.add(new ResultLine("阻断: 删除项覆盖了入口类 " + String.join(", ", result.analysis.getExcludedEntryPoints()), - Theme.DANGER, false)); - } - appendEntryPoints(lines, result.analysis); - showResultPopup("分析预览", lines); - } - - private void showExecutionResult(ExecutionResult result) { - List lines = new ArrayList<>(); - double reduction = 100.0 * (1.0 - (double) result.outputSize / result.analysis.getOriginalSizeBytes()); - lines.add(new ResultLine("精简完成", Theme.TEXT_PRIMARY, true)); - lines.add(new ResultLine("输出: " + result.outputPath.toAbsolutePath(), Theme.TEXT_PRIMARY, false)); - lines.add(new ResultLine(String.format("原始: %s,输出: %s,缩减: %.1f%%,目标: %s", - formatSize(result.analysis.getOriginalSizeBytes()), - formatSize(result.outputSize), - reduction, - result.outputSize <= ApiTrimmerService.TARGET_SIZE_BYTES ? "已小于 4M" : "仍大于 4M"), - result.outputSize <= ApiTrimmerService.TARGET_SIZE_BYTES ? Theme.SUCCESS : Theme.WARNING, false)); - ApiTrimmerService.VerificationResult verification = result.verification; - lines.add(new ResultLine("验证: " + (verification.isOk() ? "通过" : "未通过") - + ",入口类 " + verification.getEntryPointsChecked() - + ",签名可达类 " + verification.getRetainedClassesChecked(), - verification.isOk() ? Theme.SUCCESS : Theme.DANGER, false)); - for (String error : verification.getErrors()) { - lines.add(new ResultLine("[错误] " + error, Theme.DANGER, false)); - } - for (String warning : verification.getWarnings()) { - lines.add(new ResultLine("[提示] " + warning, Theme.WARNING, false)); - } - appendEntryPoints(lines, result.analysis); - showResultPopup("精简完成", lines); - } - - private void appendEntryPoints(List lines, ApiTrimmerService.TrimAnalysis result) { - if (result.getEntryPoints().isEmpty()) { - lines.add(new ResultLine("未识别到接口或 Dubbo 入口类,请确认该 JAR 是否为 API 模块。", Theme.WARNING, false)); - return; - } - lines.add(new ResultLine("入口类示例:", Theme.TEXT_PRIMARY, true)); - int limit = Math.min(12, result.getEntryPoints().size()); - for (int i = 0; i < limit; i++) { - lines.add(new ResultLine(" " + result.getEntryPoints().get(i), Theme.TEXT_SECONDARY, false)); - } - if (result.getEntryPoints().size() > limit) { - lines.add(new ResultLine(" ...还有 " + (result.getEntryPoints().size() - limit) + " 个", Theme.TEXT_SECONDARY, false)); - } - } - - private void selectRecommended() { - refreshingSelection = true; - try { - for (DirRow row : dirRows) { - row.setSelected(row.isSelectedByDefault()); - } - clearCoveredSelections(); - } finally { - refreshingSelection = false; - } - analysis = null; - updateCoverageMarkers(); - updateCandidateFilter(); - updateSummary(); - } - - private void selectSafeOnly() { - refreshingSelection = true; - try { - for (DirRow row : dirRows) { - row.setSelected(row.getRiskLevel() == ApiTrimmerService.RiskLevel.SAFE); - } - clearCoveredSelections(); - } finally { - refreshingSelection = false; - } - analysis = null; - updateCoverageMarkers(); - updateCandidateFilter(); - updateSummary(); - } - - private void clearSelection() { - refreshingSelection = true; - try { - for (DirRow row : dirRows) { - row.setSelected(false); - } - } finally { - refreshingSelection = false; - } - analysis = null; - updateCoverageMarkers(); - updateCandidateFilter(); - updateSummary(); - } - - private void clearCoveredSelections() { - Set effective = selectedPrefixes(); - for (DirRow row : dirRows) { - if (row.isSelected() && !effective.contains(row.getInfo().getPath())) { - row.setSelected(false); - } - } - } - - private void updateCoverageMarkers() { - Set selected = rawSelectedPrefixes(); - for (DirRow row : dirRows) { - String ancestor = findSelectedAncestor(row.getInfo().getPath(), selected); - if (ancestor == null) { - row.setCoverage(""); - } else if (row.isSelected()) { - row.setCoverage("父级覆盖"); - } else { - row.setCoverage("父级已选"); - } - } - } - - private void updateCandidateFilter() { - rebuildCandidateTree(); - } - - private void rebuildCandidateTree() { - if (treeRoot != null) { - rebuildCandidateTree(treeRoot); - } - if (candidatePopupRoot != null) { - rebuildCandidateTree(candidatePopupRoot); - } - } - - private void rebuildCandidateTree(TreeItem rootItem) { - Set expandedPaths = collectExpandedPaths(rootItem); - boolean firstBuild = rootItem.getChildren().isEmpty(); - - Map byPath = new LinkedHashMap<>(); - for (DirRow row : dirRows) { - byPath.put(row.getInfo().getPath(), row); - } - - Set included = new LinkedHashSet<>(); - for (DirRow row : dirRows) { - if (matchesCurrentFilter(row)) { - included.add(row); - addExistingAncestors(row.getInfo().getPath(), byPath, included); - } - } - - rootItem.getChildren().clear(); - Map> itemByPath = new LinkedHashMap<>(); - included.stream() - .sorted(Comparator - .comparing((DirRow row) -> !row.isDirectory()) - .thenComparing(row -> row.getInfo().getPath())) - .forEach(row -> { - TreeItem item = itemByPath.computeIfAbsent(row.getInfo().getPath(), - key -> new TreeItem<>(row)); - TreeItem parent = rootItem; - String parentPath = parentPrefix(row.getInfo().getPath()); - while (parentPath != null) { - TreeItem candidateParent = itemByPath.get(parentPath); - if (candidateParent != null) { - parent = candidateParent; - break; - } - parentPath = parentPrefix(parentPath); - } - parent.getChildren().add(item); - }); - rootItem.setExpanded(true); - if (firstBuild) { - expandTreeToDepth(rootItem, 2); - } else { - restoreExpandedPaths(rootItem, expandedPaths); - } - } - - private boolean matchesCurrentFilter(DirRow row) { - return true; // 主窗口不筛选,全部显示 - } - - private void addExistingAncestors(String path, Map byPath, Set included) { - String parent = parentPrefix(path); - while (parent != null) { - DirRow row = byPath.get(parent); - if (row != null) { - included.add(row); - } - parent = parentPrefix(parent); - } - } - - private static String parentPrefix(String path) { - if (path == null || path.isEmpty()) { - return null; - } - String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path; - int slash = normalized.lastIndexOf('/'); - if (slash < 0) { - return null; - } - return normalized.substring(0, slash + 1); - } - - private void openCandidatePopup() { - if (candidatePopupStage != null && candidatePopupStage.isShowing()) { - candidatePopupStage.setMaximized(true); - candidatePopupStage.toFront(); - return; - } - - candidatePopupRoot = new TreeItem<>(); - candidatePopupRoot.setExpanded(true); - candidatePopupTree = createCandidateTree(candidatePopupRoot); - candidatePopupTree.setPrefHeight(760); - candidatePopupTree.setMinWidth(680); - - Label title = new Label("候选删除项"); - title.setFont(Theme.heading(18)); - title.setTextFill(Theme.TEXT_PRIMARY); - - // ---- 弹窗工具栏:所有操作按钮集中在此 ---- - Button popSelectRecommended = StyledButton.small("选择安全推荐", Font.font("Microsoft YaHei", compactFont())); - popSelectRecommended.setOnAction(e -> { - selectRecommended(); - rebuildCandidateTree(candidatePopupRoot); - }); - Button popClear = StyledButton.small("清空选择", Font.font("Microsoft YaHei", compactFont())); - popClear.setOnAction(e -> { - clearSelection(); - rebuildCandidateTree(candidatePopupRoot); - }); - Button popSafeOnly = StyledButton.small("仅保留安全项", Font.font("Microsoft YaHei", compactFont())); - popSafeOnly.setOnAction(e -> { - selectSafeOnly(); - rebuildCandidateTree(candidatePopupRoot); - }); - Button expandBtn = StyledButton.small("展开全部", Font.font("Microsoft YaHei", compactFont())); - expandBtn.setOnAction(e -> setTreeExpanded(candidatePopupRoot, true)); - Button collapseBtn = StyledButton.small("折叠全部", Font.font("Microsoft YaHei", compactFont())); - collapseBtn.setOnAction(e -> { - setTreeExpanded(candidatePopupRoot, false); - candidatePopupRoot.setExpanded(true); - }); - HBox toolbar = new HBox(Theme.S2, - popSelectRecommended, popClear, popSafeOnly, expandBtn, collapseBtn); - toolbar.setAlignment(Pos.CENTER_LEFT); - - HBox titleBar = new HBox(Theme.S2, title); - titleBar.setAlignment(Pos.CENTER_LEFT); - - ScrollPane scrollPane = new ScrollPane(candidatePopupTree); - scrollPane.setFitToWidth(true); - scrollPane.setFitToHeight(true); - scrollPane.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); - VBox.setVgrow(scrollPane, Priority.ALWAYS); - - VBox root = new VBox(Theme.S2, titleBar, toolbar, scrollPane); - root.setPadding(new Insets(Theme.S3)); - root.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_BODY) + ";"); - - candidatePopupStage = new Stage(); - candidatePopupStage.setTitle("API Trim - 候选删除项"); - candidatePopupStage.initOwner(stage); - candidatePopupStage.initModality(Modality.NONE); - candidatePopupStage.setResizable(true); - candidatePopupStage.setMinWidth(720); - candidatePopupStage.setMinHeight(720); - candidatePopupStage.setScene(new Scene(root, 1380, 840)); - candidatePopupStage.setOnCloseRequest(e -> clearCandidatePopupReferences()); - - rebuildCandidateTree(candidatePopupRoot); - candidatePopupStage.show(); - candidatePopupStage.setMaximized(true); - } - - private void clearCandidatePopupReferences() { - candidatePopupStage = null; - candidatePopupTree = null; - candidatePopupRoot = null; - } - - private void setTreeExpanded(TreeItem item, boolean expanded) { - item.setExpanded(expanded); - for (TreeItem child : item.getChildren()) { - setTreeExpanded(child, expanded); - } - } - - private void expandTreeToDepth(TreeItem item, int depth) { - item.setExpanded(depth >= 0); - if (depth < 0) { - return; - } - for (TreeItem child : item.getChildren()) { - expandTreeToDepth(child, depth - 1); - } - } - - private Set collectExpandedPaths(TreeItem item) { - Set expanded = new LinkedHashSet<>(); - collectExpandedPaths(item, expanded); - return expanded; - } - - private void collectExpandedPaths(TreeItem item, Set expanded) { - DirRow row = item.getValue(); - if (row != null && item.isExpanded()) { - expanded.add(row.getInfo().getPath()); - } - for (TreeItem child : item.getChildren()) { - collectExpandedPaths(child, expanded); - } - } - - private void restoreExpandedPaths(TreeItem item, Set expandedPaths) { - DirRow row = item.getValue(); - item.setExpanded(row == null || expandedPaths.contains(row.getInfo().getPath())); - for (TreeItem child : item.getChildren()) { - restoreExpandedPaths(child, expandedPaths); - } - } - - private Set rawSelectedPrefixes() { - Set selected = new LinkedHashSet<>(); - for (DirRow row : dirRows) { - if (row.isSelected()) { - selected.add(row.getInfo().getPath()); - } - } - return selected; - } - - private Set selectedPrefixes() { - List raw = new ArrayList<>(rawSelectedPrefixes()); - raw.sort(Comparator.comparingInt(String::length).thenComparing(String::compareTo)); - Set selected = new LinkedHashSet<>(); - for (String prefix : raw) { - if (findSelectedAncestor(prefix, selected) == null) { - selected.add(prefix); - } - } - return selected; - } - - private String findSelectedAncestor(String path, Set selected) { - String matched = null; - for (String prefix : selected) { - if (!prefix.equals(path) && covers(prefix, path)) { - matched = prefix; - } - } - return matched; - } - - private static boolean covers(String parentPrefix, String childPath) { - return parentPrefix.endsWith("/") && childPath.startsWith(parentPrefix) - && childPath.length() > parentPrefix.length(); - } - - private boolean ensureHasRows() { - if (dirRows.isEmpty()) { - showAlert("请先选择并扫描 API JAR 文件"); - return false; - } - return true; - } - - private Path currentJarPath() { - if (jarPathField == null) { - return null; - } - String path = jarPathField.getText(); - if (path == null || path.trim().isEmpty()) { - return null; - } - Path jarPath = Path.of(path.trim()); - if (!Files.exists(jarPath)) { - return null; - } - return jarPath; - } - - private void setBusy(boolean busy, String message) { - progressIndicator.setVisible(busy); - progressIndicator.setManaged(busy); - previewBtn.setDisable(busy || selectedPrefixes().isEmpty()); - trimBtn.setDisable(busy || selectedPrefixes().isEmpty()); - statusLabel.setText(message); - } - - 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 close() { - closeLatch.countDown(); - stage.close(); - } - - 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 static class DirRow { - private final ApiTrimmerService.TrimCandidate candidate; - private final BooleanProperty selected; - private final StringProperty name; - private final StringProperty depth; - private final StringProperty fileType; - private final StringProperty risk; - private final StringProperty category; - private final StringProperty coverage; - private final StringProperty size; - private final StringProperty count; - private final StringProperty reason; - - DirRow(ApiTrimmerService.TrimCandidate candidate) { - this.candidate = candidate; - this.selected = new SimpleBooleanProperty(candidate.isSelectedByDefault()); - this.name = new SimpleStringProperty(treeName(candidate.getInfo())); - this.depth = new SimpleStringProperty(String.valueOf(candidate.getInfo().getDepth())); - this.fileType = new SimpleStringProperty(detectFileType(candidate.getInfo().getPath())); - this.risk = new SimpleStringProperty(candidate.getRiskLevel().getLabel()); - this.category = new SimpleStringProperty(candidate.getCategory()); - this.coverage = new SimpleStringProperty(""); - this.size = new SimpleStringProperty(formatSize(candidate.getInfo().getSizeBytes())); - this.count = new SimpleStringProperty(candidate.getInfo().getFileCount() - + " / " + candidate.getInfo().getClassCount()); - this.reason = new SimpleStringProperty(candidate.getDescription()); - } - - public ApiTrimmerService.DirInfo getInfo() { - return candidate.getInfo(); - } - - public ApiTrimmerService.RiskLevel getRiskLevel() { - return candidate.getRiskLevel(); - } - - public boolean isSelectedByDefault() { - return candidate.isSelectedByDefault(); - } - - public BooleanProperty selectedProperty() { - return selected; - } - - public boolean isSelected() { - return selected.get(); - } - - public void setSelected(boolean selected) { - this.selected.set(selected); - } - - public String getName() { - return name.get(); - } - - public String getReason() { - return reason.get(); - } - - public boolean isDirectory() { - return candidate.getInfo().getPath().endsWith("/"); - } - - public String getCoverage() { - return coverage.get(); - } - - public void setCoverage(String value) { - coverage.set(value == null ? "" : value); - } - - public boolean matches(String lowerKeyword) { - return candidate.getInfo().getPath().toLowerCase(Locale.ROOT).contains(lowerKeyword) - || fileType.get().toLowerCase(Locale.ROOT).contains(lowerKeyword) - || risk.get().toLowerCase(Locale.ROOT).contains(lowerKeyword) - || category.get().toLowerCase(Locale.ROOT).contains(lowerKeyword) - || coverage.get().toLowerCase(Locale.ROOT).contains(lowerKeyword) - || reason.get().toLowerCase(Locale.ROOT).contains(lowerKeyword); - } - - public StringProperty nameProperty() { - return name; - } - - public StringProperty depthProperty() { - return depth; - } - - public StringProperty fileTypeProperty() { - return fileType; - } - - public StringProperty riskProperty() { - return risk; - } - - public StringProperty categoryProperty() { - return category; - } - - public StringProperty coverageProperty() { - return coverage; - } - - public StringProperty sizeProperty() { - return size; - } - - public StringProperty countProperty() { - return count; - } - - public StringProperty reasonProperty() { - return reason; - } - - - private static String treeName(ApiTrimmerService.DirInfo info) { - return leafName(info.getPath()); - } - - private static String leafName(String path) { - String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path; - int slash = normalized.lastIndexOf('/'); - return slash >= 0 ? normalized.substring(slash + 1) : normalized; - } - - private static String detectFileType(String path) { - if (path.endsWith("/")) { - return "文件夹"; - } - String name = leafName(path); - int dot = name.lastIndexOf('.'); - if (dot < 0 || dot == name.length() - 1) { - return "文件"; - } - String ext = name.substring(dot + 1).toLowerCase(Locale.ROOT); - switch (ext) { - case "class": - return "CLASS 文件"; - case "properties": - return "Properties 文件"; - case "xml": - return "XML 文件"; - case "json": - return "JSON 文件"; - case "yml": - case "yaml": - return "YAML 文件"; - case "txt": - return "Text 文件"; - case "md": - return "Markdown 文件"; - case "mf": - return "MANIFEST 文件"; - default: - return ext.toUpperCase(Locale.ROOT) + " 文件"; - } - } - } - - private static class PreviewResult { - final ApiTrimmerService.TrimAnalysis analysis; - final long estimatedSize; - - PreviewResult(ApiTrimmerService.TrimAnalysis analysis, long estimatedSize) { - this.analysis = analysis; - this.estimatedSize = estimatedSize; - } - } - - private static class ExecutionResult { - final ApiTrimmerService.TrimAnalysis analysis; - final Path outputPath; - final long outputSize; - final ApiTrimmerService.VerificationResult verification; - - ExecutionResult(ApiTrimmerService.TrimAnalysis analysis, Path outputPath, long outputSize, - ApiTrimmerService.VerificationResult verification) { - this.analysis = analysis; - this.outputPath = outputPath; - this.outputSize = outputSize; - this.verification = verification; - } - } -} diff --git a/src/main/java/com/example/workutils/biz/apitrimmer/service/ApiTrimmerService.java b/src/main/java/com/example/workutils/biz/apitrimmer/service/ApiTrimmerService.java deleted file mode 100644 index 88179a2..0000000 --- a/src/main/java/com/example/workutils/biz/apitrimmer/service/ApiTrimmerService.java +++ /dev/null @@ -1,1179 +0,0 @@ -package com.example.workutils.biz.apitrimmer.service; - -import lombok.extern.slf4j.Slf4j; -import org.objectweb.asm.*; -import org.springframework.stereotype.Service; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.JarInputStream; -import java.util.jar.JarOutputStream; - -@Slf4j -@Service -public class ApiTrimmerService { - - public static final long TARGET_SIZE_BYTES = 4L * 1024 * 1024; - public enum RiskLevel { - SAFE("安全", 0), - CAUTION("谨慎", 1), - DANGER("高风险", 2), - BLOCKED("禁止", 3); - - private final String label; - private final int order; - - RiskLevel(String label, int order) { - this.label = label; - this.order = order; - } - - public String getLabel() { - return label; - } - - int getOrder() { - return order; - } - } - - 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 skipMavenMeta = true; - public boolean skipSpringMeta = true; - public Set excludedDirs = new LinkedHashSet<>(); - } - - public static class DirInfo { - private final String path; - private final String name; - private final int depth; - private final int classCount; - private final int fileCount; - private final long sizeBytes; - - public DirInfo(String path, String name, int depth, int classCount, int fileCount, long sizeBytes) { - this.path = path; - this.name = name; - this.depth = depth; - this.classCount = classCount; - this.fileCount = fileCount; - this.sizeBytes = sizeBytes; - } - - public String getPath() { - return path; - } - - public String getName() { - return name; - } - - public int getDepth() { - return depth; - } - - public int getClassCount() { - return classCount; - } - - public int getFileCount() { - return fileCount; - } - - public long getSizeBytes() { - return sizeBytes; - } - } - - public static class TrimCandidate { - private final DirInfo info; - private final boolean recommended; - private final boolean selectedByDefault; - private final RiskLevel riskLevel; - private final String category; - private final String description; - - public TrimCandidate(DirInfo info, boolean recommended, boolean selectedByDefault, - RiskLevel riskLevel, String category, String description) { - this.info = info; - this.recommended = recommended; - this.selectedByDefault = selectedByDefault; - this.riskLevel = riskLevel; - this.category = category; - this.description = description; - } - - public DirInfo getInfo() { - return info; - } - - public boolean isRecommended() { - return recommended; - } - - public boolean isSelectedByDefault() { - return selectedByDefault; - } - - public RiskLevel getRiskLevel() { - return riskLevel; - } - - public String getCategory() { - return category; - } - - public String getDescription() { - return description; - } - } - - public static class TrimPlan { - private final long originalSizeBytes; - private final long targetSizeBytes; - private final List candidates; - private final Set defaultExcludedDirs; - private final long defaultExcludedBytes; - private final int defaultExcludedFiles; - private final int defaultExcludedClasses; - private final long estimatedOutputBytes; - private final List warnings; - - public TrimPlan(long originalSizeBytes, long targetSizeBytes, List candidates, - Set defaultExcludedDirs, long defaultExcludedBytes, - int defaultExcludedFiles, int defaultExcludedClasses, - long estimatedOutputBytes, List warnings) { - this.originalSizeBytes = originalSizeBytes; - this.targetSizeBytes = targetSizeBytes; - this.candidates = Collections.unmodifiableList(candidates); - this.defaultExcludedDirs = Collections.unmodifiableSet(defaultExcludedDirs); - this.defaultExcludedBytes = defaultExcludedBytes; - this.defaultExcludedFiles = defaultExcludedFiles; - this.defaultExcludedClasses = defaultExcludedClasses; - this.estimatedOutputBytes = estimatedOutputBytes; - this.warnings = Collections.unmodifiableList(warnings); - } - - public long getOriginalSizeBytes() { - return originalSizeBytes; - } - - public long getTargetSizeBytes() { - return targetSizeBytes; - } - - public List getCandidates() { - return candidates; - } - - public Set getDefaultExcludedDirs() { - return defaultExcludedDirs; - } - - public long getDefaultExcludedBytes() { - return defaultExcludedBytes; - } - - public int getDefaultExcludedFiles() { - return defaultExcludedFiles; - } - - public int getDefaultExcludedClasses() { - return defaultExcludedClasses; - } - - public long getEstimatedOutputBytes() { - return estimatedOutputBytes; - } - - public boolean isTargetReached() { - return estimatedOutputBytes <= targetSizeBytes; - } - - public List getWarnings() { - return warnings; - } - } - - public static class SelectionSummary { - private final long excludedBytes; - private final int excludedFiles; - private final int excludedClasses; - private final long estimatedOutputBytes; - - public SelectionSummary(long excludedBytes, int excludedFiles, int excludedClasses, long estimatedOutputBytes) { - this.excludedBytes = excludedBytes; - this.excludedFiles = excludedFiles; - this.excludedClasses = excludedClasses; - this.estimatedOutputBytes = estimatedOutputBytes; - } - - public long getExcludedBytes() { - return excludedBytes; - } - - public int getExcludedFiles() { - return excludedFiles; - } - - public int getExcludedClasses() { - return excludedClasses; - } - - public long getEstimatedOutputBytes() { - return estimatedOutputBytes; - } - - public boolean isTargetReached() { - return estimatedOutputBytes <= TARGET_SIZE_BYTES; - } - } - - public static class TrimAnalysis { - private final int totalClassCount; - private final int retainedClassCount; - private final Set retainedClasses; - private final long originalSizeBytes; - private final List entryPoints; - private final List excludedEntryPoints; - - public TrimAnalysis(int totalClassCount, int retainedClassCount, - Set retainedClasses, long originalSizeBytes, - List entryPoints, List excludedEntryPoints) { - this.totalClassCount = totalClassCount; - this.retainedClassCount = retainedClassCount; - this.retainedClasses = Collections.unmodifiableSet(retainedClasses); - this.originalSizeBytes = originalSizeBytes; - this.entryPoints = Collections.unmodifiableList(entryPoints); - this.excludedEntryPoints = Collections.unmodifiableList(excludedEntryPoints); - } - - public int getTotalClassCount() { - return totalClassCount; - } - - public int getRetainedClassCount() { - return retainedClassCount; - } - - public int getRemovedClassCount() { - return totalClassCount - retainedClassCount; - } - - public Set getRetainedClasses() { - return retainedClasses; - } - - public long getOriginalSizeBytes() { - return originalSizeBytes; - } - - public List getEntryPoints() { - return entryPoints; - } - - public List getExcludedEntryPoints() { - return excludedEntryPoints; - } - - public double getReductionPercent() { - return totalClassCount == 0 ? 0 : - 100.0 * getRemovedClassCount() / totalClassCount; - } - } - - public static class VerificationResult { - private final boolean ok; - private final int entryPointsChecked; - private final int retainedClassesChecked; - private final List errors; - private final List warnings; - - public VerificationResult(boolean ok, int entryPointsChecked, int retainedClassesChecked, - List errors, List warnings) { - this.ok = ok; - this.entryPointsChecked = entryPointsChecked; - this.retainedClassesChecked = retainedClassesChecked; - this.errors = Collections.unmodifiableList(errors); - this.warnings = Collections.unmodifiableList(warnings); - } - - public boolean isOk() { - return ok; - } - - public int getEntryPointsChecked() { - return entryPointsChecked; - } - - public int getRetainedClassesChecked() { - return retainedClassesChecked; - } - - public List getErrors() { - return errors; - } - - public List getWarnings() { - return warnings; - } - } - - public TrimPlan buildTrimPlan(Path jarPath, Set autoPatterns) throws IOException { - long originalSize = Files.size(jarPath); - List entries = readEntries(jarPath); - LinkedHashSet prefixes = collectCandidatePrefixes(entries, autoPatterns); - List candidates = new ArrayList<>(); - for (String prefix : prefixes) { - DirInfo info = summarizePrefix(prefix, entries); - if (info.getFileCount() == 0) { - continue; - } - candidates.add(classifyCandidate(info, autoPatterns, entries)); - } - candidates.sort(Comparator - .comparing((TrimCandidate c) -> !c.isSelectedByDefault()) - .thenComparing(c -> c.getRiskLevel().getOrder()) - .thenComparing(c -> c.getInfo().getPath())); - - LinkedHashSet selected = new LinkedHashSet<>(); - for (TrimCandidate candidate : candidates) { - if (candidate.isSelectedByDefault()) { - selected.add(candidate.getInfo().getPath()); - } - } - SelectionSummary summary = summarizeSelection(jarPath, selected); - List warnings = new ArrayList<>(); - if (originalSize > TARGET_SIZE_BYTES && !summary.isTargetReached()) { - warnings.add("默认安全方案预计仍大于 4M,需要再分析谨慎项或业务依赖。"); - } - return new TrimPlan(originalSize, TARGET_SIZE_BYTES, candidates, selected, - summary.getExcludedBytes(), summary.getExcludedFiles(), summary.getExcludedClasses(), - summary.getEstimatedOutputBytes(), warnings); - } - - public SelectionSummary summarizeSelection(Path jarPath, Set excludedPrefixes) throws IOException { - long originalSize = Files.size(jarPath); - List entries = readEntries(jarPath); - long bytes = 0; - int files = 0; - int classes = 0; - for (EntryInfo entry : entries) { - if (matchesAnyPrefix(entry.name, excludedPrefixes)) { - bytes += entry.sizeBytes; - files++; - if (entry.classFile) { - classes++; - } - } - } - long estimatedOutput = Math.max(0, originalSize - bytes); - return new SelectionSummary(bytes, files, classes, estimatedOutput); - } - - public long estimateOutputSize(Path jarPath, TrimAnalysis analysis, TrimConfig config) throws IOException { - long estimate = 0; - for (EntryInfo entry : readEntries(jarPath)) { - if (isUnderExcludedDir(entry.name, config)) { - continue; - } - if (!entry.classFile) { - if (!shouldSkipResource(entry.name, config)) { - estimate += entry.sizeBytes; - } - continue; - } - if (analysis.getRetainedClasses().contains(jarEntryToClassName(entry.name))) { - estimate += entry.sizeBytes; - } - } - return estimate; - } - - public List scanJarDirectories(Path jarPath) throws IOException { - List entries = readEntries(jarPath); - LinkedHashSet topDirs = new LinkedHashSet<>(); - for (EntryInfo entry : entries) { - String top = topLevelPrefix(entry.name); - if (top != null) { - topDirs.add(top); - } - } - return topDirs.stream() - .map(prefix -> summarizePrefix(prefix, entries)) - .sorted(Comparator.comparingLong(DirInfo::getSizeBytes).reversed()) - .toList(); - } - - public TrimAnalysis analyze(Path jarPath, List extraPackages, TrimConfig config) throws IOException { - long originalSize = Files.size(jarPath); - TrimConfig noDirectoryExclusions = copyConfig(config); - noDirectoryExclusions.excludedDirs.clear(); - Map originalClasses = loadAllClasses(jarPath, noDirectoryExclusions); - Set originalEntryPoints = findEntryPoints(originalClasses); - - Map allClasses = loadAllClasses(jarPath, config); - List excludedEntryPoints = originalEntryPoints.stream() - .filter(entry -> !allClasses.containsKey(entry)) - .sorted() - .toList(); - - log.info("分析 JAR: {} 类, 原始入口 {} 个, 大小 {}MB", - allClasses.size(), originalEntryPoints.size(), - String.format("%.1f", originalSize / 1_048_576.0)); - - Set reachable = computeReachable(allClasses, originalEntryPoints); - - 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); - } - } - - Set 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<>(originalEntryPoints), - excludedEntryPoints - ); - } - - public Path execute(Path jarPath, TrimAnalysis analysis, Path outputPath, TrimConfig config) throws IOException { - Set 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; - } - - if (!name.endsWith(".class")) { - if (shouldSkipResource(name, config)) { - continue; - } - jos.putNextEntry(new JarEntry(name)); - jis.transferTo(jos); - jos.closeEntry(); - continue; - } - - 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; - } - - public VerificationResult verifyOutput(Path outputPath, TrimAnalysis analysis) throws IOException { - Set outputClasses = loadClassNames(outputPath); - List errors = new ArrayList<>(); - List warnings = new ArrayList<>(); - - for (String entryPoint : analysis.getEntryPoints()) { - if (!outputClasses.contains(entryPoint)) { - errors.add("缺少 RPC/API 入口类: " + entryPoint); - } - } - for (String retained : analysis.getRetainedClasses()) { - if (!outputClasses.contains(retained)) { - errors.add("缺少接口签名可达类: " + retained); - if (errors.size() >= 20) { - warnings.add("缺失类超过 20 个,已截断显示。"); - break; - } - } - } - if (!analysis.getExcludedEntryPoints().isEmpty()) { - errors.add("用户选择的删除项覆盖了入口类: " + String.join(", ", analysis.getExcludedEntryPoints())); - } - if (analysis.getEntryPoints().isEmpty()) { - warnings.add("未识别到接口或 Dubbo/Feign/gRPC/Thrift 入口类,请确认该 JAR 是否为 API 模块。"); - } - return new VerificationResult(errors.isEmpty(), analysis.getEntryPoints().size(), - analysis.getRetainedClasses().size(), errors, warnings); - } - - private List readEntries(Path jarPath) throws IOException { - List entries = new ArrayList<>(); - try (JarFile jarFile = new JarFile(jarPath.toFile())) { - Enumeration enumeration = jarFile.entries(); - while (enumeration.hasMoreElements()) { - JarEntry entry = enumeration.nextElement(); - if (entry.isDirectory()) { - continue; - } - long size = entry.getCompressedSize(); - if (size < 0) { - size = Math.max(0, entry.getSize()); - } - entries.add(new EntryInfo(entry.getName(), size, entry.getName().endsWith(".class"))); - } - } - return entries; - } - - private LinkedHashSet collectCandidatePrefixes(List entries, Set autoPatterns) { - LinkedHashSet prefixes = new LinkedHashSet<>(); - addIfPresent(prefixes, entries, "lombok/"); - addIfPresent(prefixes, entries, "org/projectlombok/"); - addIfPresent(prefixes, entries, "org/jetbrains/"); - addIfPresent(prefixes, entries, "org/intellij/"); - addIfPresent(prefixes, entries, "javax/annotation/processing/"); - addIfPresent(prefixes, entries, "META-INF/maven/"); - - for (EntryInfo entry : entries) { - String lower = entry.name.toLowerCase(Locale.ROOT); - if (isFontResource(lower) || isNativeLibrary(lower)) { - prefixes.add(directoryPrefix(entry.name)); - } else if (lower.equals("meta-inf/spring-configuration-metadata.json")) { - prefixes.add(entry.name); - } - } - - for (EntryInfo entry : entries) { - addDirectoryPrefixes(prefixes, entry.name); - prefixes.add(entry.name); - } - - if (autoPatterns != null && !autoPatterns.isEmpty()) { - List patterns = autoPatterns.stream() - .filter(Objects::nonNull) - .map(s -> s.toLowerCase(Locale.ROOT).trim()) - .filter(s -> !s.isEmpty()) - .toList(); - for (EntryInfo entry : entries) { - String prefix = directoryPrefix(entry.name); - String lowerTop = prefix.toLowerCase(Locale.ROOT); - for (String pattern : patterns) { - if (lowerTop.contains(pattern)) { - prefixes.add(prefix); - } - } - } - } - return prefixes; - } - - private void addIfPresent(Set prefixes, List entries, String prefix) { - for (EntryInfo entry : entries) { - if (entry.name.startsWith(prefix)) { - prefixes.add(prefix); - return; - } - } - } - - private static void addDirectoryPrefixes(Set prefixes, String entryName) { - int from = 0; - while (true) { - int slash = entryName.indexOf('/', from); - if (slash < 0) { - return; - } - prefixes.add(entryName.substring(0, slash + 1)); - from = slash + 1; - } - } - - private TrimCandidate classifyCandidate(DirInfo info, Set autoPatterns, List entries) { - String path = info.getPath(); - String lower = path.toLowerCase(Locale.ROOT); - boolean customMatched = matchesPattern(lower, autoPatterns); - boolean fileEntry = !path.endsWith("/"); - - if (lower.equals("lombok/") || lower.equals("org/projectlombok/")) { - return safe(info, "Lombok 编译期依赖", - "Lombok 只在编译期生成 getter/setter/构造器等字节码,Dubbo 消费端不需要 Lombok 处理器或注解包。"); - } - if (lower.equals("org/jetbrains/") || lower.equals("org/intellij/") - || lower.equals("javax/annotation/processing/")) { - return safe(info, "IDE/编译期注解", - "这些包用于 IDE 提示、静态分析或注解处理,不参与 RPC 方法调用和序列化;IDE 注解缺失不会影响 Dubbo 代理调用。"); - } - if (lower.startsWith("lombok/") || lower.startsWith("org/projectlombok/") - || lower.startsWith("org/jetbrains/") || lower.startsWith("org/intellij/") - || lower.startsWith("javax/annotation/processing/")) { - return safe(info, "编译期/IDE 文件", - "该文件位于已知编译期或 IDE 辅助依赖目录下,不属于 API 契约运行时必需内容,不参与 RPC 代理和序列化。"); - } - if (lower.equals("meta-inf/maven/")) { - return safe(info, "Maven 构建元数据", - "META-INF/maven 是构建和依赖溯源信息,Dubbo 运行时不会通过它解析接口或 DTO,不依赖 pom.properties/pom.xml 元数据。"); - } - if (lower.startsWith("meta-inf/maven/") && fileEntry) { - return safe(info, "Maven 构建元数据文件", - "该文件是 Maven 构建或依赖溯源信息,不承载 RPC 接口签名,Dubbo 消费端不依赖构建元数据完成接口调用。"); - } - if (isFontCandidate(path, entries) && info.getClassCount() == 0) { - return safe(info, "字体/前端资源", - "字体资源只用于页面或文档展示,API JAR 作为 RPC 契约包时不需要,Dubbo 序列化不读取 ttf/otf/woff 字体资源。"); - } - if (isNativeCandidate(path, entries) && info.getClassCount() == 0) { - return safe(info, "原生动态库", - "dll/so/dylib 属于 JNI 或平台运行库,API JAR 不应依赖本地动态库完成 Dubbo 调用,本地库不参与接口代理和序列化。"); - } - if (lower.equals("meta-inf/spring-configuration-metadata.json")) { - return safe(info, "Spring 配置提示元数据", - "该文件用于 IDE/Spring Boot 配置提示,不参与 Dubbo 接口调用,删除后不影响 Dubbo 消费端生成代理。"); - } - if (customMatched && info.getClassCount() == 0) { - return safe(info, "自定义规则命中", - "命中自动勾选规则且不包含 class 文件,属于资源类冗余内容,不会删除 Dubbo 接口、DTO、枚举或异常类型。"); - } - if (fileEntry && lower.endsWith(".class")) { - return new TrimCandidate(info, false, false, RiskLevel.CAUTION, - "Java class 文件", - "单个 class 文件可能是接口、DTO、枚举、异常、注解或签名可达类型;如需删除,先确认不在接口签名和序列化模型中。"); - } - if (fileEntry) { - return new TrimCandidate(info, false, false, RiskLevel.CAUTION, - "资源文件", - "该文件不是默认安全规则命中的资源,可能只是说明/配置,也可能被框架或运行时读取;SPI、Spring、配置类资源需人工确认。"); - } - if (lower.equals("com/")) { - return new TrimCandidate(info, false, false, RiskLevel.BLOCKED, - "业务/API 顶层包", - "com 包含 API 接口、DTO、枚举、异常等业务契约,整目录删除会破坏 Dubbo 调用,该目录禁止默认删除。"); - } - if (lower.equals("org/") || lower.equals("javax/") || lower.equals("jakarta/")) { - return new TrimCandidate(info, false, false, RiskLevel.DANGER, - "第三方顶层包", - "整目录删除可能移除 Dubbo 注解、校验注解、序列化相关类型或接口签名依赖;建议只删下方明确安全的子目录。"); - } - if (lower.startsWith("com/")) { - return new TrimCandidate(info, false, false, RiskLevel.CAUTION, - "业务/第三方子包", - "该子目录可能包含 API 接口、DTO、枚举、异常或第三方依赖;执行前后会用入口类和签名可达类做校验兜底。"); - } - if (lower.startsWith("org/") || lower.startsWith("javax/") || lower.startsWith("jakarta/")) { - return new TrimCandidate(info, false, false, RiskLevel.CAUTION, - "第三方子包", - "相比顶层目录,子包删除粒度更小,但仍可能包含关键依赖;若删除了入口或签名可达类,校验会提示阻断。"); - } - if (lower.equals("meta-inf/")) { - return new TrimCandidate(info, false, false, RiskLevel.CAUTION, - "JAR 元数据目录", - "META-INF 中可能包含 MANIFEST、services、签名或框架发现文件,整目录删除存在兼容风险;不建议整目录删除。"); - } - if (lower.startsWith("meta-inf/")) { - return new TrimCandidate(info, false, false, RiskLevel.CAUTION, - "JAR 元数据子目录", - "META-INF 子目录可能是构建元数据、SPI、框架自动发现或签名文件;services、spring、签名相关不建议盲删。"); - } - if (customMatched) { - return new TrimCandidate(info, false, false, RiskLevel.CAUTION, - "自定义规则命中", - "命中自动勾选规则但包含 class 文件,需要人工确认;若删除了接口签名可达类,执行后验证会失败。"); - } - return new TrimCandidate(info, false, false, RiskLevel.CAUTION, - "未分类目录", - "未命中安全规则,无法证明整目录删除一定安全;如需压到 4M 以下,可逐项确认并依赖执行后验证兜底。"); - } - - private TrimCandidate safe(DirInfo info, String category, String description) { - return new TrimCandidate(info, true, true, RiskLevel.SAFE, category, description); - } - - private DirInfo summarizePrefix(String prefix, List entries) { - int fileCount = 0; - int classCount = 0; - long size = 0; - for (EntryInfo entry : entries) { - if (matchesPrefix(entry.name, prefix)) { - fileCount++; - size += entry.sizeBytes; - if (entry.classFile) { - classCount++; - } - } - } - return new DirInfo(prefix, displayName(prefix), depthOfPrefix(prefix), classCount, fileCount, size); - } - - private static boolean matchesAnyPrefix(String name, Set prefixes) { - for (String prefix : prefixes) { - if (matchesPrefix(name, prefix)) { - return true; - } - } - return false; - } - - private static boolean matchesPrefix(String name, String prefix) { - return prefix.endsWith("/") ? name.startsWith(prefix) : name.equals(prefix); - } - - private static boolean matchesPattern(String lowerPath, Set patterns) { - if (patterns == null) { - return false; - } - for (String pattern : patterns) { - if (pattern != null && !pattern.trim().isEmpty() - && lowerPath.contains(pattern.toLowerCase(Locale.ROOT).trim())) { - return true; - } - } - return false; - } - - private static String topLevelPrefix(String entryName) { - int slash = entryName.indexOf('/'); - if (slash <= 0) { - return null; - } - return entryName.substring(0, slash + 1); - } - - private static String directoryPrefix(String entryName) { - int slash = entryName.lastIndexOf('/'); - if (slash < 0) { - return entryName; - } - return entryName.substring(0, slash + 1); - } - - private static int depthOfPrefix(String prefix) { - int depth = 0; - for (int i = 0; i < prefix.length(); i++) { - if (prefix.charAt(i) == '/') { - depth++; - } - } - return Math.max(depth, 1); - } - - private static String displayName(String prefix) { - if (prefix.endsWith("/")) { - return prefix.substring(0, prefix.length() - 1); - } - return prefix; - } - - private static boolean isFontCandidate(String prefix, List entries) { - for (EntryInfo entry : entries) { - if (matchesPrefix(entry.name, prefix) && isFontResource(entry.name.toLowerCase(Locale.ROOT))) { - return true; - } - } - return false; - } - - private static boolean isNativeCandidate(String prefix, List entries) { - for (EntryInfo entry : entries) { - if (matchesPrefix(entry.name, prefix) && isNativeLibrary(entry.name.toLowerCase(Locale.ROOT))) { - return true; - } - } - return false; - } - - private static boolean isFontResource(String lowerName) { - return lowerName.endsWith(".ttf") - || lowerName.endsWith(".otf") - || lowerName.endsWith(".woff") - || lowerName.endsWith(".woff2") - || lowerName.contains("/fonts/"); - } - - private static boolean isNativeLibrary(String lowerName) { - return lowerName.endsWith(".dll") - || lowerName.endsWith(".so") - || lowerName.endsWith(".dylib"); - } - - private Map loadAllClasses(Path jarPath, TrimConfig config) throws IOException { - Map 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; - } - - private Set loadClassNames(Path jarPath) throws IOException { - Set classes = new LinkedHashSet<>(); - try (JarFile jarFile = new JarFile(jarPath.toFile())) { - Enumeration enumeration = jarFile.entries(); - while (enumeration.hasMoreElements()) { - JarEntry entry = enumeration.nextElement(); - if (!entry.isDirectory() && entry.getName().endsWith(".class") - && !entry.getName().equals("module-info.class")) { - classes.add(jarEntryToClassName(entry.getName())); - } - } - } - return classes; - } - - private static boolean isUnderExcludedDir(String entryName, TrimConfig config) { - if (config.excludedDirs.isEmpty()) { - return false; - } - for (String dir : config.excludedDirs) { - if (matchesPrefix(entryName, dir)) { - return true; - } - } - return false; - } - - 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; - } - return config.skipShadeRepack && className.contains(".shaded."); - } - - private static boolean shouldSkipResource(String entryName, TrimConfig config) { - String lower = entryName.toLowerCase(Locale.ROOT); - if (config.skipMavenMeta && (entryName.startsWith("META-INF/maven/") || entryName.equals("META-INF/"))) { - return true; - } - if (config.skipSpringMeta) { - if (entryName.equals("META-INF/spring-configuration-metadata.json") - || entryName.startsWith("META-INF/spring/") - || entryName.equals("META-INF/spring.factories") - || lower.contains("application")) { - return true; - } - } - if (config.skipFonts && isFontResource(lower)) { - return true; - } - return config.skipNativeLibs && isNativeLibrary(lower); - } - - private static String jarEntryToClassName(String entryName) { - return entryName.substring(0, entryName.length() - 6).replace('/', '.'); - } - - private Set findEntryPoints(Map allClasses) { - Set entries = new LinkedHashSet<>(); - for (Map.Entry 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; - } - EntryDetector detector = new EntryDetector(); - cr.accept(detector, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); - if (detector.isRpcEntry) { - entries.add(name); - } - } catch (Exception ignored) { - } - } - return entries; - } - - private Set computeReachable(Map allClasses, Set seeds) { - Set reachable = new LinkedHashSet<>(); - Queue queue = new ArrayDeque<>(); - - for (String seed : seeds) { - if (allClasses.containsKey(seed) && reachable.add(seed)) { - queue.add(seed); - } - } - - while (!queue.isEmpty()) { - String cls = queue.poll(); - byte[] code = allClasses.get(cls); - if (code == null) { - continue; - } - try { - Set refs = extractTypeReferences(code); - for (String ref : refs) { - if (allClasses.containsKey(ref) && reachable.add(ref)) { - queue.add(ref); - } - } - } catch (Exception e) { - log.debug("BFS 分析 {} 失败: {}", cls, e.getMessage()); - } - } - - return reachable; - } - - private Set extractTypeReferences(byte[] classBytes) { - Set 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) { - 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); - } - - private static TrimConfig copyConfig(TrimConfig config) { - TrimConfig copy = new TrimConfig(); - copy.skipLombok = config.skipLombok; - copy.skipFonts = config.skipFonts; - copy.skipIdeAnnotations = config.skipIdeAnnotations; - copy.skipShadeRepack = config.skipShadeRepack; - copy.skipNativeLibs = config.skipNativeLibs; - copy.skipMavenMeta = config.skipMavenMeta; - copy.skipSpringMeta = config.skipSpringMeta; - copy.excludedDirs = new LinkedHashSet<>(config.excludedDirs); - return copy; - } - - private static class EntryInfo { - final String name; - final long sizeBytes; - final boolean classFile; - - EntryInfo(String name, long sizeBytes, boolean classFile) { - this.name = name; - this.sizeBytes = sizeBytes; - this.classFile = classFile; - } - } - - 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 refs; - - ReferenceCollector(Set 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 AnnotationVisitor visitAnnotation(String desc, boolean visible) { - addAnnotationDesc(desc); - return new AnnotationRefCollector(refs); - } - - @Override - public FieldVisitor visitField(int access, String name, String desc, - String signature, Object value) { - addTypeDesc(desc); - parseSignature(signature); - return new FieldVisitor(Opcodes.ASM9) { - @Override - public AnnotationVisitor visitAnnotation(String desc, boolean visible) { - addAnnotationDesc(desc); - return new AnnotationRefCollector(refs); - } - }; - } - - @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 new MethodVisitor(Opcodes.ASM9) { - @Override - public AnnotationVisitor visitAnnotation(String desc, boolean visible) { - addAnnotationDesc(desc); - return new AnnotationRefCollector(refs); - } - - @Override - public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { - addAnnotationDesc(desc); - return new AnnotationRefCollector(refs); - } - }; - } - - private void addAnnotationDesc(String desc) { - if (desc != null) { - addType(Type.getType(desc)); - } - } - - private void addInternalName(String internalName) { - if (internalName != null) { - 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; - } - 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('/', '.'); - refs.add(typeRef); - pos = semi + 1; - } - } - } - - private static class AnnotationRefCollector extends AnnotationVisitor { - private final Set refs; - - AnnotationRefCollector(Set refs) { - super(Opcodes.ASM9); - this.refs = refs; - } - - @Override - public void visit(String name, Object value) { - if (value instanceof Type) { - refs.add(((Type) value).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()); - } - } -} diff --git a/src/main/resources/api-trimmer-patterns.txt b/src/main/resources/api-trimmer-patterns.txt deleted file mode 100644 index ef11a9e..0000000 --- a/src/main/resources/api-trimmer-patterns.txt +++ /dev/null @@ -1,5 +0,0 @@ -# API Trim 自动勾选目录名配置(每行一个关键词,忽略大小写,匹配即勾选) -# 修改后保存,点击「刷新目录」即可生效 -lombok -ttf -class50