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