前言
在软件开发的世界中,自动化工具的使用可以极大地提高我们的工作效率。特别是在数据库操作方面,手动编写SQL语句和实体类、映射文件等不仅耗时,而且容易出错。MyBatis作为一款广泛使用的持久层框架,其代码生成器mybatis-generator可以帮助我们自动生成这些繁琐的代码。然而,mybatis-generator默认只支持MySQL数据库,对于其他数据库(如Oracle、PostgreSQL等)的支持并不完善。因此,本文将通过分析现有的开源项目mybatis-generator-gui,来探索如何实现一个支持多种数据库的MyBatis代码生成器。
本系列博客将分为以下几个部分:
- mybatis-generator-gui 使用介绍:首先,我们将介绍如何使用现有的开源项目mybatis-generator-gui进行可视化代码生成。我们将详细讲解如何配置项目,选择数据库,以及生成代码的过程。
- mybatis 代码生成原理探究:在这一部分,我们将深入mybatis-generator的内部,探究其代码生成的原理。我们将分析其配置文件的解析过程,以及如何根据配置信息生成对应的实体类、映射文件和XML配置文件。
- 自定义代码生成器实现:在前两部分的基础上,我们将实现自己的代码生成器。我们将重写mybatis-generator的核心代码,使其支持其他数据库。同时,我们也将优化生成的代码,使其更符合我们的项目需求。
通过本系列博客的学习,你将能够掌握MyBatis代码生成器的使用方法和原理,同时也能够根据自己的需求定制代码生成器。无论你是MyBatis初学者,还是有一定经验的开发者,我相信这都将对你的学习和工作有所帮助。让我们一起开始这个旅程吧!
其他文章
Mybatis代码生成系列(一)- mybatis-generator-gui开源项目介绍 - 掘金 (juejin.cn)
Mybatis代码生成系列(二) - mybatis-generator-gui使用原理 - 掘金 (juejin.cn)
正文
本文是对GUI页面的一些适配修改,主要新增如下功能:
- 支持配置service和controller生成代码的包名称和目录地址
- 支持配置的导入导出,主要是方便工具的使用,使用者可以直接根据配置生成代码,0成本配置;
FXML
属性
PreHeight
控件的初始高度,如果其值超过maxHeight,则控件的初始高度为maxHeight
主页面Service和Controller配置
修改后效果图:
主要是修改如下文件: 首先新增如下行,新增的行分别在第8和第9行;同时在rowContraints中新增两个RowConstrants(一定要新增这个行限制,否者页面布局会乱)
xml
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES"/>
xml
<Label text="Service包名" GridPane.rowIndex="7"/>
<TextField fx:id="serviceTargetPackage" prefHeight="27.0" prefWidth="248.0"
promptText="com.example.service" GridPane.columnIndex="1"
GridPane.rowIndex="7">
<HBox.margin>
<Insets right="5.0"/>
</HBox.margin>
<GridPane.margin>
<Insets left="5.0" right="5.0"/>
</GridPane.margin>
</TextField>
<Label text="存放目录" GridPane.columnIndex="2" GridPane.rowIndex="7"/>
<TextField fx:id="serviceTargetDirectory" prefHeight="27.0" prefWidth="155.0"
promptText="src/main/java" text="src/main/java"
GridPane.columnIndex="3" GridPane.columnSpan="2"
GridPane.rowIndex="7">
<GridPane.margin>
<Insets left="5.0"/>
</GridPane.margin>
</TextField>
<Label text="Controller包名" GridPane.rowIndex="8"/>
<TextField fx:id="controllerTargetPackage" prefHeight="27.0" prefWidth="248.0"
promptText="com.example.controller" GridPane.columnIndex="1"
GridPane.rowIndex="8">
<HBox.margin>
<Insets right="5.0"/>
</HBox.margin>
<GridPane.margin>
<Insets left="5.0" right="5.0"/>
</GridPane.margin>
</TextField>
<Label text="存放目录" GridPane.columnIndex="2" GridPane.rowIndex="8"/>
<TextField fx:id="controllerTargetDirectory" prefHeight="27.0" prefWidth="155.0"
promptText="src/main/java" text="src/main/java"
GridPane.columnIndex="3" GridPane.columnSpan="2"
GridPane.rowIndex="8">
<GridPane.margin>
<Insets left="5.0"/>
</GridPane.margin>
</TextField>
<Label prefHeight="27.0" prefWidth="99.0" text="映射XML文件包名"
GridPane.rowIndex="9"/>
<TextField fx:id="mapperTargetPackage" prefHeight="27.0" prefWidth="248.0"
promptText="com.example" GridPane.columnIndex="1"
GridPane.rowIndex="9">
<HBox.margin>
<Insets right="5.0"/>
</HBox.margin>
<GridPane.margin>
<Insets left="5.0" right="5.0"/>
</GridPane.margin>
</TextField>
<Label text="存放目录" GridPane.columnIndex="2" GridPane.rowIndex="9"/>
<TextField fx:id="mappingTargetProject" prefHeight="27.0" prefWidth="155.0"
promptText="src/main/resources" text="src/main/resources"
GridPane.columnIndex="3" GridPane.columnSpan="2"
GridPane.rowIndex="9">
<GridPane.margin>
<Insets left="5.0"/>
</GridPane.margin>
</TextField>
GeneratorConfig
由于页面属性增多,需要修改对应的实体类Generator
java
// service包名
private String serviceTargetPackage;
// service类存放目录
private String serviceTargetDirectory;
// controller包名
private String controllerTargetPackage;
// controller存放目录
private String controllerTargetDirectory;
并且插入基本的set和get方法
java
public String getServiceTargetPackage() {
return serviceTargetPackage;
}
public void setServiceTargetPackage(String serviceTargetPackage) {
this.serviceTargetPackage = serviceTargetPackage;
}
public String getServiceTargetDirectory() {
return serviceTargetDirectory;
}
public void setServiceTargetDirectory(String serviceTargetDirectory) {
this.serviceTargetDirectory = serviceTargetDirectory;
}
public String getControllerTargetPackage() {
return controllerTargetPackage;
}
public void setControllerTargetPackage(String controllerTargetPackage) {
this.controllerTargetPackage = controllerTargetPackage;
}
public String getControllerTargetDirectory() {
return controllerTargetDirectory;
}
public void setControllerTargetDirectory(String controllerTargetDirectory) {
this.controllerTargetDirectory = controllerTargetDirectory;
}
MainUIController
新增对应是个TextField控件绑定的在控制器中的属性:
java
@FXML
private TextField serviceTargetPackage;
@FXML
private TextField serviceTargetDirectory;
@FXML
private TextField controllerTargetPackage;
@FXML
private TextField controllerTargetDirectory;
同时,在主要面点击保存配置和在配置页面点击应用配置时,新增的service和controller属性需要设置和加载,修改对应的方法:
java
public GeneratorConfig getGeneratorConfigFromUI() {
GeneratorConfig generatorConfig = new GeneratorConfig();
generatorConfig.setProjectFolder(projectFolderField.getText());
generatorConfig.setModelPackage(modelTargetPackage.getText());
generatorConfig.setGenerateKeys(generateKeysField.getText());
generatorConfig.setModelPackageTargetFolder(modelTargetProject.getText());
generatorConfig.setDaoPackage(daoTargetPackage.getText());
generatorConfig.setDaoTargetFolder(daoTargetProject.getText());
generatorConfig.setMapperName(mapperName.getText());
generatorConfig.setMappingXMLPackage(mapperTargetPackage.getText());
generatorConfig.setMappingXMLTargetFolder(mappingTargetProject.getText());
generatorConfig.setTableName(tableNameField.getText());
generatorConfig.setDomainObjectName(domainObjectNameField.getText());
generatorConfig.setOffsetLimit(offsetLimitCheckBox.isSelected());
generatorConfig.setComment(commentCheckBox.isSelected());
generatorConfig.setOverrideXML(overrideXML.isSelected());
generatorConfig.setNeedToStringHashcodeEquals(needToStringHashcodeEquals.isSelected());
generatorConfig.setUseLombokPlugin(useLombokPlugin.isSelected());
generatorConfig.setUseTableNameAlias(useTableNameAliasCheckbox.isSelected());
generatorConfig.setNeedForUpdate(forUpdateCheckBox.isSelected());
generatorConfig.setAnnotationDAO(annotationDAOCheckBox.isSelected());
generatorConfig.setAnnotation(annotationCheckBox.isSelected());
generatorConfig.setUseActualColumnNames(useActualColumnNamesCheckbox.isSelected());
generatorConfig.setEncoding(encodingChoice.getValue());
generatorConfig.setUseExample(useExample.isSelected());
generatorConfig.setUseDAOExtendStyle(useDAOExtendStyle.isSelected());
generatorConfig.setUseSchemaPrefix(useSchemaPrefix.isSelected());
generatorConfig.setJsr310Support(jsr310Support.isSelected());
generatorConfig.setServiceTargetDirectory(serviceTargetDirectory.getText());
generatorConfig.setServiceTargetPackage(serviceTargetPackage.getText());
generatorConfig.setControllerTargetDirectory(controllerTargetDirectory.getText());
generatorConfig.setControllerTargetPackage(controllerTargetPackage.getText());
return generatorConfig;
}
public void setGeneratorConfigIntoUI(GeneratorConfig generatorConfig) {
projectFolderField.setText(generatorConfig.getProjectFolder());
modelTargetPackage.setText(generatorConfig.getModelPackage());
generateKeysField.setText(generatorConfig.getGenerateKeys());
modelTargetProject.setText(generatorConfig.getModelPackageTargetFolder());
daoTargetPackage.setText(generatorConfig.getDaoPackage());
daoTargetProject.setText(generatorConfig.getDaoTargetFolder());
mapperTargetPackage.setText(generatorConfig.getMappingXMLPackage());
mappingTargetProject.setText(generatorConfig.getMappingXMLTargetFolder());
if (StringUtils.isBlank(tableNameField.getText())) {
tableNameField.setText(generatorConfig.getTableName());
mapperName.setText(generatorConfig.getMapperName());
domainObjectNameField.setText(generatorConfig.getDomainObjectName());
}
offsetLimitCheckBox.setSelected(generatorConfig.isOffsetLimit());
commentCheckBox.setSelected(generatorConfig.isComment());
overrideXML.setSelected(generatorConfig.isOverrideXML());
needToStringHashcodeEquals.setSelected(generatorConfig.isNeedToStringHashcodeEquals());
useLombokPlugin.setSelected(generatorConfig.isUseLombokPlugin());
useTableNameAliasCheckbox.setSelected(generatorConfig.getUseTableNameAlias());
forUpdateCheckBox.setSelected(generatorConfig.isNeedForUpdate());
annotationDAOCheckBox.setSelected(generatorConfig.isAnnotationDAO());
annotationCheckBox.setSelected(generatorConfig.isAnnotation());
useActualColumnNamesCheckbox.setSelected(generatorConfig.isUseActualColumnNames());
encodingChoice.setValue(generatorConfig.getEncoding());
useExample.setSelected(generatorConfig.isUseExample());
useDAOExtendStyle.setSelected(generatorConfig.isUseDAOExtendStyle());
useSchemaPrefix.setSelected(generatorConfig.isUseSchemaPrefix());
jsr310Support.setSelected(generatorConfig.isJsr310Support());
serviceTargetPackage.setText(generatorConfig.getServiceTargetPackage());
serviceTargetDirectory.setText(generatorConfig.getServiceTargetDirectory());
controllerTargetDirectory.setText(generatorConfig.getControllerTargetDirectory());
controllerTargetPackage.setText(generatorConfig.getControllerTargetPackage());
}
MybatisGeneratorBridge
为了让最后页面的service和controller参数在生成代码生效,还需要将参数传入插件:
java
// 生成service和controller插件
PluginConfiguration pluginConfiguration = new PluginConfiguration();
pluginConfiguration.addProperty("service", "com.zzg.mybatis.generator.plugins.ServiceAndControllerGeneratorPlugin");
pluginConfiguration.addProperty("targetProject", generatorConfig.getProjectFolder() + "/" + generatorConfig.getServiceTargetDirectory());
pluginConfiguration.addProperty("servicePackage", generatorConfig.getServiceTargetPackage());
pluginConfiguration.addProperty("serviceImplPackage", generatorConfig.getControllerTargetDirectory());
pluginConfiguration.addProperty("controllerPackage", generatorConfig.getControllerTargetPackage());
pluginConfiguration.addProperty("serviceSuffix", "Service");
pluginConfiguration.addProperty("serviceImplSuffix", "ServiceImpl");
pluginConfiguration.addProperty("controllerSuffix", "Controller");
pluginConfiguration.setConfigurationType("com.zzg.mybatis.generator.plugins.ServiceAndControllerGeneratorPlugin");
context.addPluginConfiguration(pluginConfiguration);
配置页面导入导出
修改后效果:
主要修改文件如下:
generatorConfigs
新增导入配置按钮:
xml
<Button mnemonicParsing="false" onAction="#importGeneratorConfig"
text="导入配置"
AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="350.0">
<styleClass>
<String fx:value="btn"/>
<String fx:value="btn-default"/>
</styleClass>
</Button>
绑定importGeneratorConfig9(在GeneratorConfigController中)方法:
java
@FXML
public void importGeneratorConfig() {
FileChooser fileChooser = new FileChooser();
//设置文件上传类型
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("json files (*.json)", "*.json");
fileChooser.getExtensionFilters().add(extFilter);
File file = fileChooser.showOpenDialog(this.mainUIController.getPrimaryStage());
StringBuilder config = new StringBuilder();
String content = "";
GeneratorConfig generatorConfig = null;
try {
BufferedReader bufferedReader = new BufferedReader(new FileReader( file));
while ((content = bufferedReader.readLine()) != null) {
config.append(content);
}
generatorConfig = JSONObject.parseObject(config.toString(), GeneratorConfig.class);
// 设置文件名为导入配置文件的默认名
ConfigHelper.deleteGeneratorConfig(file.getName());
generatorConfig.setName(file.getName().split(".json")[0]);
ConfigHelper.saveGeneratorConfig(generatorConfig);
// 导入成功后,刷新页面
refreshTableView();
} catch (FileNotFoundException e) {
_LOG.error("导入配置失败: 配置文件不存在");
} catch (IOException e) {
_LOG.error("导入配置失败: 读取配置文件失败", e);
} catch (Exception e) {
_LOG.error("导入配置失败:保存配置失败", e);
}
}
GeneratorConfigController
新增导出配置按钮,修改对应的而初始化方法:
java
@Override
public void initialize(URL location, ResourceBundle resources) {
controller = this;
nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
// 自定义操作列
opsColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
opsColumn.setCellFactory(cell -> {
return new TableCell() {
@Override
protected void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
} else {
Button btn1 = new Button("应用");
Button btn2 = new Button("删除");
Button btn3 = new Button("导出");
HBox hBox = new HBox();
hBox.setSpacing(10);
hBox.getChildren().add(btn1);
hBox.getChildren().add(btn2);
hBox.getChildren().add(btn3);
btn1.setOnAction(event -> {
try {
// 应用配置
GeneratorConfig generatorConfig = ConfigHelper.loadGeneratorConfig(item.toString());
mainUIController.setGeneratorConfigIntoUI(generatorConfig);
controller.closeDialogStage();
} catch (Exception e) {
AlertUtil.showErrorAlert(e.getMessage());
}
});
btn2.setOnAction(event -> {
try {
// 删除配置
_LOG.debug("item: {}", item);
ConfigHelper.deleteGeneratorConfig(item.toString());
refreshTableView();
} catch (Exception e) {
AlertUtil.showErrorAlert(e.getMessage());
}
});
btn3.setOnAction(event -> {
try {
_LOG.info("导出配置: {}", item);
ConfigHelper.exportGeneratorConfig(item.toString(), mainUIController.getPrimaryStage());
// 导入成功后,刷新组件
refreshTableView();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
setGraphic(hBox);
}
}
};
});
refreshTableView();
}
ConfigHelper
在该工具类中新增导出方法:
java
public static void exportGeneratorConfig(String name, Stage primaryStage) {
try {
GeneratorConfig generatorConfig = loadGeneratorConfig(name);
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle("请选择导出配置文件的目录");
directoryChooser.setInitialDirectory(new File(generatorConfig.getProjectFolder()));
File file = directoryChooser.showDialog(primaryStage);
// 设置配置名称为默认配置导出文件名
File exportFile = new File(file.getAbsolutePath().concat("\").concat(name).concat(".json"));
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(exportFile));
bufferedWriter.write(JSONObject.toJSONString(generatorConfig));
bufferedWriter.close();
} catch (Exception e) {
_LOG.error("导出配置失败: 数据库加载配置失败");
}
}
小结
本文只是简单的通过修改源代码从而适配GUI的使用;
后续使用的话,还想新增生成自定义类文件注释,这个就需要深入了解mybatis生成器的配置和原理,后续的文章会着重分析mybatis生成代码的原理;
参考
TextField(文本框) - 《JavaFX 教程中文翻译》 - 极客文档 (geekdaxue.co) JavaFX教程 (yiibai.com)