JAR 瘦身插件

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