代码提交

This commit is contained in:
张家训
2026-06-29 10:40:31 +08:00
parent 8d26502f29
commit 3f5e6e7ad0
2 changed files with 183 additions and 296 deletions
@@ -58,7 +58,7 @@ public class ApiTrimmerWindow {
private ProgressIndicator progressIndicator; private ProgressIndicator progressIndicator;
private static final int TREE_FONT_SIZE = 12; private static final int TREE_FONT_SIZE = 12;
private static final int TREE_ROW_HEIGHT = 28; private static final int TREE_ROW_HEIGHT = 26;
private TreeTableView<DirRow> dirTree; private TreeTableView<DirRow> dirTree;
private TreeItem<DirRow> treeRoot; private TreeItem<DirRow> treeRoot;
@@ -67,16 +67,9 @@ public class ApiTrimmerWindow {
private TreeItem<DirRow> candidatePopupRoot; private TreeItem<DirRow> candidatePopupRoot;
private final ObservableList<DirRow> dirRows = FXCollections.observableArrayList(); private final ObservableList<DirRow> dirRows = FXCollections.observableArrayList();
private Label dirTableLabel; private Label dirTableLabel;
private TextField candidateFilterField;
private CheckBox selectedOnlyCheck;
private CheckBox riskOnlyCheck;
private TextArea selectedPathsArea;
private VBox resultBox;
private Button previewBtn; private Button previewBtn;
private Button trimBtn; private Button trimBtn;
private Button cancelBtn;
private ApiTrimmerService.TrimPlan currentPlan; private ApiTrimmerService.TrimPlan currentPlan;
private ApiTrimmerService.TrimAnalysis analysis; private ApiTrimmerService.TrimAnalysis analysis;
private Set<String> autoCheckPatterns = new LinkedHashSet<>(); private Set<String> autoCheckPatterns = new LinkedHashSet<>();
@@ -201,15 +194,10 @@ public class ApiTrimmerWindow {
title.setTextFill(Theme.TEXT_PRIMARY); title.setTextFill(Theme.TEXT_PRIMARY);
VBox inputSection = createInputSection(); VBox inputSection = createInputSection();
resultBox = createResultBox();
ScrollPane resultScroll = new ScrollPane(resultBox);
resultScroll.setFitToWidth(true);
resultScroll.setPrefHeight(190);
resultScroll.setStyle("-fx-background-color: transparent;");
HBox btnBar = createButtonBar(); HBox btnBar = createButtonBar();
root.getChildren().addAll(title, inputSection, resultScroll, btnBar); root.getChildren().addAll(title, inputSection, btnBar);
VBox.setVgrow(inputSection, Priority.ALWAYS); VBox.setVgrow(inputSection, Priority.ALWAYS);
Scene scene = new Scene(root, 1440, 900); Scene scene = new Scene(root, 1440, 900);
@@ -255,58 +243,10 @@ public class ApiTrimmerWindow {
GridPane summary = createSummaryPanel(); GridPane summary = createSummaryPanel();
HBox selectRow = new HBox(Theme.S2); HBox selectRow = new HBox(Theme.S2);
selectRow.setAlignment(Pos.CENTER_LEFT); selectRow.setAlignment(Pos.CENTER_RIGHT);
Button selectRecommendedBtn = StyledButton.small("选择安全推荐", Font.font("Microsoft YaHei", fsBody() - 1));
selectRecommendedBtn.setOnAction(e -> selectRecommended());
Button clearBtn = StyledButton.small("清空选择", Font.font("Microsoft YaHei", fsBody() - 1));
clearBtn.setOnAction(e -> clearSelection());
Button selectSafeBtn = StyledButton.small("仅保留安全项", Font.font("Microsoft YaHei", fsBody() - 1));
selectSafeBtn.setOnAction(e -> selectSafeOnly());
Button expandAllBtn = StyledButton.small("展开全部", Font.font("Microsoft YaHei", compactFont()));
expandAllBtn.setOnAction(e -> setTreeExpanded(true));
Button collapseAllBtn = StyledButton.small("折叠全部", Font.font("Microsoft YaHei", compactFont()));
collapseAllBtn.setOnAction(e -> setTreeExpanded(false));
Button popupCandidatesBtn = StyledButton.small("弹出候选窗口", Font.font("Microsoft YaHei", compactFont())); Button popupCandidatesBtn = StyledButton.small("弹出候选窗口", Font.font("Microsoft YaHei", compactFont()));
popupCandidatesBtn.setOnAction(e -> openCandidatePopup()); popupCandidatesBtn.setOnAction(e -> openCandidatePopup());
Button maximizeBtn = StyledButton.small("最大化窗口", Font.font("Microsoft YaHei", compactFont())); selectRow.getChildren().addAll(popupCandidatesBtn);
maximizeBtn.setOnAction(e -> {
stage.setMaximized(true);
stage.toFront();
});
selectRow.getChildren().addAll(selectRecommendedBtn, clearBtn, selectSafeBtn,
expandAllBtn, collapseAllBtn, popupCandidatesBtn, maximizeBtn);
candidateFilterField = new TextField();
candidateFilterField.setPromptText("筛选路径 / 类型 / 删除原因 / Dubbo 说明");
candidateFilterField.getStyleClass().add(Theme.CSS_TEXT_FIELD);
HBox.setHgrow(candidateFilterField, Priority.ALWAYS);
candidateFilterField.textProperty().addListener((obs, oldVal, newVal) -> updateCandidateFilter());
selectedOnlyCheck = new CheckBox("只看已选");
selectedOnlyCheck.setFont(Theme.base(compactFont()));
selectedOnlyCheck.selectedProperty().addListener((obs, oldVal, newVal) -> updateCandidateFilter());
riskOnlyCheck = new CheckBox("只看需确认");
riskOnlyCheck.setFont(Theme.base(compactFont()));
riskOnlyCheck.selectedProperty().addListener((obs, oldVal, newVal) -> updateCandidateFilter());
Button clearFilterBtn = StyledButton.small("清除筛选", Font.font("Microsoft YaHei", fsBody() - 2));
clearFilterBtn.setOnAction(e -> {
candidateFilterField.clear();
selectedOnlyCheck.setSelected(false);
riskOnlyCheck.setSelected(false);
});
HBox filterRow = new HBox(Theme.S2, candidateFilterField, selectedOnlyCheck, riskOnlyCheck, clearFilterBtn);
filterRow.setAlignment(Pos.CENTER_LEFT);
selectedPathsArea = new TextArea();
selectedPathsArea.setEditable(false);
selectedPathsArea.setWrapText(true);
selectedPathsArea.setPrefRowCount(3);
selectedPathsArea.setMaxHeight(88);
selectedPathsArea.setFont(Theme.base(compactFont()));
selectedPathsArea.setText("有效删除前缀: -");
dirTableLabel = new Label("JAR 文件和目录(勾选 = 删除该文件或整目录):"); dirTableLabel = new Label("JAR 文件和目录(勾选 = 删除该文件或整目录):");
dirTableLabel.setFont(Font.font("Microsoft YaHei", Math.max(12, fsLabel() - 2))); dirTableLabel.setFont(Font.font("Microsoft YaHei", Math.max(12, fsLabel() - 2)));
@@ -315,8 +255,7 @@ public class ApiTrimmerWindow {
treeRoot.setExpanded(true); treeRoot.setExpanded(true);
dirTree = createCandidateTree(treeRoot); dirTree = createCandidateTree(treeRoot);
section.getChildren().addAll(jarRow, configRow, summary, selectRow, filterRow, section.getChildren().addAll(jarRow, configRow, summary, dirTableLabel, dirTree, selectRow);
selectedPathsArea, dirTableLabel, dirTree);
VBox.setVgrow(dirTree, Priority.ALWAYS); VBox.setVgrow(dirTree, Priority.ALWAYS);
return section; return section;
} }
@@ -343,22 +282,24 @@ public class ApiTrimmerWindow {
}); });
selCol.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(selCol)); selCol.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(selCol));
TreeTableColumn<DirRow, String> iconCol = treeIconColumn("图标", 42); TreeTableColumn<DirRow, String> nameCol = treeNameColumn("名称", 280);
TreeTableColumn<DirRow, String> nameCol = treeNameColumn("名称", 300); TreeTableColumn<DirRow, String> fileTypeCol = treeTextColumn("类型", 80, DirRow::fileTypeProperty);
TreeTableColumn<DirRow, String> fileTypeCol = treeTextColumn("文件类型", 92, DirRow::fileTypeProperty); TreeTableColumn<DirRow, String> sizeCol = treeTextColumn("大小", 72, DirRow::sizeProperty);
TreeTableColumn<DirRow, String> riskCol = treeTextColumn("风险", 70, DirRow::riskProperty);
TreeTableColumn<DirRow, String> categoryCol = treeTextColumn("删除类型", 120, DirRow::categoryProperty);
TreeTableColumn<DirRow, String> coverageCol = treeTextColumn("覆盖", 78, DirRow::coverageProperty);
TreeTableColumn<DirRow, String> sizeCol = treeTextColumn("大小", 86, DirRow::sizeProperty);
sizeCol.setStyle("-fx-alignment: CENTER-RIGHT;"); sizeCol.setStyle("-fx-alignment: CENTER-RIGHT;");
TreeTableColumn<DirRow, String> countCol = treeTextColumn("文件/类", 88, DirRow::countProperty); TreeTableColumn<DirRow, String> coverageCol = treeTextColumn("覆盖", 50, DirRow::coverageProperty);
countCol.setStyle("-fx-alignment: CENTER-RIGHT;"); TreeTableColumn<DirRow, String> categoryCol = treeTextColumn("来源", 96, DirRow::categoryProperty);
TreeTableColumn<DirRow, String> reasonCol = treeTextColumn("为什么可删除", 360, DirRow::reasonProperty); TreeTableColumn<DirRow, String> riskCol = treeTextColumn("风险", 55, DirRow::riskProperty);
TreeTableColumn<DirRow, String> rpcCol = treeTextColumn("RPC/Dubbo 安全说明", 440, DirRow::rpcSafetyProperty);
tree.getColumns().setAll(selCol, iconCol, nameCol, fileTypeCol, riskCol, categoryCol, // 所有列可拖拽调整宽度
coverageCol, sizeCol, countCol, reasonCol, rpcCol); for (TreeTableColumn<DirRow, ?> 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.setTreeColumn(nameCol);
tree.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
tree.setRowFactory(table -> new TreeTableRow<>() { tree.setRowFactory(table -> new TreeTableRow<>() {
@Override @Override
protected void updateItem(DirRow item, boolean empty) { protected void updateItem(DirRow item, boolean empty) {
@@ -367,17 +308,30 @@ public class ApiTrimmerWindow {
setStyle(""); setStyle("");
return; 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()) { switch (item.getRiskLevel()) {
case SAFE: case SAFE: bg = "#dbeafe"; break;
setStyle("-fx-background-color: #f0fdf4;");
break;
case DANGER: case DANGER:
case BLOCKED: case BLOCKED: bg = "#fecdd3"; break;
setStyle("-fx-background-color: #fff1f2;"); default: bg = "#eff6ff"; break;
break;
default:
setStyle("");
} }
} 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; return tree;
@@ -404,7 +358,7 @@ public class ApiTrimmerWindow {
return; return;
} }
setText(item); setText(item);
setGraphic(null); setGraphic(createFileIcon(row));
tooltip.setText(row.getInfo().getPath()); tooltip.setText(row.getInfo().getPath());
setTooltip(tooltip); setTooltip(tooltip);
} }
@@ -412,31 +366,6 @@ public class ApiTrimmerWindow {
return col; return col;
} }
private TreeTableColumn<DirRow, String> treeIconColumn(String title, int width) {
TreeTableColumn<DirRow, String> col = new TreeTableColumn<>(title);
col.setPrefWidth(width);
col.setMinWidth(width);
col.setMaxWidth(width);
col.setSortable(false);
col.setCellValueFactory(cdf -> new SimpleStringProperty(""));
col.setCellFactory(column -> new TreeTableCell<>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
DirRow row = getTreeTableRow() == null ? null : getTreeTableRow().getItem();
if (empty || row == null) {
setText(null);
setGraphic(null);
return;
}
setText(null);
setGraphic(createFileIcon(row));
setAlignment(Pos.CENTER);
}
});
return col;
}
private TreeTableColumn<DirRow, String> treeTextColumn(String title, int width, private TreeTableColumn<DirRow, String> treeTextColumn(String title, int width,
java.util.function.Function<DirRow, StringProperty> valueFactory) { java.util.function.Function<DirRow, StringProperty> valueFactory) {
TreeTableColumn<DirRow, String> col = new TreeTableColumn<>(title); TreeTableColumn<DirRow, String> col = new TreeTableColumn<>(title);
@@ -448,17 +377,24 @@ public class ApiTrimmerWindow {
col.setCellFactory(column -> new TreeTableCell<>() { col.setCellFactory(column -> new TreeTableCell<>() {
private final Tooltip tooltip = new Tooltip(); private final Tooltip tooltip = new Tooltip();
{
tooltip.setMaxWidth(480);
tooltip.setWrapText(true);
tooltip.setFont(Theme.base(TREE_FONT_SIZE));
}
@Override @Override
protected void updateItem(String item, boolean empty) { protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
if (empty || item == null) { if (empty || item == null || item.isEmpty()) {
setText(null); setText(null);
setTooltip(null); setTooltip(null);
} else { } else {
setText(item); setText(item);
DirRow row = getTreeTableRow() == null ? null : getTreeTableRow().getItem(); DirRow row = getTreeTableRow() == null ? null : getTreeTableRow().getItem();
tooltip.setText("名称".equals(title) && row != null ? row.getInfo().getPath() : item); String tip = "名称".equals(title) && row != null ? row.getInfo().getPath() : item;
setTooltip(tooltip); tooltip.setText(tip);
setTooltip(tip.length() > 40 ? tooltip : null);
} }
} }
}); });
@@ -551,19 +487,39 @@ public class ApiTrimmerWindow {
return label; return label;
} }
private VBox createResultBox() { private void showResultPopup(String title, List<ResultLine> lines) {
VBox box = new VBox(Theme.S2); Platform.runLater(() -> {
box.setPadding(new Insets(Theme.S2, Theme.S3, Theme.S2, Theme.S3)); VBox content = new VBox(4);
box.setStyle("-fx-background-color: white; -fx-background-radius: " content.setPadding(new Insets(Theme.S3));
+ Theme.RADIUS_SM + "; -fx-border-color: " + Theme.toWeb(Theme.BORDER) for (ResultLine line : lines) {
+ "; -fx-border-width: 1; -fx-border-radius: " + Theme.RADIUS_SM + ";"); Label label = new Label(line.text);
Label empty = new Label("分析预览和执行结果会显示在这里。安全模式会优先保留 Dubbo 接口、DTO、枚举、异常及其签名可达类。"); label.setWrapText(true);
empty.setFont(Font.font("Microsoft YaHei", fsBody() - 1)); label.setFont(line.bold ? Theme.bold(fsBody()) : Theme.base(fsBody() - 1));
empty.setTextFill(Theme.TEXT_MUTED); label.setTextFill(line.color);
box.getChildren().add(empty); content.getChildren().add(label);
return box;
} }
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() { private HBox createButtonBar() {
HBox bar = new HBox(Theme.S3); HBox bar = new HBox(Theme.S3);
bar.setAlignment(Pos.CENTER_RIGHT); bar.setAlignment(Pos.CENTER_RIGHT);
@@ -577,11 +533,7 @@ public class ApiTrimmerWindow {
trimBtn.setPrefWidth(110); trimBtn.setPrefWidth(110);
trimBtn.setOnAction(e -> onTrim()); trimBtn.setOnAction(e -> onTrim());
cancelBtn = StyledButton.secondary("取消", Font.font("Microsoft YaHei", fsButton())); bar.getChildren().addAll(previewBtn, trimBtn);
cancelBtn.setPrefWidth(80);
cancelBtn.setOnAction(e -> close());
bar.getChildren().addAll(previewBtn, trimBtn, cancelBtn);
return bar; return bar;
} }
@@ -623,7 +575,7 @@ public class ApiTrimmerWindow {
Task<ApiTrimmerService.TrimPlan> task = new Task<>() { Task<ApiTrimmerService.TrimPlan> task = new Task<>() {
@Override @Override
protected ApiTrimmerService.TrimPlan call() throws Exception { protected ApiTrimmerService.TrimPlan call() throws Exception {
return service.buildTrimPlan(jarPath, autoCheckPatterns); return service.buildTrimPlan(jarPath, Set.of());
} }
}; };
task.setOnRunning(e -> setBusy(true, "正在扫描 JAR...")); task.setOnRunning(e -> setBusy(true, "正在扫描 JAR..."));
@@ -791,9 +743,6 @@ public class ApiTrimmerWindow {
selectionLabel.setText("未选择 JAR"); selectionLabel.setText("未选择 JAR");
riskLabel.setText("-"); riskLabel.setText("-");
statusLabel.setText("等待选择 API JAR"); statusLabel.setText("等待选择 API JAR");
if (selectedPathsArea != null) {
selectedPathsArea.setText("有效删除前缀: -");
}
if (previewBtn != null) { if (previewBtn != null) {
previewBtn.setDisable(true); previewBtn.setDisable(true);
trimBtn.setDisable(true); trimBtn.setDisable(true);
@@ -802,7 +751,6 @@ public class ApiTrimmerWindow {
} }
updateCoverageMarkers(); updateCoverageMarkers();
Set<String> selected = selectedPrefixes(); Set<String> selected = selectedPrefixes();
updateSelectedPathsArea(selected);
try { try {
ApiTrimmerService.SelectionSummary summary = service.summarizeSelection(jarPath, selected); ApiTrimmerService.SelectionSummary summary = service.summarizeSelection(jarPath, selected);
targetLabel.setText("目标 < " + formatSize(ApiTrimmerService.TARGET_SIZE_BYTES) targetLabel.setText("目标 < " + formatSize(ApiTrimmerService.TARGET_SIZE_BYTES)
@@ -828,83 +776,78 @@ public class ApiTrimmerWindow {
} }
private void showPlanWarnings(ApiTrimmerService.TrimPlan plan) { private void showPlanWarnings(ApiTrimmerService.TrimPlan plan) {
resultBox.getChildren().clear(); List<ResultLine> lines = new ArrayList<>();
addResultLine("扫描完成", Theme.TEXT_PRIMARY, true); lines.add(new ResultLine("扫描完成", Theme.TEXT_PRIMARY, true));
addResultLine("原始大小: " + formatSize(plan.getOriginalSizeBytes()) lines.add(new ResultLine("原始大小: " + formatSize(plan.getOriginalSizeBytes())
+ ",默认安全方案估算: " + formatSize(plan.getEstimatedOutputBytes()) + ",默认安全方案估算: " + formatSize(plan.getEstimatedOutputBytes())
+ ",目标: < " + formatSize(plan.getTargetSizeBytes()), Theme.TEXT_PRIMARY, false); + ",目标: < " + formatSize(plan.getTargetSizeBytes()), Theme.TEXT_PRIMARY, false));
addResultLine("绿色安全项会说明为什么可删以及为什么不影响 Dubbo/RPC。" lines.add(new ResultLine("绿色安全项会说明为什么可删以及为什么不影响 Dubbo/RPC。"
+ "如果默认方案仍超过 4M,请先点“分析预览”,再逐项确认谨慎项。", Theme.TEXT_SECONDARY, false); + "如果默认方案仍超过 4M,请先点「分析预览」,再逐项确认谨慎项。", Theme.TEXT_SECONDARY, false));
for (String warning : plan.getWarnings()) { for (String warning : plan.getWarnings()) {
addResultLine("[提示] " + warning, Theme.WARNING, false); lines.add(new ResultLine("[提示] " + warning, Theme.WARNING, false));
} }
showResultPopup("扫描完成", lines);
} }
private void showPreviewResult(PreviewResult result) { private void showPreviewResult(PreviewResult result) {
resultBox.getChildren().clear(); List<ResultLine> lines = new ArrayList<>();
addResultLine("分析预览", Theme.TEXT_PRIMARY, true); lines.add(new ResultLine("分析预览", Theme.TEXT_PRIMARY, true));
addResultLine("原始类数: " + result.analysis.getTotalClassCount() lines.add(new ResultLine("原始类数: " + result.analysis.getTotalClassCount()
+ ",保留类数: " + result.analysis.getRetainedClassCount() + ",保留类数: " + result.analysis.getRetainedClassCount()
+ ",移除类数: " + result.analysis.getRemovedClassCount(), Theme.TEXT_PRIMARY, false); + ",移除类数: " + result.analysis.getRemovedClassCount(), Theme.TEXT_PRIMARY, false));
addResultLine("识别入口类: " + result.analysis.getEntryPoints().size() lines.add(new ResultLine("识别入口类: " + result.analysis.getEntryPoints().size()
+ ",估算输出: " + formatSize(result.estimatedSize) + ",估算输出: " + formatSize(result.estimatedSize)
+ (result.estimatedSize <= ApiTrimmerService.TARGET_SIZE_BYTES ? "(预计达标)" : "(预计未达标)"), + (result.estimatedSize <= ApiTrimmerService.TARGET_SIZE_BYTES ? "(预计达标)" : "(预计未达标)"),
result.estimatedSize <= ApiTrimmerService.TARGET_SIZE_BYTES ? Theme.SUCCESS : Theme.WARNING, false); result.estimatedSize <= ApiTrimmerService.TARGET_SIZE_BYTES ? Theme.SUCCESS : Theme.WARNING, false));
if (!result.analysis.getExcludedEntryPoints().isEmpty()) { if (!result.analysis.getExcludedEntryPoints().isEmpty()) {
addResultLine("阻断: 删除项覆盖了入口类 " + String.join(", ", result.analysis.getExcludedEntryPoints()), lines.add(new ResultLine("阻断: 删除项覆盖了入口类 " + String.join(", ", result.analysis.getExcludedEntryPoints()),
Theme.DANGER, false); Theme.DANGER, false));
} }
appendEntryPoints(result.analysis); appendEntryPoints(lines, result.analysis);
showResultPopup("分析预览", lines);
} }
private void showExecutionResult(ExecutionResult result) { private void showExecutionResult(ExecutionResult result) {
resultBox.getChildren().clear(); List<ResultLine> lines = new ArrayList<>();
double reduction = 100.0 * (1.0 - (double) result.outputSize / result.analysis.getOriginalSizeBytes()); double reduction = 100.0 * (1.0 - (double) result.outputSize / result.analysis.getOriginalSizeBytes());
addResultLine("精简完成", Theme.TEXT_PRIMARY, true); lines.add(new ResultLine("精简完成", Theme.TEXT_PRIMARY, true));
addResultLine("输出: " + result.outputPath.toAbsolutePath(), Theme.TEXT_PRIMARY, false); lines.add(new ResultLine("输出: " + result.outputPath.toAbsolutePath(), Theme.TEXT_PRIMARY, false));
addResultLine(String.format("原始: %s,输出: %s,缩减: %.1f%%,目标: %s", lines.add(new ResultLine(String.format("原始: %s,输出: %s,缩减: %.1f%%,目标: %s",
formatSize(result.analysis.getOriginalSizeBytes()), formatSize(result.analysis.getOriginalSizeBytes()),
formatSize(result.outputSize), formatSize(result.outputSize),
reduction, reduction,
result.outputSize <= ApiTrimmerService.TARGET_SIZE_BYTES ? "已小于 4M" : "仍大于 4M"), result.outputSize <= ApiTrimmerService.TARGET_SIZE_BYTES ? "已小于 4M" : "仍大于 4M"),
result.outputSize <= ApiTrimmerService.TARGET_SIZE_BYTES ? Theme.SUCCESS : Theme.WARNING, false); result.outputSize <= ApiTrimmerService.TARGET_SIZE_BYTES ? Theme.SUCCESS : Theme.WARNING, false));
ApiTrimmerService.VerificationResult verification = result.verification; ApiTrimmerService.VerificationResult verification = result.verification;
addResultLine("验证: " + (verification.isOk() ? "通过" : "未通过") lines.add(new ResultLine("验证: " + (verification.isOk() ? "通过" : "未通过")
+ ",入口类 " + verification.getEntryPointsChecked() + ",入口类 " + verification.getEntryPointsChecked()
+ ",签名可达类 " + verification.getRetainedClassesChecked(), + ",签名可达类 " + verification.getRetainedClassesChecked(),
verification.isOk() ? Theme.SUCCESS : Theme.DANGER, false); verification.isOk() ? Theme.SUCCESS : Theme.DANGER, false));
for (String error : verification.getErrors()) { for (String error : verification.getErrors()) {
addResultLine("[错误] " + error, Theme.DANGER, false); lines.add(new ResultLine("[错误] " + error, Theme.DANGER, false));
} }
for (String warning : verification.getWarnings()) { for (String warning : verification.getWarnings()) {
addResultLine("[提示] " + warning, Theme.WARNING, false); lines.add(new ResultLine("[提示] " + warning, Theme.WARNING, false));
} }
appendEntryPoints(result.analysis); appendEntryPoints(lines, result.analysis);
showResultPopup("精简完成", lines);
} }
private void appendEntryPoints(ApiTrimmerService.TrimAnalysis result) { private void appendEntryPoints(List<ResultLine> lines, ApiTrimmerService.TrimAnalysis result) {
if (result.getEntryPoints().isEmpty()) { if (result.getEntryPoints().isEmpty()) {
addResultLine("未识别到接口或 Dubbo 入口类,请确认该 JAR 是否为 API 模块。", Theme.WARNING, false); lines.add(new ResultLine("未识别到接口或 Dubbo 入口类,请确认该 JAR 是否为 API 模块。", Theme.WARNING, false));
return; return;
} }
addResultLine("入口类示例:", Theme.TEXT_PRIMARY, true); lines.add(new ResultLine("入口类示例:", Theme.TEXT_PRIMARY, true));
int limit = Math.min(12, result.getEntryPoints().size()); int limit = Math.min(12, result.getEntryPoints().size());
for (int i = 0; i < limit; i++) { for (int i = 0; i < limit; i++) {
addResultLine(" " + result.getEntryPoints().get(i), Theme.TEXT_SECONDARY, false); lines.add(new ResultLine(" " + result.getEntryPoints().get(i), Theme.TEXT_SECONDARY, false));
} }
if (result.getEntryPoints().size() > limit) { if (result.getEntryPoints().size() > limit) {
addResultLine(" ...还有 " + (result.getEntryPoints().size() - limit) + " 个", Theme.TEXT_SECONDARY, false); lines.add(new ResultLine(" ...还有 " + (result.getEntryPoints().size() - limit) + " 个", Theme.TEXT_SECONDARY, false));
} }
} }
private void addResultLine(String text, javafx.scene.paint.Color color, boolean bold) {
Label label = new Label(text);
label.setWrapText(true);
label.setFont(bold ? Theme.bold(fsBody()) : Theme.base(fsBody() - 1));
label.setTextFill(color);
resultBox.getChildren().add(label);
}
private void selectRecommended() { private void selectRecommended() {
refreshingSelection = true; refreshingSelection = true;
try { try {
@@ -1035,17 +978,7 @@ public class ApiTrimmerWindow {
} }
private boolean matchesCurrentFilter(DirRow row) { private boolean matchesCurrentFilter(DirRow row) {
String keyword = candidateFilterField == null ? "" : candidateFilterField.getText(); return true; // 主窗口不筛选,全部显示
String lowerKeyword = keyword == null ? "" : keyword.trim().toLowerCase(Locale.ROOT);
boolean selectedOnly = selectedOnlyCheck != null && selectedOnlyCheck.isSelected();
boolean riskOnly = riskOnlyCheck != null && riskOnlyCheck.isSelected();
if (selectedOnly && !row.isSelected()) {
return false;
}
if (riskOnly && row.getRiskLevel() == ApiTrimmerService.RiskLevel.SAFE) {
return false;
}
return lowerKeyword.isEmpty() || row.matches(lowerKeyword);
} }
private void addExistingAncestors(String path, Map<String, DirRow> byPath, Set<DirRow> included) { private void addExistingAncestors(String path, Map<String, DirRow> byPath, Set<DirRow> included) {
@@ -1082,11 +1015,28 @@ public class ApiTrimmerWindow {
candidatePopupRoot.setExpanded(true); candidatePopupRoot.setExpanded(true);
candidatePopupTree = createCandidateTree(candidatePopupRoot); candidatePopupTree = createCandidateTree(candidatePopupRoot);
candidatePopupTree.setPrefHeight(760); candidatePopupTree.setPrefHeight(760);
candidatePopupTree.setMinWidth(680);
Label title = new Label("候选删除项"); Label title = new Label("候选删除项");
title.setFont(Theme.heading(18)); title.setFont(Theme.heading(18));
title.setTextFill(Theme.TEXT_PRIMARY); 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())); Button expandBtn = StyledButton.small("展开全部", Font.font("Microsoft YaHei", compactFont()));
expandBtn.setOnAction(e -> setTreeExpanded(candidatePopupRoot, true)); expandBtn.setOnAction(e -> setTreeExpanded(candidatePopupRoot, true));
Button collapseBtn = StyledButton.small("折叠全部", Font.font("Microsoft YaHei", compactFont())); Button collapseBtn = StyledButton.small("折叠全部", Font.font("Microsoft YaHei", compactFont()));
@@ -1094,33 +1044,32 @@ public class ApiTrimmerWindow {
setTreeExpanded(candidatePopupRoot, false); setTreeExpanded(candidatePopupRoot, false);
candidatePopupRoot.setExpanded(true); candidatePopupRoot.setExpanded(true);
}); });
Button closeBtn = StyledButton.secondary("关闭", Font.font("Microsoft YaHei", compactFont())); HBox toolbar = new HBox(Theme.S2,
popSelectRecommended, popClear, popSafeOnly, expandBtn, collapseBtn);
HBox toolbar = new HBox(Theme.S2, expandBtn, collapseBtn);
toolbar.setAlignment(Pos.CENTER_LEFT); toolbar.setAlignment(Pos.CENTER_LEFT);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS); HBox titleBar = new HBox(Theme.S2, title);
HBox titleBar = new HBox(Theme.S2, title, spacer, closeBtn);
titleBar.setAlignment(Pos.CENTER_LEFT); titleBar.setAlignment(Pos.CENTER_LEFT);
VBox root = new VBox(Theme.S2, titleBar, toolbar, candidatePopupTree); 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.setPadding(new Insets(Theme.S3));
root.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_BODY) + ";"); root.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_BODY) + ";");
VBox.setVgrow(candidatePopupTree, Priority.ALWAYS);
candidatePopupStage = new Stage(); candidatePopupStage = new Stage();
candidatePopupStage.setTitle("API Trim - 候选删除项"); candidatePopupStage.setTitle("API Trim - 候选删除项");
candidatePopupStage.initOwner(stage); candidatePopupStage.initOwner(stage);
candidatePopupStage.initModality(Modality.NONE); candidatePopupStage.initModality(Modality.NONE);
candidatePopupStage.setResizable(true); candidatePopupStage.setResizable(true);
candidatePopupStage.setMinWidth(1180); candidatePopupStage.setMinWidth(720);
candidatePopupStage.setMinHeight(720); candidatePopupStage.setMinHeight(720);
candidatePopupStage.setScene(new Scene(root, 1380, 840)); candidatePopupStage.setScene(new Scene(root, 1380, 840));
candidatePopupStage.setOnCloseRequest(e -> clearCandidatePopupReferences()); candidatePopupStage.setOnCloseRequest(e -> clearCandidatePopupReferences());
closeBtn.setOnAction(e -> {
candidatePopupStage.close();
clearCandidatePopupReferences();
});
rebuildCandidateTree(candidatePopupRoot); rebuildCandidateTree(candidatePopupRoot);
candidatePopupStage.show(); candidatePopupStage.show();
@@ -1133,17 +1082,6 @@ public class ApiTrimmerWindow {
candidatePopupRoot = null; candidatePopupRoot = null;
} }
private void setTreeExpanded(boolean expanded) {
if (treeRoot != null) {
setTreeExpanded(treeRoot, expanded);
treeRoot.setExpanded(true);
}
if (candidatePopupRoot != null) {
setTreeExpanded(candidatePopupRoot, expanded);
candidatePopupRoot.setExpanded(true);
}
}
private void setTreeExpanded(TreeItem<DirRow> item, boolean expanded) { private void setTreeExpanded(TreeItem<DirRow> item, boolean expanded) {
item.setExpanded(expanded); item.setExpanded(expanded);
for (TreeItem<DirRow> child : item.getChildren()) { for (TreeItem<DirRow> child : item.getChildren()) {
@@ -1185,29 +1123,6 @@ public class ApiTrimmerWindow {
} }
} }
private void updateSelectedPathsArea(Set<String> selected) {
if (selectedPathsArea == null) {
return;
}
if (selected.isEmpty()) {
selectedPathsArea.setText("有效删除前缀: -");
return;
}
StringBuilder sb = new StringBuilder("有效删除前缀(父级已覆盖的子项不会重复执行):\n");
int index = 0;
for (String prefix : selected) {
if (index >= 16) {
sb.append("... 还有 ").append(selected.size() - index).append(" 项");
break;
}
sb.append("- ").append(prefix);
if (++index < selected.size()) {
sb.append('\n');
}
}
selectedPathsArea.setText(sb.toString());
}
private Set<String> rawSelectedPrefixes() { private Set<String> rawSelectedPrefixes() {
Set<String> selected = new LinkedHashSet<>(); Set<String> selected = new LinkedHashSet<>();
for (DirRow row : dirRows) { for (DirRow row : dirRows) {
@@ -1273,7 +1188,6 @@ public class ApiTrimmerWindow {
progressIndicator.setManaged(busy); progressIndicator.setManaged(busy);
previewBtn.setDisable(busy || selectedPrefixes().isEmpty()); previewBtn.setDisable(busy || selectedPrefixes().isEmpty());
trimBtn.setDisable(busy || selectedPrefixes().isEmpty()); trimBtn.setDisable(busy || selectedPrefixes().isEmpty());
cancelBtn.setDisable(false);
statusLabel.setText(message); statusLabel.setText(message);
} }
@@ -1315,7 +1229,6 @@ public class ApiTrimmerWindow {
private final StringProperty size; private final StringProperty size;
private final StringProperty count; private final StringProperty count;
private final StringProperty reason; private final StringProperty reason;
private final StringProperty rpcSafety;
DirRow(ApiTrimmerService.TrimCandidate candidate) { DirRow(ApiTrimmerService.TrimCandidate candidate) {
this.candidate = candidate; this.candidate = candidate;
@@ -1329,8 +1242,7 @@ public class ApiTrimmerWindow {
this.size = new SimpleStringProperty(formatSize(candidate.getInfo().getSizeBytes())); this.size = new SimpleStringProperty(formatSize(candidate.getInfo().getSizeBytes()));
this.count = new SimpleStringProperty(candidate.getInfo().getFileCount() this.count = new SimpleStringProperty(candidate.getInfo().getFileCount()
+ " / " + candidate.getInfo().getClassCount()); + " / " + candidate.getInfo().getClassCount());
this.reason = new SimpleStringProperty(candidate.getReason()); this.reason = new SimpleStringProperty(candidate.getDescription());
this.rpcSafety = new SimpleStringProperty(candidate.getRpcSafety());
} }
public ApiTrimmerService.DirInfo getInfo() { public ApiTrimmerService.DirInfo getInfo() {
@@ -1369,6 +1281,10 @@ public class ApiTrimmerWindow {
return candidate.getInfo().getPath().endsWith("/"); return candidate.getInfo().getPath().endsWith("/");
} }
public String getCoverage() {
return coverage.get();
}
public void setCoverage(String value) { public void setCoverage(String value) {
coverage.set(value == null ? "" : value); coverage.set(value == null ? "" : value);
} }
@@ -1379,8 +1295,7 @@ public class ApiTrimmerWindow {
|| risk.get().toLowerCase(Locale.ROOT).contains(lowerKeyword) || risk.get().toLowerCase(Locale.ROOT).contains(lowerKeyword)
|| category.get().toLowerCase(Locale.ROOT).contains(lowerKeyword) || category.get().toLowerCase(Locale.ROOT).contains(lowerKeyword)
|| coverage.get().toLowerCase(Locale.ROOT).contains(lowerKeyword) || coverage.get().toLowerCase(Locale.ROOT).contains(lowerKeyword)
|| reason.get().toLowerCase(Locale.ROOT).contains(lowerKeyword) || reason.get().toLowerCase(Locale.ROOT).contains(lowerKeyword);
|| rpcSafety.get().toLowerCase(Locale.ROOT).contains(lowerKeyword);
} }
public StringProperty nameProperty() { public StringProperty nameProperty() {
@@ -1419,9 +1334,6 @@ public class ApiTrimmerWindow {
return reason; return reason;
} }
public StringProperty rpcSafetyProperty() {
return rpcSafety;
}
private static String treeName(ApiTrimmerService.DirInfo info) { private static String treeName(ApiTrimmerService.DirInfo info) {
return leafName(info.getPath()); return leafName(info.getPath());
@@ -100,18 +100,16 @@ public class ApiTrimmerService {
private final boolean selectedByDefault; private final boolean selectedByDefault;
private final RiskLevel riskLevel; private final RiskLevel riskLevel;
private final String category; private final String category;
private final String reason; private final String description;
private final String rpcSafety;
public TrimCandidate(DirInfo info, boolean recommended, boolean selectedByDefault, public TrimCandidate(DirInfo info, boolean recommended, boolean selectedByDefault,
RiskLevel riskLevel, String category, String reason, String rpcSafety) { RiskLevel riskLevel, String category, String description) {
this.info = info; this.info = info;
this.recommended = recommended; this.recommended = recommended;
this.selectedByDefault = selectedByDefault; this.selectedByDefault = selectedByDefault;
this.riskLevel = riskLevel; this.riskLevel = riskLevel;
this.category = category; this.category = category;
this.reason = reason; this.description = description;
this.rpcSafety = rpcSafety;
} }
public DirInfo getInfo() { public DirInfo getInfo() {
@@ -134,12 +132,8 @@ public class ApiTrimmerService {
return category; return category;
} }
public String getReason() { public String getDescription() {
return reason; return description;
}
public String getRpcSafety() {
return rpcSafety;
} }
} }
@@ -629,114 +623,95 @@ public class ApiTrimmerService {
if (lower.equals("lombok/") || lower.equals("org/projectlombok/")) { if (lower.equals("lombok/") || lower.equals("org/projectlombok/")) {
return safe(info, "Lombok 编译期依赖", return safe(info, "Lombok 编译期依赖",
"Lombok 只在编译期生成 getter/setter/构造器等字节码,运行时 Dubbo 消费端不需要 Lombok 处理器或注解包。", "Lombok 只在编译期生成 getter/setter/构造器等字节码,Dubbo 消费端不需要 Lombok 处理器或注解包。");
"Dubbo 调用依赖接口、方法签名、DTO/枚举/异常的 class;这些字节码已经生成,不依赖 Lombok。");
} }
if (lower.equals("org/jetbrains/") || lower.equals("org/intellij/") if (lower.equals("org/jetbrains/") || lower.equals("org/intellij/")
|| lower.equals("javax/annotation/processing/")) { || lower.equals("javax/annotation/processing/")) {
return safe(info, "IDE/编译期注解", return safe(info, "IDE/编译期注解",
"这些包用于 IDE 提示、静态分析或注解处理,不参与 RPC 方法调用和序列化。", "这些包用于 IDE 提示、静态分析或注解处理,不参与 RPC 方法调用和序列化;IDE 注解缺失不会影响 Dubbo 代理调用。");
"接口签名中的业务类型会被可达性分析保留;IDE 注解缺失不会影响 Dubbo 代理调用。");
} }
if (lower.startsWith("lombok/") || lower.startsWith("org/projectlombok/") if (lower.startsWith("lombok/") || lower.startsWith("org/projectlombok/")
|| lower.startsWith("org/jetbrains/") || lower.startsWith("org/intellij/") || lower.startsWith("org/jetbrains/") || lower.startsWith("org/intellij/")
|| lower.startsWith("javax/annotation/processing/")) { || lower.startsWith("javax/annotation/processing/")) {
return safe(info, "编译期/IDE 文件", return safe(info, "编译期/IDE 文件",
"该文件位于已知编译期或 IDE 辅助依赖目录下,不属于 API 契约运行时必需内容。", "该文件位于已知编译期或 IDE 辅助依赖目录下,不属于 API 契约运行时必需内容,不参与 RPC 代理和序列化。");
"Dubbo 消费端加载接口、DTO、枚举、异常等业务 class;这些辅助文件不参与 RPC 代理和序列化。");
} }
if (lower.equals("meta-inf/maven/")) { if (lower.equals("meta-inf/maven/")) {
return safe(info, "Maven 构建元数据", return safe(info, "Maven 构建元数据",
"META-INF/maven 是构建和依赖溯源信息,Dubbo 运行时不会通过它解析接口或 DTO。", "META-INF/maven 是构建和依赖溯源信息,Dubbo 运行时不会通过它解析接口或 DTO,不依赖 pom.properties/pom.xml 元数据。");
"Dubbo 消费端按 class 加载接口与模型对象,不依赖 pom.properties/pom.xml 元数据。");
} }
if (lower.startsWith("meta-inf/maven/") && fileEntry) { if (lower.startsWith("meta-inf/maven/") && fileEntry) {
return safe(info, "Maven 构建元数据文件", return safe(info, "Maven 构建元数据文件",
"该文件是 Maven 构建或依赖溯源信息,不承载 RPC 接口签名。", "该文件是 Maven 构建或依赖溯源信息,不承载 RPC 接口签名,Dubbo 消费端不依赖构建元数据完成接口调用。");
"Dubbo 消费端不依赖 pom.xml/pom.properties 等构建元数据完成接口调用。");
} }
if (isFontCandidate(path, entries) && info.getClassCount() == 0) { if (isFontCandidate(path, entries) && info.getClassCount() == 0) {
return safe(info, "字体/前端资源", return safe(info, "字体/前端资源",
"字体资源只用于页面或文档展示,API JAR 作为 RPC 契约包时不需要字体文件。", "字体资源只用于页面或文档展示,API JAR 作为 RPC 契约包时不需要,Dubbo 序列化不读取 ttf/otf/woff 字体资源。");
"Dubbo 参数序列化只依赖接口和参数/返回值类型,不读取 ttf/otf/woff 字体资源。");
} }
if (isNativeCandidate(path, entries) && info.getClassCount() == 0) { if (isNativeCandidate(path, entries) && info.getClassCount() == 0) {
return safe(info, "原生动态库", return safe(info, "原生动态库",
"dll/so/dylib 属于 JNI 或平台运行库,API 契约 JAR 不应依赖本地动态库完成 Dubbo 调用。", "dll/so/dylib 属于 JNI 或平台运行库,API JAR 不应依赖本地动态库完成 Dubbo 调用,本地库不参与接口代理和序列化。");
"Dubbo 网关桥只需要 Java 接口和模型类;本地库不参与接口代理和序列化。");
} }
if (lower.equals("meta-inf/spring-configuration-metadata.json")) { if (lower.equals("meta-inf/spring-configuration-metadata.json")) {
return safe(info, "Spring 配置提示元数据", return safe(info, "Spring 配置提示元数据",
"该文件用于 IDE/Spring Boot 配置提示,不参与 Dubbo 接口调用。", "该文件用于 IDE/Spring Boot 配置提示,不参与 Dubbo 接口调用,删除后不影响 Dubbo 消费端生成代理。");
"删除配置提示不会删除接口 class,也不会影响 Dubbo 消费端生成代理。");
} }
if (customMatched && info.getClassCount() == 0) { if (customMatched && info.getClassCount() == 0) {
return safe(info, "自定义规则命中", return safe(info, "自定义规则命中",
"命中自动勾选规则且不包含 class 文件,通常属于资源类冗余内容。", "命中自动勾选规则且不包含 class 文件,属于资源类冗余内容,不会删除 Dubbo 接口、DTO、枚举或异常类型。");
"不包含 class 文件,不会删除 Dubbo 接口、DTO、枚举或异常类型。");
} }
if (fileEntry && lower.endsWith(".class")) { if (fileEntry && lower.endsWith(".class")) {
return new TrimCandidate(info, false, false, RiskLevel.CAUTION, return new TrimCandidate(info, false, false, RiskLevel.CAUTION,
"Java class 文件", "Java class 文件",
"单个 class 文件可能是接口、DTO、枚举、异常、注解或签名可达类型,插件不会默认删除。", "单个 class 文件可能是接口、DTO、枚举、异常、注解或签名可达类型;如需删除,先确认不在接口签名和序列化模型中。");
"如需删除,建议先确认它不在接口签名和序列化模型中;预览/执行校验会检查入口类和签名可达类。");
} }
if (fileEntry) { if (fileEntry) {
return new TrimCandidate(info, false, false, RiskLevel.CAUTION, return new TrimCandidate(info, false, false, RiskLevel.CAUTION,
"资源文件", "资源文件",
"该文件不是默认安全规则命中的资源,可能只是说明/配置,也可能被框架或运行时读取。", "该文件不是默认安全规则命中的资源,可能只是说明/配置,也可能被框架或运行时读取;SPI、Spring、配置类资源需人工确认。");
"删除非 class 文件通常不影响 Dubbo 接口签名,但 SPI、Spring、配置类资源仍需人工确认。");
} }
if (lower.equals("com/")) { if (lower.equals("com/")) {
return new TrimCandidate(info, false, false, RiskLevel.BLOCKED, return new TrimCandidate(info, false, false, RiskLevel.BLOCKED,
"业务/API 顶层包", "业务/API 顶层包",
"com 通常包含 API 接口、DTO、枚举、异常等业务契约,整目录删除会直接破坏 Dubbo 调用。", "com 包含 API 接口、DTO、枚举、异常等业务契约,整目录删除会破坏 Dubbo 调用,该目录禁止默认删除。");
"Dubbo 消费端必须加载接口和参数/返回值 class;该目录禁止默认删除。");
} }
if (lower.equals("org/") || lower.equals("javax/") || lower.equals("jakarta/")) { if (lower.equals("org/") || lower.equals("javax/") || lower.equals("jakarta/")) {
return new TrimCandidate(info, false, false, RiskLevel.DANGER, return new TrimCandidate(info, false, false, RiskLevel.DANGER,
"第三方顶层包", "第三方顶层包",
"整目录删除可能移除 Dubbo 注解、校验注解、序列化相关类型或接口签名依赖。", "整目录删除可能移除 Dubbo 注解、校验注解、序列化相关类型或接口签名依赖;建议只删下方明确安全的子目录。");
"建议只删除下方明确安全的子目录,例如 org/projectlombok,而不是删除整个顶层目录。");
} }
if (lower.startsWith("com/")) { if (lower.startsWith("com/")) {
return new TrimCandidate(info, false, false, RiskLevel.CAUTION, return new TrimCandidate(info, false, false, RiskLevel.CAUTION,
"业务/第三方子包", "业务/第三方子包",
"该子目录可能包含 API 接口、DTO、枚举、异常,也可能只是被 shade 进来的第三方依赖;插件无法默认判断可删。", "该子目录可能包含 API 接口、DTO、枚举、异常或第三方依赖;执行前后会用入口类和签名可达类做校验兜底。");
"只有在确认它不是接口签名、注解、序列化模型依赖后再勾选;执行前后会用入口类和签名可达类做校验兜底。");
} }
if (lower.startsWith("org/") || lower.startsWith("javax/") || lower.startsWith("jakarta/")) { if (lower.startsWith("org/") || lower.startsWith("javax/") || lower.startsWith("jakarta/")) {
return new TrimCandidate(info, false, false, RiskLevel.CAUTION, return new TrimCandidate(info, false, false, RiskLevel.CAUTION,
"第三方子包", "第三方子包",
"相比顶层目录,子包删除粒度更小,但仍可能包含 Dubbo、校验、序列化、注解或接口签名依赖。", "相比顶层目录,子包删除粒度更小,但仍可能包含关键依赖;若删除了入口或签名可达类,校验会提示阻断。");
"适合在默认安全项仍超过 4M 时逐项确认;若删除了入口或签名可达类,预览/执行校验会提示阻断。");
} }
if (lower.equals("meta-inf/")) { if (lower.equals("meta-inf/")) {
return new TrimCandidate(info, false, false, RiskLevel.CAUTION, return new TrimCandidate(info, false, false, RiskLevel.CAUTION,
"JAR 元数据目录", "JAR 元数据目录",
"META-INF 中可能包含 MANIFEST、services、签名或框架发现文件,整目录删除存在兼容风险。", "META-INF 中可能包含 MANIFEST、services、签名或框架发现文件,整目录删除存在兼容风险;不建议整目录删除。");
"可删除 META-INF/maven 等明确元数据,不建议整目录删除。");
} }
if (lower.startsWith("meta-inf/")) { if (lower.startsWith("meta-inf/")) {
return new TrimCandidate(info, false, false, RiskLevel.CAUTION, return new TrimCandidate(info, false, false, RiskLevel.CAUTION,
"JAR 元数据子目录", "JAR 元数据子目录",
"META-INF 子目录可能是构建元数据,也可能是 SPI、框架自动发现或签名文件,需要按目录含义确认。", "META-INF 子目录可能是构建元数据、SPI、框架自动发现或签名文件;services、spring、签名相关不建议盲删。");
"META-INF/maven 已作为安全项默认勾选;services、spring、签名相关目录不建议盲删。");
} }
if (customMatched) { if (customMatched) {
return new TrimCandidate(info, false, false, RiskLevel.CAUTION, return new TrimCandidate(info, false, false, RiskLevel.CAUTION,
"自定义规则命中", "自定义规则命中",
"命中自动勾选规则,但目录中包含 class 文件,需要人工确认是否属于非 API 依赖。", "命中自动勾选规则但包含 class 文件,需要人工确认;若删除了接口签名可达类,执行后验证会失败。");
"如果删除了接口签名可达类,执行后验证会失败;建议先查看原因再选择。");
} }
return new TrimCandidate(info, false, false, RiskLevel.CAUTION, return new TrimCandidate(info, false, false, RiskLevel.CAUTION,
"未分类目录", "未分类目录",
"未命中安全规则,插件无法证明整目录删除一定安全。", "未命中安全规则,无法证明整目录删除一定安全;如需压到 4M 以下,可逐项确认并依赖执行后验证兜底。");
"仅安全模式不会默认删除;如需压到 4M 以下,可逐项确认并依赖执行后验证兜底。");
} }
private TrimCandidate safe(DirInfo info, String category, String reason, String rpcSafety) { private TrimCandidate safe(DirInfo info, String category, String description) {
return new TrimCandidate(info, true, true, RiskLevel.SAFE, category, reason, rpcSafety); return new TrimCandidate(info, true, true, RiskLevel.SAFE, category, description);
} }
private DirInfo summarizePrefix(String prefix, List<EntryInfo> entries) { private DirInfo summarizePrefix(String prefix, List<EntryInfo> entries) {