代码提交
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user