对逆向工程插件优化代码提交

This commit is contained in:
张家训
2026-06-10 18:18:39 +08:00
parent 0248cca546
commit 9d41d068e9
7 changed files with 847 additions and 116 deletions
@@ -3,6 +3,7 @@ package com.example.workutils.biz.diff;
import com.example.workutils.system.config.WorkUtilsProperties; import com.example.workutils.system.config.WorkUtilsProperties;
import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.Plugin;
import com.example.workutils.system.plugin.PluginDescriptor; import com.example.workutils.system.plugin.PluginDescriptor;
import com.example.workutils.system.support.JfxBootstrap;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -51,60 +52,16 @@ public class DiffPlugin implements Plugin {
@Override @Override
public String[] getMenuOptions() { return MENU_OPTIONS; } public String[] getMenuOptions() { return MENU_OPTIONS; }
// ==================== JavaFX 初始化 ==================== // ==================== JavaFX 初始化(复用共享启动器) ====================
/**
* Spring Boot 默认开启 headless 模式,Swing 无法初始化。
* 这里用 JavaFX Application.launch 在后台线程启动 JavaFX 运行时,
* 后续通过 Platform.runLater 在 JavaFX 线程上创建窗口。
*/
private static final java.util.concurrent.CountDownLatch JFX_READY =
new java.util.concurrent.CountDownLatch(1);
private static volatile boolean jfxFailed;
static {
Thread jfxLauncher = new Thread(() -> {
try {
javafx.application.Platform.setImplicitExit(false);
javafx.application.Application.launch(JFXBootstrap.class);
} catch (Exception e) {
jfxFailed = true;
JFX_READY.countDown();
}
}, "JavaFX-Bootstrap");
jfxLauncher.setDaemon(true);
jfxLauncher.start();
}
/** 占位 Application,仅用于触发 JavaFX 工具包初始化 */
public static class JFXBootstrap extends javafx.application.Application {
@Override
public void start(javafx.stage.Stage stage) {
JFX_READY.countDown();
}
}
// ==================== 交互模式 ====================
@Override
public void handleChoice(int choice, Scanner scanner) throws Exception {
switch (choice) {
case 1: openVisualDiff(); break;
default: printInvalidOption(); break;
}
}
/**
* 启动 JavaFX 可视化对比窗口,阻塞直到窗口关闭
*/
private void openVisualDiff() { private void openVisualDiff() {
try { try {
JFX_READY.await(); JfxBootstrap.await();
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
return; return;
} }
if (jfxFailed) { if (JfxBootstrap.isFailed()) {
System.err.println("JavaFX 初始化失败,请确认 JDK 8 包含 JavaFX 运行时(OracleJDK 内置;OpenJDK 需安装 OpenJFX)"); System.err.println("JavaFX 初始化失败,请确认 JDK 8 包含 JavaFX 运行时(OracleJDK 内置;OpenJDK 需安装 OpenJFX)");
return; return;
} }
@@ -1,18 +1,21 @@
package com.example.workutils.biz.mybatisgenerator; package com.example.workutils.biz.mybatisgenerator;
import com.example.workutils.biz.mybatisgenerator.service.generator.MyBatisGeneratorService; import com.example.workutils.biz.mybatisgenerator.service.generator.MyBatisGeneratorService;
import com.example.workutils.system.config.WorkUtilsProperties;
import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.Plugin;
import com.example.workutils.system.plugin.PluginDescriptor; import com.example.workutils.system.plugin.PluginDescriptor;
import com.example.workutils.system.support.FileOperationHelper;
import com.example.workutils.system.support.JfxBootstrap;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Scanner; import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
/** /**
* MyBatis逆向工程插件(实现 Tool 接口) * MyBatis逆向工程插件
*/ */
@Slf4j @Slf4j
@Component @Component
@@ -28,82 +31,84 @@ public class MyBatisGeneratorPlugin implements Plugin {
.build(); .build();
private static final String[] MENU_OPTIONS = { private static final String[] MENU_OPTIONS = {
"1. 根据表名生成(单表)", "1. UI可视化操作(选择表+目录)",
"2. 根据表名生成(多表,逗号分隔)", "2. 查看数据库所有表",
"3. 根据表名通配符生成(如 user_%)", "3. 查看当前配置信息",
"4. 查看数据库所有表",
"5. 查看当前配置信息",
"0. 返回上级菜单" "0. 返回上级菜单"
}; };
private final MyBatisGeneratorService generatorService; private final MyBatisGeneratorService generatorService;
private final FileOperationHelper fileOperationHelper;
private final WorkUtilsProperties properties;
@Override @Override public PluginDescriptor getDescriptor() { return DESCRIPTOR; }
public PluginDescriptor getDescriptor() { @Override public String[] getMenuOptions() { return MENU_OPTIONS; }
return DESCRIPTOR;
}
@Override
public String[] getMenuOptions() {
return MENU_OPTIONS;
}
@Override @Override
public void handleChoice(int choice, Scanner scanner) throws Exception { public void handleChoice(int choice, Scanner scanner) throws Exception {
switch (choice) { switch (choice) {
case 1: generateBySingleTable(scanner); break; case 1: openVisualUI(); break;
case 2: generateByMultipleTables(scanner); break; case 2: showAllTables(); break;
case 3: generateByPattern(scanner); break; case 3: showConfiguration(); break;
case 4: showAllTables(); break;
case 5: showConfiguration(); break;
default: printInvalidOption(); break; default: printInvalidOption(); break;
} }
} }
private void generateBySingleTable(Scanner scanner) throws Exception { // ==================== UI 可视化操作 ====================
System.out.print("请输入表名: ");
String tableName = scanner.nextLine().trim();
if (tableName.isEmpty()) { System.out.println("表名不能为空"); return; }
log.info("开始生成表 [{}] 的Mapper文件", tableName);
String result = generatorService.generateByTableNames(Arrays.asList(tableName), true);
System.out.println(result);
printSuccess("生成完成");
}
private void generateByMultipleTables(Scanner scanner) throws Exception { private void openVisualUI() {
System.out.print("请输入表名(多个用逗号分隔): "); try {
String input = scanner.nextLine().trim(); JfxBootstrap.await();
if (input.isEmpty()) { System.out.println("表名不能为空"); return; } } catch (InterruptedException e) {
List<String> tableNames = Arrays.asList(input.split(",")); Thread.currentThread().interrupt();
for (int i = 0; i < tableNames.size(); i++) {
tableNames.set(i, tableNames.get(i).trim());
}
log.info("开始生成 {} 个表的Mapper文件", tableNames.size());
String result = generatorService.generateByTableNames(tableNames, true);
System.out.println(result);
printSuccess("生成完成");
}
private void generateByPattern(Scanner scanner) throws Exception {
System.out.print("请输入表名通配符(如 user_%): ");
String pattern = scanner.nextLine().trim();
if (pattern.isEmpty()) { System.out.println("通配符不能为空"); return; }
List<String> matchedTables = generatorService.queryTablesByPattern(pattern);
if (matchedTables.isEmpty()) {
System.out.println("未找到匹配通配符 [" + pattern + "] 的表");
return; return;
} }
System.out.println("匹配到 " + matchedTables.size() + " 张表:"); if (JfxBootstrap.isFailed()) {
for (int i = 0; i < matchedTables.size(); i++) { System.err.println("JavaFX 初始化失败,请确认 JDK 8 包含 JavaFX 运行时");
System.out.println(" " + (i + 1) + ". " + matchedTables.get(i)); return;
} }
System.out.print("是否继续生成?(y/n): ");
String confirm = scanner.nextLine().trim().toLowerCase(); System.out.println("正在打开 MyBatis 逆向工程窗口...");
if (!"y".equals(confirm)) { System.out.println("已取消生成"); return; }
log.info("开始根据通配符 [{}] 生成 {} 张表的Mapper文件", pattern, matchedTables.size()); CountDownLatch windowClose = new CountDownLatch(1);
String result = generatorService.generateByTableNames(matchedTables, true);
System.out.println(result); MyBatisGeneratorWindow.GeneratorCallback cb = new MyBatisGeneratorWindow.GeneratorCallback() {
printSuccess("生成完成"); @Override
public List<String> queryTables(String pattern) throws Exception {
return generatorService.queryTablesByPattern(pattern);
}
@Override
public String getDbUrl() {
return generatorService.getDatabaseUrl();
}
@Override
public String getDbUsername() {
return generatorService.getDatabaseUsername();
}
@Override
public String generate(List<String> tables, String modelDir, String xmlDir, String mapperDir) throws Exception {
return generatorService.generateByTableNames(tables, true, modelDir, xmlDir, mapperDir);
}
};
javafx.application.Platform.runLater(() -> {
try {
MyBatisGeneratorWindow window = new MyBatisGeneratorWindow(
cb, fileOperationHelper, properties, windowClose);
window.show();
} catch (Exception e) {
System.err.println("窗口创建失败: " + e.getMessage());
e.printStackTrace();
windowClose.countDown();
}
});
try {
windowClose.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("MyBatis 逆向工程窗口已关闭");
} }
private void showAllTables() throws Exception { private void showAllTables() throws Exception {
@@ -0,0 +1,547 @@
package com.example.workutils.biz.mybatisgenerator;
import com.example.workutils.system.config.WorkUtilsProperties;
import com.example.workutils.system.support.FileOperationHelper;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.scene.layout.*;
import javafx.scene.text.Font;
import javafx.stage.DirectoryChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* MyBatis 逆向工程 JavaFX 可视化窗口
* <p>
* 提供表名通配符查询、输出目录选择(支持从项目路径快速导航)、一键生成功能。
*/
public class MyBatisGeneratorWindow {
private final Stage stage;
private final CountDownLatch closeLatch;
private final FileOperationHelper fileOperationHelper;
private final GeneratorCallback callback;
// ---- UI 组件 ----
private TextField patternField;
private ListView<TableItem> tableListView;
private final ObservableList<TableItem> tableItems = FXCollections.observableArrayList();
private TextField modelDirField;
private TextField xmlDirField;
private TextField mapperDirField;
private TreeView<String> projectTree;
private ToggleGroup fillTargetGroup;
/** 表项(含复选框) */
public static class TableItem {
private final String tableName;
private final SimpleBooleanProperty selected;
public TableItem(String tableName, boolean selected) {
this.tableName = tableName;
this.selected = new SimpleBooleanProperty(selected);
}
public String getTableName() { return tableName; }
public ObservableValue<Boolean> selectedProperty() { return selected; }
public boolean isSelected() { return selected.get(); }
public void setSelected(boolean v) { selected.set(v); }
@Override
public String toString() { return tableName; }
}
/** 生成回调接口 */
public interface GeneratorCallback {
List<String> queryTables(String pattern) throws Exception;
String getDbUrl();
String getDbUsername();
String generate(List<String> tables, String modelDir, String xmlDir, String mapperDir) throws Exception;
}
public MyBatisGeneratorWindow(GeneratorCallback callback, FileOperationHelper fileOperationHelper,
WorkUtilsProperties properties, CountDownLatch closeLatch) {
this.callback = callback;
this.fileOperationHelper = fileOperationHelper;
this.closeLatch = closeLatch;
this.stage = new Stage();
buildUI();
}
public void show() {
stage.show();
}
// ==================== UI 构建 ====================
private void buildUI() {
stage.setTitle("MyBatis 逆向工程");
stage.initModality(Modality.NONE);
stage.setOnCloseRequest(e -> close());
VBox root = new VBox(10);
root.setPadding(new Insets(15));
root.setStyle("-fx-background-color: #f5f5f5;");
Label titleLabel = new Label("MyBatis 逆向工程");
titleLabel.setFont(Font.font("Microsoft YaHei", 18));
HBox dbBox = createDbInfoSection();
SplitPane splitPane = new SplitPane();
splitPane.setDividerPositions(0.28);
VBox leftPanel = createProjectTreePanel();
VBox rightPanel = createFormPanel();
splitPane.getItems().addAll(leftPanel, rightPanel);
VBox.setVgrow(splitPane, Priority.ALWAYS);
HBox btnBox = createButtonBar();
root.getChildren().addAll(titleLabel, dbBox, splitPane, btnBox);
Scene scene = new Scene(root, 960, 660);
stage.setScene(scene);
}
/** 根据选中的按钮返回对应的输出目录字段 */
private TextField resolveFillTarget() {
if (fillTargetGroup == null) return null;
Toggle selected = fillTargetGroup.getSelectedToggle();
if (selected == null) return null;
String type = (String) ((ToggleButton) selected).getUserData();
if ("model".equals(type)) return modelDirField;
if ("xml".equals(type)) return xmlDirField;
if ("mapper".equals(type)) return mapperDirField;
return null;
}
private ToggleButton createFillButton(String label, String userData) {
ToggleButton btn = new ToggleButton(label);
btn.setToggleGroup(fillTargetGroup);
btn.setUserData(userData);
btn.setPrefWidth(75);
btn.setStyle("-fx-font-size: 12px; -fx-cursor: hand;");
// 点击按钮时,将当前树节点路径填充到对应字段
btn.setOnAction(e -> {
TreeItem<String> selected = projectTree.getSelectionModel().getSelectedItem();
if (selected != null) {
String path = buildTreePath(selected);
if (path != null && !path.isEmpty()) {
TextField target = resolveFillTarget();
if (target != null) target.setText(path);
}
}
});
// 选中/未选中样式切换
fillTargetGroup.selectedToggleProperty().addListener((obs, old, cur) -> {
boolean active = (btn == cur);
btn.setStyle(active
? "-fx-background-color: #4a90d9; -fx-text-fill: white; -fx-font-size: 12px; "
+ "-fx-background-radius: 4; -fx-padding: 6 16;"
: "-fx-background-color: #e0e0e0; -fx-text-fill: #333; -fx-font-size: 12px; "
+ "-fx-background-radius: 4; -fx-padding: 6 16;");
});
// 初始化默认选中样式
btn.fire();
return btn;
}
private HBox createDbInfoSection() {
HBox box = new HBox(20);
box.setPadding(new Insets(8, 12, 8, 12));
box.setStyle("-fx-background-color: #e8f0fe; -fx-background-radius: 4;");
Label urlLabel = new Label("数据库: " + callback.getDbUrl());
urlLabel.setFont(Font.font("Consolas", 11));
Label userLabel = new Label("用户: " + callback.getDbUsername());
box.getChildren().addAll(urlLabel, userLabel);
return box;
}
// ==================== 左侧:项目路径树 ====================
private VBox createProjectTreePanel() {
VBox panel = new VBox(8);
panel.setPadding(new Insets(5));
Label treeTitle = new Label("项目路径 (快速定位)");
treeTitle.setFont(Font.font("Microsoft YaHei", 13));
projectTree = new TreeView<>();
projectTree.setRoot(new TreeItem<>("项目列表"));
projectTree.setShowRoot(false);
projectTree.setCellFactory(tv -> new TreeCell<String>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
int lastSep = Math.max(item.lastIndexOf('/'), item.lastIndexOf('\\'));
setText(lastSep >= 0 ? item.substring(lastSep + 1) : item);
}
}
});
loadProjectTree();
VBox.setVgrow(projectTree, Priority.ALWAYS);
// 填充目标选择(按钮组)
Label quickLabel = new Label("点击按钮填充到:");
quickLabel.setFont(Font.font("Microsoft YaHei", 11));
quickLabel.setPadding(new Insets(10, 0, 2, 0));
this.fillTargetGroup = new ToggleGroup();
ToggleButton btnModel = createFillButton("Model", "model");
btnModel.setSelected(true);
ToggleButton btnXml = createFillButton("XML", "xml");
ToggleButton btnMapper = createFillButton("Mapper", "mapper");
HBox rbBox = new HBox(8, btnModel, btnXml, btnMapper);
panel.getChildren().addAll(treeTitle, projectTree, quickLabel, rbBox);
return panel;
}
private String buildTreePath(TreeItem<String> item) {
if (item == null || item.getValue() == null) return null;
return item.getValue().replace('\\', '/');
}
private void loadProjectTree() {
TreeItem<String> root = projectTree.getRoot();
List<String> projectPaths;
try {
projectPaths = fileOperationHelper.getProjectPaths();
} catch (Exception e) {
projectPaths = Collections.emptyList();
}
if (projectPaths.isEmpty()) {
TreeItem<String> emptyItem = new TreeItem<>("(未配置项目路径)");
root.getChildren().add(emptyItem);
return;
}
for (String projPath : projectPaths) {
TreeItem<String> projNode = new TreeItem<>(projPath.replace('\\', '/'));
root.getChildren().add(projNode);
projNode.expandedProperty().addListener((obs, wasExpanded, isExpanded) -> {
if (isExpanded && !projNode.getChildren().isEmpty()
&& "...".equals(projNode.getChildren().get(0).getValue())) {
loadSubDirs(projNode, projPath);
}
});
projNode.getChildren().add(new TreeItem<>("..."));
}
}
private void loadSubDirs(TreeItem<String> parentNode, String parentPath) {
parentNode.getChildren().clear();
Path dir = Paths.get(parentPath);
if (!Files.isDirectory(dir)) return;
List<Path> subDirs = new ArrayList<>();
try {
Files.list(dir)
.filter(Files::isDirectory)
.filter(p -> !p.getFileName().toString().startsWith("."))
.sorted()
.forEach(subDirs::add);
} catch (IOException ignored) { return; }
for (Path sub : subDirs) {
String subPath = sub.toString().replace('\\', '/');
TreeItem<String> subNode = new TreeItem<>(subPath);
parentNode.getChildren().add(subNode);
boolean hasSubDirs;
try (Stream<Path> stream = Files.list(sub)) {
hasSubDirs = stream.anyMatch(p -> Files.isDirectory(p)
&& !p.getFileName().toString().startsWith("."));
} catch (IOException e) {
hasSubDirs = false;
}
if (hasSubDirs) {
subNode.getChildren().add(new TreeItem<>("..."));
}
subNode.expandedProperty().addListener((obs, wasExpanded, isExpanded) -> {
if (isExpanded && !subNode.getChildren().isEmpty()
&& "...".equals(subNode.getChildren().get(0).getValue())) {
loadSubDirs(subNode, subPath);
}
});
}
}
// ==================== 右侧:表单 ====================
private VBox createFormPanel() {
VBox panel = new VBox(10);
panel.setPadding(new Insets(5));
Label patternLabel = new Label("表名通配符:");
patternLabel.setFont(Font.font("Microsoft YaHei", 12));
HBox patternBox = new HBox(10);
patternField = new TextField();
patternField.setPromptText("如 trade%");
patternField.setPrefWidth(200);
Button queryBtn = new Button("查询表");
queryBtn.setStyle("-fx-background-color: #4a90d9; -fx-text-fill: white;");
queryBtn.setOnAction(e -> onQueryTables());
patternBox.getChildren().addAll(patternField, queryBtn);
Label tableLabel = new Label("匹配表 (勾选要生成的表):");
tableLabel.setFont(Font.font("Microsoft YaHei", 12));
tableListView = new ListView<>(tableItems);
tableListView.setPrefHeight(170);
tableListView.setCellFactory(CheckBoxListCell.forListView(TableItem::selectedProperty));
HBox selectBox = new HBox(10);
Button selectAllBtn = new Button("全选");
selectAllBtn.setOnAction(e -> tableItems.forEach(t -> t.setSelected(true)));
Button deselectAllBtn = new Button("取消全选");
deselectAllBtn.setOnAction(e -> tableItems.forEach(t -> t.setSelected(false)));
selectBox.getChildren().addAll(selectAllBtn, deselectAllBtn);
VBox dirBox = createDirectorySection();
panel.getChildren().addAll(patternLabel, patternBox, tableLabel, tableListView, selectBox, dirBox);
VBox.setVgrow(tableListView, Priority.ALWAYS);
return panel;
}
private VBox createDirectorySection() {
VBox box = new VBox(8);
box.setPadding(new Insets(10, 0, 0, 0));
Label dirLabel = new Label("输出目录设置:");
dirLabel.setFont(Font.font("Microsoft YaHei", 13));
HBox modelRow = createDirRow("Model (PO)", fm -> modelDirField = fm);
modelDirField = (TextField) modelRow.getChildren().get(1);
HBox xmlRow = createDirRow("XML Mapper", fm -> xmlDirField = fm);
xmlDirField = (TextField) xmlRow.getChildren().get(1);
HBox mapperRow = createDirRow("Mapper 接口", fm -> mapperDirField = fm);
mapperDirField = (TextField) mapperRow.getChildren().get(1);
loadDefaultDirs();
box.getChildren().addAll(dirLabel, modelRow, xmlRow, mapperRow);
return box;
}
private HBox createDirRow(String label, java.util.function.Consumer<TextField> fieldRef) {
HBox row = new HBox(8);
row.setAlignment(Pos.CENTER_LEFT);
Label lbl = new Label(label + ":");
lbl.setPrefWidth(100);
lbl.setFont(Font.font("Microsoft YaHei", 12));
TextField field = new TextField();
field.setPrefWidth(360);
HBox.setHgrow(field, Priority.ALWAYS);
Button browseBtn = new Button("浏览...");
browseBtn.setOnAction(e -> onBrowseDir(field));
if (fieldRef != null) fieldRef.accept(field);
row.getChildren().addAll(lbl, field, browseBtn);
return row;
}
private void loadDefaultDirs() {
Properties p = new Properties();
try (InputStream is = getClass().getClassLoader().getResourceAsStream("mybatis-generator.properties")) {
if (is != null) p.load(is);
} catch (Exception ignored) {}
setDirField(modelDirField, p.getProperty("model_target_project", "./generator"));
setDirField(xmlDirField, p.getProperty("sqlmapper_target_project", "./generator"));
setDirField(mapperDirField, p.getProperty("dao_target_project", "./generator"));
}
private void setDirField(TextField field, String dir) {
if (field == null || dir == null) return;
if (dir.startsWith("./") || dir.startsWith(".\\")) {
field.setText(new File(dir).getAbsolutePath().replace('\\', '/'));
} else {
field.setText(dir.replace('\\', '/'));
}
}
// ==================== 事件 ====================
private void onBrowseDir(TextField targetField) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle("选择目录");
String current = targetField.getText();
if (current != null && !current.isEmpty()) {
File initDir = new File(current);
if (initDir.exists() && initDir.isDirectory()) {
chooser.setInitialDirectory(initDir);
}
}
if (chooser.getInitialDirectory() == null) {
try {
List<String> paths = fileOperationHelper.getProjectPaths();
if (!paths.isEmpty()) {
File firstProj = new File(paths.get(0));
if (firstProj.exists() && firstProj.isDirectory()) {
chooser.setInitialDirectory(firstProj);
}
}
} catch (Exception ignored) {}
}
File selected = chooser.showDialog(stage);
if (selected != null) {
targetField.setText(selected.getAbsolutePath().replace('\\', '/'));
}
}
private void onQueryTables() {
String pattern = patternField.getText().trim();
if (pattern.isEmpty()) {
showAlert("请输入表名通配符");
return;
}
try {
List<String> tables = callback.queryTables(pattern);
tableItems.clear();
if (tables.isEmpty()) {
showAlert("未找到匹配通配符 [" + pattern + "] 的表");
} else {
for (String t : tables) {
tableItems.add(new TableItem(t, true));
}
}
} catch (Exception e) {
showAlert("查询失败: " + e.getMessage());
}
}
private void onGenerate() {
List<String> selectedTables = tableItems.stream()
.filter(TableItem::isSelected)
.map(TableItem::getTableName)
.collect(Collectors.toList());
if (selectedTables.isEmpty()) {
showAlert("请至少选择一个表");
return;
}
String modelDir = getDirText(modelDirField);
String xmlDir = getDirText(xmlDirField);
String mapperDir = getDirText(mapperDirField);
try {
String result = callback.generate(selectedTables, modelDir, xmlDir, mapperDir);
showInfo("生成结果", result);
} catch (Exception e) {
showAlert("生成失败: " + e.getMessage());
e.printStackTrace();
}
}
private String getDirText(TextField field) {
String text = field == null ? "" : field.getText().trim();
return text.isEmpty() ? "./generator" : text;
}
// ---- 底部按钮栏 ----
private HBox createButtonBar() {
HBox box = new HBox(15);
box.setAlignment(Pos.CENTER_RIGHT);
box.setPadding(new Insets(10, 0, 0, 0));
Button generateBtn = new Button("生成");
generateBtn.setPrefWidth(100);
generateBtn.setStyle("-fx-background-color: #4caf50; -fx-text-fill: white; -fx-font-size: 14px;");
generateBtn.setOnAction(e -> onGenerate());
Button cancelBtn = new Button("取消");
cancelBtn.setPrefWidth(80);
cancelBtn.setOnAction(e -> close());
box.getChildren().addAll(generateBtn, cancelBtn);
return box;
}
// ==================== 对话框 ====================
private void showAlert(String msg) {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("提示");
alert.setHeaderText(null);
alert.setContentText(msg);
alert.initOwner(stage);
alert.showAndWait();
});
}
private void showInfo(String title, String msg) {
Platform.runLater(() -> {
Stage dialog = new Stage();
dialog.initOwner(stage);
dialog.setTitle(title);
dialog.setResizable(true);
TextArea textArea = new TextArea(msg);
textArea.setEditable(false);
textArea.setWrapText(true);
textArea.setFont(Font.font("Consolas", 12));
Button closeBtn = new Button("关闭");
closeBtn.setOnAction(e -> dialog.close());
HBox btnBar = new HBox(closeBtn);
btnBar.setAlignment(Pos.CENTER_RIGHT);
btnBar.setPadding(new Insets(8));
btnBar.setStyle("-fx-background-color: #f0f0f0;");
VBox root = new VBox(textArea, btnBar);
VBox.setVgrow(textArea, Priority.ALWAYS);
Scene scene = new Scene(root, 580, 420);
dialog.setScene(scene);
dialog.showAndWait();
});
}
private void close() {
closeLatch.countDown();
stage.close();
}
}
@@ -33,7 +33,15 @@ public class MyBatisGeneratorService {
* @return 生成结果信息 * @return 生成结果信息
*/ */
public String generateByTableNames(List<String> tableNames, boolean overwrite) throws Exception { public String generateByTableNames(List<String> tableNames, boolean overwrite) throws Exception {
return generateInternal(tableNames, overwrite); return generateInternal(tableNames, overwrite, null, null, null);
}
/**
* 根据表名列表 + 自定义输出目录生成Mapper文件
*/
public String generateByTableNames(List<String> tableNames, boolean overwrite,
String modelTarget, String xmlTarget, String mapperTarget) throws Exception {
return generateInternal(tableNames, overwrite, modelTarget, xmlTarget, mapperTarget);
} }
/** /**
@@ -52,7 +60,7 @@ public class MyBatisGeneratorService {
} }
log.info("通配符 [{}] 匹配到 {} 张表: {}", pattern, matchedTables.size(), matchedTables); log.info("通配符 [{}] 匹配到 {} 张表: {}", pattern, matchedTables.size(), matchedTables);
return generateInternal(matchedTables, overwrite); return generateInternal(matchedTables, overwrite, null, null, null);
} }
/** /**
@@ -132,11 +140,15 @@ public class MyBatisGeneratorService {
/** /**
* 内部生成方法 * 内部生成方法
*/ */
private String generateInternal(List<String> tableNames, boolean overwrite) throws Exception { private String generateInternal(List<String> tableNames, boolean overwrite,
String modelTarget, String xmlTarget, String mapperTarget) throws Exception {
// 确保 targetProject 目录存在,避免 "does not exist" 警告 // 确保 targetProject 目录存在,避免 "does not exist" 警告
Properties props = getConfigurationProperties(); String[] dirs = {
for (String key : new String[]{"model_target_project", "sqlmapper_target_project", "dao_target_project"}) { modelTarget != null ? modelTarget : getConfigurationProperties().getProperty("model_target_project", "./generator"),
String dir = props.getProperty(key); xmlTarget != null ? xmlTarget : getConfigurationProperties().getProperty("sqlmapper_target_project", "./generator"),
mapperTarget != null ? mapperTarget : getConfigurationProperties().getProperty("dao_target_project", "./generator")
};
for (String dir : dirs) {
if (dir != null && !dir.isEmpty()) { if (dir != null && !dir.isEmpty()) {
File targetDir = new File(dir); File targetDir = new File(dir);
if (!targetDir.exists()) { if (!targetDir.exists()) {
@@ -146,7 +158,10 @@ public class MyBatisGeneratorService {
} }
} }
File tempConfigFile = createTempConfigWithTables(tableNames); File tempConfigFile = createTempConfigWithTables(tableNames,
modelTarget != null ? dirs[0] : null,
xmlTarget != null ? dirs[1] : null,
mapperTarget != null ? dirs[2] : null);
try { try {
List<String> warnings = new ArrayList<>(); List<String> warnings = new ArrayList<>();
@@ -180,7 +195,8 @@ public class MyBatisGeneratorService {
/** /**
* 创建临时配置文件 * 创建临时配置文件
*/ */
private File createTempConfigWithTables(List<String> tableNames) throws Exception { private File createTempConfigWithTables(List<String> tableNames,
String modelTarget, String xmlTarget, String mapperTarget) throws Exception {
InputStream is = getClass().getClassLoader().getResourceAsStream(CONFIG_FILE); InputStream is = getClass().getClassLoader().getResourceAsStream(CONFIG_FILE);
if (is == null) { if (is == null) {
throw new RuntimeException("无法找到配置文件: " + CONFIG_FILE); throw new RuntimeException("无法找到配置文件: " + CONFIG_FILE);
@@ -208,6 +224,65 @@ public class MyBatisGeneratorService {
); );
content = tablePattern.matcher(content).replaceAll(""); content = tablePattern.matcher(content).replaceAll("");
// 如果指定了自定义输出目录,覆盖 XML 中的 targetProject 和 targetPackage
// MBG 会在 targetProject 下按 targetPackage 创建子目录,所以:
// targetProject = src/main/java(或 src/main/resources)根目录
// targetPackage = 其后的包名路径
String modelPkg = null;
String mapperPkg = null;
if (modelTarget != null) {
String[] resolved = resolveTargetAndPackage(modelTarget, "src/main/java/");
if (resolved != null) {
content = content.replaceFirst(
"(<javaModelGenerator[^>]*targetProject=\")[^\"]*(\")",
"$1" + java.util.regex.Matcher.quoteReplacement(resolved[0]) + "$2");
modelPkg = resolved[1];
content = content.replaceFirst(
"(<javaModelGenerator[^>]*targetPackage=\")[^\"]*(\")",
"$1" + java.util.regex.Matcher.quoteReplacement(modelPkg) + "$2");
} else {
// 路径中不含 src/main/java,回退为仅替换 targetProject
content = content.replaceFirst(
"(<javaModelGenerator[^>]*targetProject=\")[^\"]*(\")",
"$1" + java.util.regex.Matcher.quoteReplacement(modelTarget.replace('\\', '/')) + "$2");
}
}
if (mapperTarget != null) {
String[] resolved = resolveTargetAndPackage(mapperTarget, "src/main/java/");
if (resolved != null) {
content = content.replaceFirst(
"(<javaClientGenerator[^>]*targetProject=\")[^\"]*(\")",
"$1" + java.util.regex.Matcher.quoteReplacement(resolved[0]) + "$2");
mapperPkg = resolved[1];
content = content.replaceFirst(
"(<javaClientGenerator[^>]*targetPackage=\")[^\"]*(\")",
"$1" + java.util.regex.Matcher.quoteReplacement(mapperPkg) + "$2");
} else {
content = content.replaceFirst(
"(<javaClientGenerator[^>]*targetProject=\")[^\"]*(\")",
"$1" + java.util.regex.Matcher.quoteReplacement(mapperTarget.replace('\\', '/')) + "$2");
}
}
if (xmlTarget != null) {
String[] resolved = resolveTargetAndPackage(xmlTarget, "src/main/resources/");
if (resolved != null) {
content = content.replaceFirst(
"(<sqlMapGenerator[^>]*targetProject=\")[^\"]*(\")",
"$1" + java.util.regex.Matcher.quoteReplacement(resolved[0]) + "$2");
} else {
content = content.replaceFirst(
"(<sqlMapGenerator[^>]*targetProject=\")[^\"]*(\")",
"$1" + java.util.regex.Matcher.quoteReplacement(xmlTarget.replace('\\', '/')) + "$2");
}
}
// sqlmap 的 targetPackage(即 XML namespace)应与 Mapper 接口包名一致
if (mapperPkg != null) {
content = content.replaceFirst(
"(<sqlMapGenerator[^>]*targetPackage=\")[^\"]*(\")",
"$1" + java.util.regex.Matcher.quoteReplacement(mapperPkg) + "$2");
}
StringBuilder tableElements = new StringBuilder(); StringBuilder tableElements = new StringBuilder();
for (String tableName : tableNames) { for (String tableName : tableNames) {
tableElements.append(" <table schema=\"\" tableName=\"") tableElements.append(" <table schema=\"\" tableName=\"")
@@ -231,6 +306,35 @@ public class MyBatisGeneratorService {
return tempFile; return tempFile;
} }
/**
* 从目录路径中解析出 targetProject 和 targetPackage
* MBG 会在 targetProject 下按 targetPackage 创建子目录,
* 所以需要将路径拆分:
* 例如: D:/project/src/main/java/com/example/model
* → targetProject = D:/project/src/main/java
* → targetPackage = com.example.model
*
* @param dirPath 用户选择的目录
* @param sourceRoot 标记(如 "src/main/java/" 或 "src/main/resources/")
* @return [targetProject, targetPackage],找不到标记则返回 null
*/
private String[] resolveTargetAndPackage(String dirPath, String sourceRoot) {
if (dirPath == null) return null;
String normalized = dirPath.replace('\\', '/');
int idx = normalized.indexOf(sourceRoot);
if (idx < 0) return null;
// targetProject = .../src/main/java (包含 sourceRoot 本身)
String projectRoot = normalized.substring(0, idx + sourceRoot.length());
// targetPackage = src/main/java 之后的部分,转为包名
String packagePath = normalized.substring(idx + sourceRoot.length());
if (packagePath.endsWith("/")) {
packagePath = packagePath.substring(0, packagePath.length() - 1);
}
// 如果包名为空(用户恰好选到了 src/main/java),返回空包名
String packageName = packagePath.isEmpty() ? "" : packagePath.replace('/', '.');
return new String[]{projectRoot, packageName};
}
/** /**
* 根据表名生成实体类名 * 根据表名生成实体类名
*/ */
@@ -0,0 +1,55 @@
package com.example.workutils.biz.mybatisgenerator.service.generator;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import java.util.List;
import java.util.Properties;
/**
* 自定义插件:抑制 *Key 类的生成,将主键字段直接合并到 PO 类中。
* <p>
* MBG 默认对含复合主键的表生成两个类:
* <ul>
* <li>XXXPO(完整 PO,含所有字段)</li>
* <li>XXXPOKey(仅主键字段)</li>
* </ul>
* 本插件在初始化时将 {@code hasPrimaryKeyColumns} 重置为 false,
* 使 MBG 认为该表没有需要单独提取的主键类,
* 从而只生成一个包含所有字段的 PO 类。
*/
public class SuppressKeyPlugin extends PluginAdapter {
private boolean suppressKey;
@Override
public void setProperties(Properties properties) {
super.setProperties(properties);
suppressKey = !"false".equalsIgnoreCase(properties.getProperty("suppressKey"));
}
@Override
public boolean validate(List<String> warnings) {
return true;
}
/**
* 在表内省完成后,清除主键列标记。
* MBG 仅当表含复合主键且 {@code getHasPrimaryKeyColumns()==true}
* 时才会生成独立的 *Key 类;置为 false 后跳过 Key 类生成。
*/
@Override
public void initialized(IntrospectedTable introspectedTable) {
if (!suppressKey) return;
// 通过反射将 hasInternalPrimaryKey 强制设为 false,
// 阻止 MBG 生成单独的 PrimaryKey 类
try {
java.lang.reflect.Field field = introspectedTable.getClass().getDeclaredField("hasInternalPrimaryKey");
field.setAccessible(true);
field.set(introspectedTable, false);
} catch (Exception e) {
// 反射失败时静默忽略,回退到默认行为
}
}
}
@@ -0,0 +1,61 @@
package com.example.workutils.system.support;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import java.util.concurrent.CountDownLatch;
/**
* 共享的 JavaFX 启动器 —— 全局仅初始化一次 JavaFX 运行时
* <p>
* 由于 {@link Application#launch} 每个 JVM 只能调用一次,
* 所有需要 JavaFX 的插件复用此启动器,避免重复启动。
*/
public final class JfxBootstrap {
private static final CountDownLatch READY = new CountDownLatch(1);
private static volatile boolean failed;
private static volatile boolean launched;
private JfxBootstrap() {}
static {
launchOnce();
}
private static synchronized void launchOnce() {
if (launched) return;
launched = true;
Thread t = new Thread(() -> {
try {
Platform.setImplicitExit(false);
Application.launch(DummyApp.class);
} catch (Exception e) {
failed = true;
} finally {
READY.countDown();
}
}, "JavaFX-Bootstrap");
t.setDaemon(true);
t.start();
}
/** 阻塞等待 JavaFX 运行时就绪 */
public static void await() throws InterruptedException {
READY.await();
}
/** JavaFX 是否初始化失败 */
public static boolean isFailed() {
return failed;
}
/** 占位 Application,仅用于触发 JavaFX 工具包初始化 */
public static class DummyApp extends Application {
@Override
public void start(Stage stage) {
READY.countDown();
}
}
}
+2
View File
@@ -13,6 +13,8 @@
</plugin> </plugin>
<!-- 只保留Selective版本的方法 --> <!-- 只保留Selective版本的方法 -->
<plugin type="com.example.workutils.biz.mybatisgenerator.service.generator.SelectiveOnlyPlugin" /> <plugin type="com.example.workutils.biz.mybatisgenerator.service.generator.SelectiveOnlyPlugin" />
<!-- 抑制 *Key 类生成,主键字段合并到 PO 类 -->
<plugin type="com.example.workutils.biz.mybatisgenerator.service.generator.SuppressKeyPlugin" />
<!-- 使用Lombok @Data注解,不生成getter/setter --> <!-- 使用Lombok @Data注解,不生成getter/setter -->
<plugin type="com.example.workutils.biz.mybatisgenerator.service.generator.LombokPlugin" /> <plugin type="com.example.workutils.biz.mybatisgenerator.service.generator.LombokPlugin" />