From 3f5e6e7ad01faf978f6478b063a45777fd610dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Mon, 29 Jun 2026 10:40:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../biz/apitrimmer/ApiTrimmerWindow.java | 402 +++++++----------- .../apitrimmer/service/ApiTrimmerService.java | 77 ++-- 2 files changed, 183 insertions(+), 296 deletions(-) diff --git a/src/main/java/com/example/workutils/biz/apitrimmer/ApiTrimmerWindow.java b/src/main/java/com/example/workutils/biz/apitrimmer/ApiTrimmerWindow.java index 039bdb3..7a2c463 100644 --- a/src/main/java/com/example/workutils/biz/apitrimmer/ApiTrimmerWindow.java +++ b/src/main/java/com/example/workutils/biz/apitrimmer/ApiTrimmerWindow.java @@ -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 dirTree; private TreeItem treeRoot; @@ -67,16 +67,9 @@ public class ApiTrimmerWindow { private TreeItem candidatePopupRoot; private final ObservableList 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 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 iconCol = treeIconColumn("图标", 42); - TreeTableColumn nameCol = treeNameColumn("名称", 300); - TreeTableColumn fileTypeCol = treeTextColumn("文件类型", 92, DirRow::fileTypeProperty); - TreeTableColumn riskCol = treeTextColumn("风险", 70, DirRow::riskProperty); - TreeTableColumn categoryCol = treeTextColumn("删除类型", 120, DirRow::categoryProperty); - TreeTableColumn coverageCol = treeTextColumn("覆盖", 78, DirRow::coverageProperty); - TreeTableColumn sizeCol = treeTextColumn("大小", 86, DirRow::sizeProperty); + TreeTableColumn nameCol = treeNameColumn("名称", 280); + TreeTableColumn fileTypeCol = treeTextColumn("类型", 80, DirRow::fileTypeProperty); + TreeTableColumn sizeCol = treeTextColumn("大小", 72, DirRow::sizeProperty); sizeCol.setStyle("-fx-alignment: CENTER-RIGHT;"); - TreeTableColumn countCol = treeTextColumn("文件/类", 88, DirRow::countProperty); - countCol.setStyle("-fx-alignment: CENTER-RIGHT;"); - TreeTableColumn reasonCol = treeTextColumn("为什么可删除", 360, DirRow::reasonProperty); - TreeTableColumn rpcCol = treeTextColumn("RPC/Dubbo 安全说明", 440, DirRow::rpcSafetyProperty); + TreeTableColumn coverageCol = treeTextColumn("覆盖", 50, DirRow::coverageProperty); + TreeTableColumn categoryCol = treeTextColumn("来源", 96, DirRow::categoryProperty); + TreeTableColumn riskCol = treeTextColumn("风险", 55, DirRow::riskProperty); - tree.getColumns().setAll(selCol, iconCol, nameCol, fileTypeCol, riskCol, categoryCol, - coverageCol, sizeCol, countCol, reasonCol, rpcCol); + // 所有列可拖拽调整宽度 + for (TreeTableColumn c : List.of(nameCol, fileTypeCol, sizeCol, + coverageCol, categoryCol, riskCol)) { + c.setResizable(true); + } + + tree.getColumns().setAll(selCol, nameCol, fileTypeCol, sizeCol, coverageCol, + categoryCol, riskCol); tree.setTreeColumn(nameCol); + tree.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); tree.setRowFactory(table -> new TreeTableRow<>() { @Override protected void updateItem(DirRow item, boolean empty) { @@ -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 treeIconColumn(String title, int width) { - TreeTableColumn 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 treeTextColumn(String title, int width, java.util.function.Function valueFactory) { TreeTableColumn 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 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 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 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 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 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 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 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 byPath, Set 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 item, boolean expanded) { item.setExpanded(expanded); for (TreeItem child : item.getChildren()) { @@ -1185,29 +1123,6 @@ public class ApiTrimmerWindow { } } - private void updateSelectedPathsArea(Set 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 rawSelectedPrefixes() { Set 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()); diff --git a/src/main/java/com/example/workutils/biz/apitrimmer/service/ApiTrimmerService.java b/src/main/java/com/example/workutils/biz/apitrimmer/service/ApiTrimmerService.java index 52c391a..88179a2 100644 --- a/src/main/java/com/example/workutils/biz/apitrimmer/service/ApiTrimmerService.java +++ b/src/main/java/com/example/workutils/biz/apitrimmer/service/ApiTrimmerService.java @@ -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 entries) {