前言
在软件开发的世界中,自动化工具的使用可以极大地提高我们的工作效率。特别是在数据库操作方面,手动编写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)
正文
本文会简要分析一下生成代码的源码,并且修改源码从而使得生成的代码满足我的要求:
-
Mapper包含基本insert、delete、update和select
-
生成的mapper、model类文件需要添加上类注释 --未实现,后面完善
-
生成service代码
先验知识
JavaFX
mybatis-generator-gui是一个JavaFX项目,我们可以在idea中直接新建一个,对比一下,这样我们就大概了解源码的文件代码结构了
其中resources下面的fxml文件就是定义gui页面的源文件,style.css就是页面的样式源码, 在fxml通过下面这种方式引用
Controller文件夹下面的控制层类是用户处理鼠标和键盘输入的控制器文件
MainUI类就是启动类,其继承了javafx.application.Application, 通过@FXML注解关联fxml中的变量和响应事件。
代码生成
源码解析
MainUI中代码生成对应的响应事件是:generateCode
分析下con troller下对应的generateCode方法:
主要是用于生成代码的过程。让我来梳理一下主要逻辑:
- 首先进行页面控制,如果tableName为空,则显示一个警告对话框,提示用户在左侧选择数据库表,然后返回。
- 进行参数校验,如果validateConfig返回的结果不为空,则显示一个错误对话框,提示用户出现了错误,然后返回。
- 获取用户在UI上的选择和输入的配置信息,然后检查目录是否存在,如果不存在,则返回。
- 创建一个MybatisGeneratorBridge对象,并设置相关的配置信息。
- 创建一个UIProgressCallback对象,用于在代码生成过程中显示进度和状态的对话框。
- 将UIProgressCallback对象设置为MybatisGeneratorBridge的进度回调。
- 调用UIProgressCallback的show方法,显示一个非阻塞的对话框,不会等待用户响应。
- 进行一系列操作,包括SSH会话的建立、端口转发的处理、生成代码等。
- 在异常处理中,如果出现异常,则打印异常信息并显示一个错误对话框,然后关闭图片处理状态控制器,并显示相应的错误状态。 总的来说,这段代码主要是用于生成代码的过程,并在过程中显示对话框、处理异常等。同时也涉及到了一些并发处理,比如使用Task来延迟关闭图片处理状态控制器。
其中的核心就是8, 生成代码操作:
它包含了一系列操作,主要是用于生成MyBatis的配置文件和相关代码。让我来梳理一下这段代码的主要逻辑:
- 创建一个MyBatis Generator的Configuration对象,并添加一个Context对象。
- 设置一些属性,比如设置java文件编码格式为UTF-8,设置连接数据库的类路径,设置自动限定关键字等。
- 针对不同的数据库类型进行特定的配置,比如设置数据库的schema、catalog、是否使用表名前缀等。
- 添加GeneratedKey主键生成的配置,设置实体类的包名和生成路径等。
- 配置JDBC连接信息,包括驱动类、连接URL、用户名和密码等。
- 设置Java模型生成器、SQL映射文件生成器、Java客户端生成器的配置信息。
- 配置注释生成器,设置生成的Java文件编码格式。
- 添加一些自定义的插件配置,比如序列化插件、Lombok插件、toString/hashCode/equals插件、分页插件等。
- 根据配置生成MyBatis的配置文件和相关代码。
- 如果选择覆盖XML文件,删除旧的XML文件并生成新的XML文件。 总的来说,这段代码主要是用于根据配置生成MyBatis的配置文件和相关代码,包括实体类、映射文件、DAO接口等。
配置信息
java
public class GeneratorConfig {
/**
* 本配置的名称
*/
private String name;
// 驱动类路径
private String connectorJarPath;
// 项目所在目录
private String projectFolder;
// 实体类包名
private String modelPackage;
// 生成的实体类存放路径
private String modelPackageTargetFolder;
// mapper接口包名
private String daoPackage;
// 生成的mapper接口存放路径
private String daoTargetFolder;
// 自定义接口名称, 即生成的mapper接口文件名称
private String mapperName;
// 映射xml文件包名
private String mappingXMLPackage;
// 生成的映射xml文件存放目录
private String mappingXMLTargetFolder;
// 表名,即数据库源表名
private String tableName;
// 生成的实体类名
private String domainObjectName;
// 是否使用分页插件
private boolean offsetLimit;
// 生成实体域注释(来自表注释)
private boolean comment;
// 覆盖原XML
private boolean overrideXML;
// 生成toString/hashCode/equals方法
private boolean needToStringHashcodeEquals;
// LombokPlugin
private boolean useLombokPlugin;
// select 增加ForUpdate
private boolean needForUpdate;
// DAO使用 @Repository 注解
private boolean annotationDAO;
// 生成JPA注解
private boolean annotation;
// 使用实际的列名
private boolean useActualColumnNames;
// 使用Example
private boolean useExample;
// 主键(选填)
private String generateKeys;
// 生成文件的编码
private String encoding;
// 启用as别名查询
private boolean useTableNameAlias;
// DAO方法抽出到公共父接口
private boolean useDAOExtendStyle;
// 使用Schema前缀
private boolean useSchemaPrefix;
// SR310: Date and Time API
private boolean jsr310Support;
}
适配
Mapper中添加select、delete和update
生成的mapper文件中包含基本的insert、delete、update和select
配置是在org.mybatis.generator.config.TableConfiguration中配置的,源码中可以看到,有8个可配置项, 默认都是开启,其中
- insertStatementEnabled配置为true, 会生成insert,insertSelective
- selectByPrimaryKeyStatementEnabled配置为true, 会生成selectByPrimaryKey
- selectByExampleStatementEnabled配置为true, 会生成selectByExample
- updateByPrimaryKeyStatementEnabled 配置为true, 会生成updateByPrimaryKey, updateByPrimaryKeySelective
- updateByExampleStatementEnabled配置为true, 会生成updateByExample,updateByExampleSelective
- deleteByPrimaryKeyStatementEnabled 配置为true,会生成deleteByPrimaryKey
- deleteByExampleStatementEnabled 配置为true, 会生成:deleteByExample
- countByExampleStatementEnabled 配置为true, 会生成countByExample
而我不需要example,所以将example配置都设为false
然而源码中已经设置了,那么为什么之前生成的只有insert和insert
Selective呢?原来是因为我的源表没有设置主键,导致对应的select、update和delete没有生成!!最后添加主键后,最后生成我需要的mapper接口文件啦。
Model中添加字段注解
Model中添加字段注解, gui中选择生成JPA注解
生成service和controller代码
在plugin新增自定义生成service和controller代码插件
代码如下:
java
package com.zzg.mybatis.generator.plugins;
import org.mybatis.generator.api.GeneratedJavaFile;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.*;
import java.util.ArrayList;
import java.util.List;
import static org.mybatis.generator.internal.util.StringUtility.stringHasValue;
public class ServiceAndControllerGeneratorPlugin extends PluginAdapter {
// 项目目录,一般为 src/main/java
private String targetProject;
// service包名,如:com.thinkj2ee.cms.service.service
private String servicePackage;
// service实现类包名,如:com.thinkj2ee.cms.service.service.impl
private String serviceImplPackage;
// Controlle类包名,如:com.thinkj2ee.cms.service.controller
private String controllerPackage;
// service接口名前缀
private String servicePreffix;
// service接口名后缀
private String serviceSuffix;
// service接口的父接口
private String superServiceInterface;
// service实现类的父类
private String superServiceImpl;
// controller类的父类
private String superController;
// dao接口基类
private String superDaoInterface;
// Example类的包名
private String examplePacket;
private String recordType;
private String modelName;
private FullyQualifiedJavaType model;
private String serviceName;
private String serviceImplName;
private String controllerName;
@Override
public boolean validate(List<String> warnings) {
boolean valid = true;
/* if (!stringHasValue(properties
.getProperty("targetProject"))) { //$NON-NLS-1$
warnings.add(getString("ValidationError.18", //$NON-NLS-1$
"MapperConfigPlugin", //$NON-NLS-1$
"targetProject")); //$NON-NLS-1$
valid = false;
}
if (!stringHasValue(properties.getProperty("servicePackage"))) { //$NON-NLS-1$
warnings.add(getString("ValidationError.18", //$NON-NLS-1$
"MapperConfigPlugin", //$NON-NLS-1$
"servicePackage")); //$NON-NLS-1$
valid = false;
}
if (!stringHasValue(properties.getProperty("serviceImplPackage"))) { //$NON-NLS-1$
warnings.add(getString("ValidationError.18", //$NON-NLS-1$
"MapperConfigPlugin", //$NON-NLS-1$
"serviceImplPackage")); //$NON-NLS-1$
valid = false;
}
*/
targetProject = properties.getProperty("targetProject");
servicePackage = properties.getProperty("servicePackage");
serviceImplPackage = properties.getProperty("serviceImplPackage");
servicePreffix = properties.getProperty("servicePreffix");
servicePreffix = stringHasValue(servicePreffix) ? servicePreffix : "";
serviceSuffix = properties.getProperty("serviceSuffix");
serviceSuffix = stringHasValue(serviceSuffix) ? serviceSuffix : "";
superServiceInterface = properties.getProperty("superServiceInterface");
superServiceImpl = properties.getProperty("superServiceImpl");
superDaoInterface = properties.getProperty("superDaoInterface");
controllerPackage = properties.getProperty("controllerPackage");
superController = properties.getProperty("superController");
return valid;
}
@Override
public List<GeneratedJavaFile> contextGenerateAdditionalJavaFiles(IntrospectedTable introspectedTable) {
recordType = introspectedTable.getBaseRecordType();
modelName = recordType.substring(recordType.lastIndexOf(".") + 1);
model = new FullyQualifiedJavaType(recordType);
serviceName = servicePackage + "." + servicePreffix + modelName + serviceSuffix;
serviceImplName = serviceImplPackage + "." + modelName + serviceSuffix+"Impl";
examplePacket=recordType.substring(0,recordType.lastIndexOf("."));
controllerName=controllerPackage.concat(".").concat(modelName).concat("Controller");
List<GeneratedJavaFile> answer = new ArrayList<>();
GeneratedJavaFile gjf = generateServiceInterface(introspectedTable);
GeneratedJavaFile gjf2 = generateServiceImpl(introspectedTable);
GeneratedJavaFile gjf3 = generateController(introspectedTable);
answer.add(gjf);
answer.add(gjf2);
answer.add(gjf3);
return answer;
}
// 生成service接口
private GeneratedJavaFile generateServiceInterface(IntrospectedTable introspectedTable) {
FullyQualifiedJavaType service = new FullyQualifiedJavaType(serviceName);
Interface serviceInterface = new Interface(service);
serviceInterface.setVisibility(JavaVisibility.PUBLIC);
// 添加父接口
if(stringHasValue(superServiceInterface)) {
String superServiceInterfaceName = superServiceInterface.substring(superServiceInterface.lastIndexOf(".") + 1);
serviceInterface.addImportedType(new FullyQualifiedJavaType(superServiceInterface));
serviceInterface.addImportedType(new FullyQualifiedJavaType(recordType));
serviceInterface.addSuperInterface(new FullyQualifiedJavaType(superServiceInterfaceName + "<" + modelName + ">"));
}
GeneratedJavaFile gjf = new GeneratedJavaFile(serviceInterface, targetProject, context.getJavaFormatter());
return gjf;
}
// 生成serviceImpl实现类
private GeneratedJavaFile generateServiceImpl(IntrospectedTable introspectedTable) {
FullyQualifiedJavaType service = new FullyQualifiedJavaType(serviceName);
FullyQualifiedJavaType serviceImpl = new FullyQualifiedJavaType(serviceImplName);
TopLevelClass clazz = new TopLevelClass(serviceImpl);
//描述类的作用域修饰符
clazz.setVisibility(JavaVisibility.PUBLIC);
//描述类 引入的类
clazz.addImportedType(service);
//描述类 的实现接口类
clazz.addSuperInterface(service);
if(stringHasValue(superServiceImpl)) {
String superServiceImplName = superServiceImpl.substring(superServiceImpl.lastIndexOf(".") + 1);
clazz.addImportedType(superServiceImpl);
clazz.addImportedType(recordType);
clazz.setSuperClass(superServiceImplName + "<" + modelName + ">");
}
clazz.addImportedType(new FullyQualifiedJavaType("org.springframework.stereotype.Service"));
clazz.addAnnotation("@Service");
String daoFieldType = introspectedTable.getMyBatis3JavaMapperType();
String daoFieldName = firstCharToLowCase(daoFieldType.substring(daoFieldType.lastIndexOf(".") + 1));
//描述类的成员属性
Field daoField = new Field(daoFieldName, new FullyQualifiedJavaType(daoFieldType));
clazz.addImportedType(new FullyQualifiedJavaType(daoFieldType));
clazz.addImportedType(new FullyQualifiedJavaType("org.springframework.beans.factory.annotation.Autowired"));
//描述成员属性 的注解
daoField.addAnnotation("@Autowired");
//描述成员属性修饰符
daoField.setVisibility(JavaVisibility.PRIVATE);
clazz.addField(daoField);
//描述 方法名
Method method = new Method("getMapper");
//方法注解
method.addAnnotation("@Override");
FullyQualifiedJavaType methodReturnType = new FullyQualifiedJavaType("Object");
//返回值
method.setReturnType(methodReturnType);
//方法体,逻辑代码
method.addBodyLine("return " + daoFieldName + ";");
//修饰符
method.setVisibility(JavaVisibility.PUBLIC);
clazz.addMethod(method);
Method method1 = new Method("getExample");
method1.addAnnotation("@Override");
FullyQualifiedJavaType methodReturnType1 = new FullyQualifiedJavaType("Object");
clazz.addImportedType(new FullyQualifiedJavaType(examplePacket.concat(".").concat(modelName).concat("Example")));
method1.setReturnType(methodReturnType1);
method1.addBodyLine("return new " + modelName + "Example();");
method1.setVisibility(JavaVisibility.PUBLIC);
clazz.addMethod(method1);
GeneratedJavaFile gjf2 = new GeneratedJavaFile(clazz, targetProject, context.getJavaFormatter());
return gjf2;
}
// 生成controller类
private GeneratedJavaFile generateController(IntrospectedTable introspectedTable) {
FullyQualifiedJavaType controller = new FullyQualifiedJavaType(controllerName);
TopLevelClass clazz = new TopLevelClass(controller);
//描述类的作用域修饰符
clazz.setVisibility(JavaVisibility.PUBLIC);
//添加@Controller注解,并引入相应的类
clazz.addImportedType(new FullyQualifiedJavaType("org.springframework.web.bind.annotation.RestController"));
clazz.addAnnotation("@RestController");
//添加@RequestMapping注解,并引入相应的类
clazz.addImportedType(new FullyQualifiedJavaType("org.springframework.web.bind.annotation.RequestMapping"));
clazz.addAnnotation("@RequestMapping("/"+firstCharToLowCase(modelName)+"")");
//添加@Api注解,并引入相应的类
clazz.addImportedType(new FullyQualifiedJavaType("io.swagger.annotations.Api"));
String controllerSimpleName = controllerName.substring(controllerName.lastIndexOf(".") + 1);
clazz.addAnnotation("@Api(tags = ""+controllerSimpleName+"", description = ""+controllerSimpleName+"")");
//引入controller的父类和model,并添加泛型
if(stringHasValue(superController)) {
clazz.addImportedType(superController);
clazz.addImportedType(recordType);
FullyQualifiedJavaType superInterfac = new FullyQualifiedJavaType(superController+"<"+modelName+">");
clazz.addSuperInterface(superInterfac);
}
//引入Service
FullyQualifiedJavaType service = new FullyQualifiedJavaType(serviceName);
clazz.addImportedType(service);
//添加Service成员变量
String serviceFieldName = firstCharToLowCase(serviceName.substring(serviceName.lastIndexOf(".") + 1));
Field daoField = new Field(serviceFieldName, new FullyQualifiedJavaType(serviceName));
clazz.addImportedType(new FullyQualifiedJavaType(serviceName));
clazz.addImportedType(new FullyQualifiedJavaType("org.springframework.beans.factory.annotation.Autowired"));
//描述成员属性 的注解
daoField.addAnnotation("@Autowired");
//描述成员属性修饰符
daoField.setVisibility(JavaVisibility.PRIVATE);
clazz.addField(daoField);
//描述 方法名
Method method = new Method("getService");
//方法注解
method.addAnnotation("@Override");
// String simpleSuperServiceName = superServiceInterface.substring(superServiceInterface.lastIndexOf(".") + 1);
// FullyQualifiedJavaType methodReturnType = new FullyQualifiedJavaType(simpleSuperServiceName+"<"+modelName+">");
//返回类型
// method.setReturnType(methodReturnType);
//方法体,逻辑代码
method.addBodyLine("return " + serviceFieldName + ";");
//修饰符
method.setVisibility(JavaVisibility.PUBLIC);
// clazz.addImportedType(superServiceInterface);
// clazz.addMethod(method);
GeneratedJavaFile gjf2 = new GeneratedJavaFile(clazz, targetProject, context.getJavaFormatter());
return gjf2;
}
private String firstCharToLowCase(String str) {
char[] chars = new char[1];
//String str="ABCDE1234";
chars[0] = str.charAt(0);
String temp = new String(chars);
if(chars[0] >= 'A' && chars[0] <= 'Z') {
return str.replaceFirst(temp,temp.toLowerCase());
}
return str;
}
}
在生成代码的函数中添加如下代码:
java
// 生成service和controller插件
PluginConfiguration pluginConfiguration = new PluginConfiguration();
pluginConfiguration.addProperty("service", "com.zzg.mybatis.generator.plugins.ServiceAndControllerGeneratorPlugin");
pluginConfiguration.addProperty("targetProject", "./src/main/java");
pluginConfiguration.addProperty("servicePackage", "com.heaven.service");
pluginConfiguration.addProperty("serviceImplPackage", "com.heaven.service.impl");
pluginConfiguration.addProperty("controllerPackage", "com.heaven.controller");
pluginConfiguration.addProperty("serviceSuffix", "Service");
pluginConfiguration.addProperty("serviceImplSuffix", "ServiceImpl");
pluginConfiguration.addProperty("controllerSuffix", "Controller");
pluginConfiguration.setConfigurationType("com.zzg.mybatis.generator.plugins.ServiceAndControllerGeneratorPlugin");
context.addPluginConfiguration(pluginConfiguration);
最后点击gui生成代码如下:
小结
本文会简要分析一下生成代码的源码,并且修改源码从而使得生成的代码满足我的要求:
-
Mapper包含基本insert、delete、update和select
-
生成的mapper、model类文件需要添加上类注释 --未实现,后面完善
-
生成service代码
后续的文章会
-
改写gui,使得生成service和controller支持gui配置
-
改写gui,增加支持其他数据库
参考
JavaFX官方教程(七)之使用FXML创建用户界面_fxml如何创建页面-CSDN博客
mybatis generator自动生成代码时 只生成了insert 而没有其他的_mybatis generate dowithblobs 只生成插入未生成update-CSDN博客