对逆向工程插件优化代码提交
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-61
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+112
-8
@@ -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};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据表名生成实体类名
|
* 根据表名生成实体类名
|
||||||
*/
|
*/
|
||||||
|
|||||||
+55
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user