本文为《从零打造项目》系列第一篇文章,首发于个人网站。
《从零打造项目》系列文章
比MyBatis Generator更强大的代码生成器
SpringBoot项目基础设施搭建
工欲善其事,必先利其器。作为《从零打造项目》系列的第一篇文章,本文主要带大家认识一下生成基础模版代码的脚手架。
文章更新记录
2021年4月构建本项目,集成了 Mybatis 和 Mybatis Plus 两种的生成逻辑。
2021年5月添加resultMap模板生成逻辑。
2022年9月补充了 SpringData JPA 的代码生成逻辑,同时重构了一下代码。
在 SpringBoot 项目开发前,关于初始代码的生成,是值得考虑的一件事。当我们根据业务需求完成表设计后,接下来就需要根据表生成相关代码,在 SpringBoot 项目中需要以下几部分内容:
除了上述项目架构中最基本的文件,为了更好的管理项目,我们还增加以下几个层级:
项目中使用的 ORM 框架多为 Mybatis、 Mybatis Plus 和 Spring Data JPA,虽然各自的官方文档都有代码生成器配置,但是过于简单,无法满足实际需求,因此整理出一套通用的代码生成器,势在必行。
在开始本文之前,首先介绍一下要用到的知识点。
FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像 PHP 那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
Spring Data JPA 是 Spring Data 项目的一部分,它可以更轻松地实现基于 JPA 的存储库。
Spring Data JPA 可以与 Hibernate、Eclipse Link 或任何其他 JPA 提供程序一起使用。使用 Spring 或 Java EE 的一个非常有趣的好处是您可以使用@Transactional
注解以声明方式控制事务边界。
本文主要讲述选择使用 Mybatis、 Mybatis Plus 和 Spring Data JPA 时,相关代码文件的生成过程。
JCommander 是一个用于解析命令行参数的Java框架,支持解析所有基本的数据类型,也支持将命令行解析成用户自定义的类型,只需要写一个转变函数。
接下来就进行代码实战环节。
首先新建一个 maven 项目,命名为 mybatis-generator。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.3version>
<relativePath/>
parent>
<groupId>com.msdn.generatorgroupId>
<artifactId>orm-generatorartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<java.version>1.8java.version>
<logback.version>1.2.3logback.version>
<fastjson.version>1.2.73fastjson.version>
<hutool.version>5.5.1hutool.version>
<mysql.version>8.0.19mysql.version>
<mybatis.version>2.1.4mybatis.version>
<mapper.version>4.1.5mapper.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>${fastjson.version}version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>${logback.version}version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>${hutool.version}version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>${mybatis.version}version>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapperartifactId>
<version>${mapper.version}version>
dependency>
<dependency>
<groupId>com.beustgroupId>
<artifactId>jcommanderartifactId>
<version>1.78version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-freemarkerartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-starterartifactId>
<version>1.4.3version>
dependency>
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-commonsartifactId>
<version>2.4.6version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>org.springdocgroupId>
<artifactId>springdoc-openapi-uiartifactId>
<version>1.6.9version>
dependency>
<dependency>
<groupId>org.mapstructgroupId>
<artifactId>mapstruct-jdk8artifactId>
<version>1.5.2.Finalversion>
dependency>
<dependency>
<groupId>org.mapstructgroupId>
<artifactId>mapstruct-processorartifactId>
<version>1.5.2.Finalversion>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
application.yml 文件内容如下:
server:
port: 8525
spring:
application:
name: orm-generator
springdoc:
swagger-ui:
# 修改Swagger UI路径
path: /swagger-ui.html
# 开启Swagger UI界面
enabled: true
api-docs:
# 修改api-docs路径
path: /v3/api-docs
# 开启api-docs
enabled: true
# 配置需要生成接口文档的扫描包
packages-to-scan: com.msdn.generator.controller
为了接收相关配置参数,我们通过 JCommander 解析命令行参数,此处创建对应的实体类 GenerateParameter来接收这些参数。
@Getter
@Setter
@Schema(name = "使用帮助")
@Parameters(commandDescription = "使用帮助")
public class GenerateParameter {
@Schema(name = "mysql主机名")
@Parameter(names = {"--host", "-h"}, description = "mysql主机名")
private String host;
@Schema(name = "mysql端口")
@Parameter(names = {"--port", "-P"}, description = "mysql端口")
private Integer port;
@Schema(name = "mysql用户名")
@Parameter(names = {"--username", "-u"}, description = "mysql用户名")
private String username;
@Schema(name = "mysql密码")
@Parameter(names = {"--password", "-p"}, description = "mysql密码")
private String password;
@Schema(name = "mysql数据库名")
@Parameter(names = {"--database", "-d"}, description = "mysql数据库名")
private String database;
@Schema(name = "mysql数据库表")
@Parameter(names = {"--table", "-t"}, description = "mysql数据库表")
private List<String> table;
@Schema(name = "业务模块名")
@Parameter(names = {"--module", "-m"}, description = "业务模块名")
private String module;
@Schema(name = "业务分组,目前是base和business")
@Parameter(names = {"--group", "-g"}, description = "业务分组,目前是base和business")
private String group;
@Schema(name = "是否按表名分隔目录")
@Parameter(names = {"--flat"}, description = "是否按表名分隔目录")
private boolean flat;
@Schema(name = "orm框架选择")
@Parameter(names = {"--type"}, description = "orm框架选择")
private String type;
@Schema(name = "查看帮助")
@Parameter(names = "--help", help = true, description = "查看帮助")
private boolean help;
@Schema(name = "表名截取起始索引,比如表名叫做t_sale_contract_detail,生成的实体类为ContractDetail,则该字段为7")
@Parameter(names = {"--tableStartIndex", "-tsi"}, description = "表名截取起始索引")
private String tableStartIndex;
}
当连接上数据库后,我们需要解析读取的表结构,包括获取表字段,字段备注,字段类型等内容,对应此处创建的 Column 类。
@Data
public class Column {
/**
* 是否是主键
*/
private Boolean isPrimaryKey;
/**
* Mybatis plus生成类主键类型,默认为ASSIGN_ID(3)
*/
private String primaryKeyType = "ASSIGN_ID";
/**
* 数据库表名称
*/
private String tableName;
/**
* 表描述
*/
private String tableDesc;
/**
* 数据库字段名称
**/
private String fieldName;
/**
* 数据库字段类型
**/
private String fieldType;
/**
* Java类型
*/
private String javaType;
/**
* 是否是数字类型
*/
private Boolean isNumber;
/**
* 数据库字段驼峰命名,saleBooke
**/
private String camelName;
/**
* 数据库字段Pascal命名,SaleBook
**/
private String pascalName;
/**
* 数据库字段注释
**/
private String comment;
private String field;
private String key;
/**
* 是否是公共字段
*/
private Boolean isCommonField;
}
最后创建一个常量类 Config,来存储常量信息。
public class Config {
public static final String OUTPUT_PATH = "." + File.separator + "output";
public static final String AUTHOR = "hresh";
// 公共实体类字段
public static final String[] JPA_COMMON_COLUMNS = new String[]{
"create_user_code", "create_user_name", "created_date", "last_modified_code",
"last_modified_name"
, "last_modified_date", "version", "id", "del_flag"
};
public static final String[] MYBATIS_COMMON_COLUMNS = new String[]{
"create_user_code", "create_user_name", "created_date", "last_modified_code",
"last_modified_name"
, "last_modified_date", "version", "id", "del_flag"
};
public static final String[] MYBATIS_PLUS_COMMON_COLUMNS = new String[]{
"create_user_code", "create_user_name", "created_date", "last_modified_code",
"last_modified_name"
, "last_modified_date", "version", "id", "del_flag"
};
}
首先定义 FreeMarker 的使用代码:
@Service
public class FreemarkerService {
@Autowired
private Configuration configuration;
/**
* 输出文件模板
*
* @param templateName resources 文件夹下的模板名,比如说model.ftl,是生成实体类的模块
* @param dataModel 表名,字段名等内容集合
* @param filePath 输出文件名,包括路径
* @param generateParameter
* @throws Exception
*/
public void write(String templateName, Map<String, Object> dataModel, String filePath,
GenerateParameter generateParameter) throws Exception {
// FTL(freemarker templete language)模板的文件名称
Template template = configuration
.getTemplate(dataModel.get("type") + File.separator + templateName + ".ftl");
File file;
// 判断是不是多表,如果是,则按照表名生成各自的文件夹目录
if (generateParameter.isFlat()) {
file = new File(
Config.OUTPUT_PATH + File.separator + dataModel.get("tempId") + File.separator + filePath);
} else {
file = new File(
Config.OUTPUT_PATH + File.separator + dataModel.get("tempId") + File.separator + dataModel
.get("tableName") + File.separator + filePath);
}
if (!file.exists()) {
file.getParentFile().mkdirs();
file.createNewFile();
}
FileOutputStream fileOutputStream = new FileOutputStream(file);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream,
StandardCharsets.UTF_8);
template.process(dataModel, outputStreamWriter);
fileOutputStream.flush();
fileOutputStream.close();
}
}
接下来是本项目最核心的代码,通过读取数据表,获取表的定义信息,然后利用 FreeMarker 读取 Ftl 模板文件来生成关于该表的基础代码。
基础服务类 BaseService
public class BaseService {
private static Connection connection;
public static void setConnection(GenerateParameter generateParameter) throws Exception {
connection = getConnection(generateParameter);
}
public static void closeConnection() throws SQLException {
connection.close();
}
public static String getUrl(GenerateParameter generateParameter) {
return "jdbc:mysql://" + generateParameter.getHost() + ":" + generateParameter.getPort() + "/"
+ generateParameter.getDatabase()
+ "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8";
}
/**
* 数据库连接,类似于:DriverManager.getConnection("jdbc:mysql://localhost:3306/test_demo?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC","root","password");
*
* @param generateParameter 请求参数
* @return 数据库连接
* @throws Exception
*/
public static Connection getConnection(GenerateParameter generateParameter) throws Exception {
return DriverManager.getConnection(getUrl(generateParameter), generateParameter.getUsername(),
generateParameter.getPassword());
}
/**
* 根据表具体位置,获取表中字段的具体信息,包括字段名,字段类型,备注等
*
* @param tableName
* @return
* @throws Exception
*/
public List<Column> getColumns(String tableName, String[] commonColumns) throws Exception {
// 获取表定义的字段信息
ResultSet resultSet = connection.createStatement()
.executeQuery("SHOW FULL COLUMNS FROM " + tableName);
List<Column> columnList = new ArrayList<>();
while (resultSet.next()) {
String fieldName = resultSet.getString("Field");
Column column = new Column();
// 判断是否是主键
column.setIsPrimaryKey("PRI".equals(resultSet.getString("Key")));
// 获取字段名称
column.setFieldName(fieldName);
// 实体类特定字段从核心类里获取
if (Objects.nonNull(commonColumns) && Arrays.asList(commonColumns).contains(fieldName)) {
column.setIsCommonField(true);
} else {
column.setIsCommonField(false);
}
// 获取字段类型
column.setFieldType(resultSet.getString("Type").replaceAll("\\(.*\\)", ""));
switch (column.getFieldType()) {
case "json":
case "longtext":
case "char":
case "varchar":
case "text":
column.setJavaType("String");
column.setIsNumber(false);
break;
case "date":
case "datetime":
column.setJavaType("Date");
column.setIsNumber(false);
break;
case "timestamp":
column.setJavaType("LocalDateTime");
column.setIsNumber(false);
break;
case "bit":
column.setJavaType("Boolean");
column.setIsNumber(false);
break;
case "int":
case "tinyint":
column.setJavaType("Integer");
column.setIsNumber(true);
break;
case "bigint":
column.setJavaType("Long");
column.setIsNumber(true);
break;
case "decimal":
column.setJavaType("BigDecimal");
column.setIsNumber(true);
break;
case "varbinary":
column.setJavaType("byte[]");
column.setIsNumber(false);
break;
default:
throw new Exception(
tableName + " " + column.getFieldName() + " " + column.getFieldType() + "类型没有解析");
}
// 转换字段名称,receipt_sign_name字段改为 receiptSignName
column.setCamelName(StringUtils.underscoreToCamel(column.getFieldName()));
// 首字母大写
column.setPascalName(StringUtils.firstLetterUpperCase(column.getCamelName()));
// 字段在数据库的注释
column.setComment(resultSet.getString("Comment"));
columnList.add(column);
}
return columnList;
}
/**
* 获取表的描述
*
* @param tableName
* @param parameter
* @return
* @throws Exception
*/
public String getTableComment(String tableName, GenerateParameter parameter) throws Exception {
Connection connection = getConnection(parameter);
ResultSet resultSet = connection.createStatement().executeQuery(
"SELECT table_comment FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = '" + parameter
.getDatabase()
+ "' AND table_name = '" + tableName + "'");
String tableComment = "";
while (resultSet.next()) {
tableComment = resultSet.getString("table_comment");
}
return tableComment;
}
}
GenerateService 获取表信息生成相关代码
@Service
@Slf4j
public class GenerateService extends BaseService {
@Autowired
private FreemarkerService freemarkerService;
/**
* @param tableName 数据库表名
* @param parameter 模块名
* @param uuid 生成uuid
* @throws Exception
*/
public void generate(String tableName, GenerateParameter parameter, String uuid)
throws Exception {
// 各模块包名,比如 com.msdn.sale 或 com.msdn.finance
String packagePrefix = "com.msdn." + parameter.getModule();
// 分组
if (!StringUtils.isEmpty(parameter.getGroup())) {
packagePrefix = packagePrefix + "." + parameter.getGroup();
}
// 根据项目设计的表名获取到表名,比如表名叫做:t_sale_contract_detail
// 现在表名截取起始索引该由参数配置
Integer index = new Integer(parameter.getTableStartIndex());
// 驼峰命名,首字母小写,比如:contractDetail
String camelName = StringUtils.underscoreToCamel(tableName.substring(index));
Map<String, Object> dataModel = new HashMap<>();
//获取表中字段的具体信息,包括字段名,字段类型,备注等,排除指定字段
String[] commonColumns = Config.MYBATIS_COMMON_COLUMNS;
if ("jpa".equals(parameter.getType().toLowerCase())) {
commonColumns = Config.JPA_COMMON_COLUMNS;
} else if ("mybatis".equals(parameter.getType().toLowerCase())) {
commonColumns = Config.MYBATIS_COMMON_COLUMNS;
} else if ("mybatisplus".equals(parameter.getType().toLowerCase())) {
commonColumns = Config.MYBATIS_PLUS_COMMON_COLUMNS;
}
List<Column> columns = getColumns(tableName, commonColumns);
Column primaryColumn = columns.stream().filter(Column::getIsPrimaryKey).findFirst()
.orElse(null);
dataModel.put("package", packagePrefix);
dataModel.put("camelName", camelName);
// 首字母转大写,作为实体类名称等
dataModel.put("pascalName", StringUtils.firstLetterUpperCase(camelName));
dataModel.put("moduleName", parameter.getModule());
dataModel.put("tableName", tableName);
// 表描述
dataModel.put("tableComment", getTableComment(tableName, parameter));
dataModel.put("columns", columns);
dataModel.put("primaryColumn", primaryColumn);
dataModel.put("tempId", uuid);
dataModel.put("author", Config.AUTHOR);
dataModel.put("date", DateUtil.now());
dataModel.put("type", parameter.getType());
log.info("准备生成模板代码的表名为:" + tableName + ",表描述为:" + dataModel.get("tableComment"));
// 生成模板代码
log.info("**********开始生成Model模板文件**********");
generateModel(dataModel, parameter);
log.info("**********开始生成VO视图模板文件**********");
generateVO(dataModel, parameter);
log.info("**********开始生成DTO模板文件**********");
generateDTO(dataModel, parameter);
log.info("**********开始生成Struct模板文件**********");
generateStruct(dataModel, parameter);
log.info("**********开始生成Mapper模板文件**********");
generateMapper(dataModel, parameter);
log.info("**********开始生成Service模板文件**********");
generateService(dataModel, parameter);
log.info("**********开始生成Controller模板文件**********");
generateController(dataModel, parameter);
}
/**
* 生成 controller 模板代码
*
* @param dataModel
* @param generateParameter
* @throws Exception
*/
private void generateController(Map<String, Object> dataModel,
GenerateParameter generateParameter) throws Exception {
String path =
"java" + File.separator + "controller" + File.separator + dataModel.get("pascalName")
+ "Controller.java";
freemarkerService.write("controller", dataModel, path, generateParameter);
}
private void generateDTO(Map<String, Object> dataModel, GenerateParameter generateParameter)
throws Exception {
String path = "java" + File.separator + "dto" + File.separator + dataModel.get("pascalName");
freemarkerService.write("dto", dataModel, path + "DTO.java", generateParameter);
freemarkerService.write("dto-page", dataModel, path + "QueryPageDTO.java", generateParameter);
}
//
private void generateModel(Map<String, Object> dataModel, GenerateParameter generateParameter)
throws Exception {
String path =
"java" + File.separator + "model" + File.separator + dataModel.get("pascalName") + ".java";
freemarkerService.write("model", dataModel, path, generateParameter);
}
private void generateStruct(Map<String, Object> dataModel, GenerateParameter generateParameter)
throws Exception {
String path = "java" + File.separator + "struct" + File.separator + dataModel.get("pascalName")
+ "Struct.java";
freemarkerService.write("struct", dataModel, path, generateParameter);
}
private void generateMapper(Map<String, Object> dataModel, GenerateParameter generateParameter)
throws Exception {
if (!"jpa".equals(generateParameter.getType().toLowerCase())) {
String path = "java" + File.separator + "mapper" + File.separator + dataModel.get("pascalName")
+ "Mapper.java";
freemarkerService.write("mapper", dataModel, path, generateParameter);
path = "resources" + File.separator + dataModel.get("pascalName") + "Mapper.xml";
freemarkerService.write("mapper-xml", dataModel, path, generateParameter);
}else {
String path = "java" + File.separator + "repository" + File.separator + dataModel.get("pascalName")
+ "Repository.java";
freemarkerService.write("repository", dataModel, path, generateParameter);
}
}
private void generateService(Map<String, Object> dataModel, GenerateParameter generateParameter)
throws Exception {
String path = "java" + File.separator + "service" + File.separator + dataModel.get("pascalName")
+ "Service.java";
freemarkerService.write("service", dataModel, path, generateParameter);
path =
"java" + File.separator + "service" + File.separator + "impl" + File.separator + dataModel
.get("pascalName") + "ServiceImpl.java";
freemarkerService.write("service-impl", dataModel, path, generateParameter);
}
private void generateVO(Map<String, Object> dataModel, GenerateParameter generateParameter)
throws Exception {
String path =
"java" + File.separator + "vo" + File.separator + dataModel.get("pascalName") + "VO.java";
freemarkerService.write("vo", dataModel, path, generateParameter);
}
}
为了更加方便地使用代码生成器,我们通过 swagger 来调用 Rest 服务接口。
@RestController("/generator")
@Slf4j
@RequiredArgsConstructor
public class GeneratorController {
private final GenerateService generateService;
private final XmlGenerateService xmlGenerateService;
/*
// 请求参数
{
"database": "db_tl_sale",
"flat": true,
"type": "mybatis",
"group": "base",
"host": "127.0.0.1",
"module": "sale",
"password": "123456",
"port": 3306,
"table": [
"t_xs_sale_contract"
],
"username": "root"
}
*/
@PostMapping("/build")
@Operation(description = "选择orm框架后生成基础模版代码")
public void build(@RequestBody GenerateParameter parameter, HttpServletResponse response)
throws Exception {
log.info("**********欢迎使用基于FreeMarker的模板文件生成器**********");
log.info("************************************************************");
String uuid = UUID.randomUUID().toString();
BaseService.setConnection(parameter);
for (String table : parameter.getTable()) {
generateService.generate(table, parameter, uuid);
}
log.info("**********模板文件生成完毕,准备下载**********");
String path = Config.OUTPUT_PATH + File.separator + uuid;
//设置响应头控制浏览器的行为,这里我们下载zip
response.setHeader("Content-disposition", "attachment; filename=code.zip");
response.setHeader("Access-Control-Expose-Headers", "Content-disposition");
// 将response中的输出流中的文件压缩成zip形式
ZipDirectory(path, response.getOutputStream());
// 递归删除目录
FileSystemUtils.deleteRecursively(new File(path));
BaseService.closeConnection();
log.info("************************************************************");
log.info("**********模板文件下载完毕,谢谢使用**********");
}
@PostMapping("/buildXml")
@Operation(description = "选择orm框架后生成基础模版代码,针对Mybatis会补充生成xml文件中的resultMap")
public void buildXml(@RequestBody GenerateParameter parameter, HttpServletResponse response)
throws Exception {
log.info("**********欢迎使用基于FreeMarker的模板文件生成器**********");
log.info("************************************************************");
String uuid = UUID.randomUUID().toString();
BaseService.setConnection(parameter);
for (String table : parameter.getTable()) {
xmlGenerateService.generate(table, parameter, uuid);
}
log.info("**********模板文件生成完毕,准备下载**********");
String path = Config.OUTPUT_PATH + File.separator + uuid;
//设置响应头控制浏览器的行为,这里我们下载zip
response.setHeader("Content-disposition", "attachment; filename=code.zip");
response.setHeader("Access-Control-Expose-Headers", "Content-disposition");
// 将response中的输出流中的文件压缩成zip形式
ZipDirectory(path, response.getOutputStream());
// 递归删除目录
FileSystemUtils.deleteRecursively(new File(path));
BaseService.closeConnection();
log.info("************************************************************");
log.info("**********模板文件下载完毕,谢谢使用**********");
}
/**
* 一次性压缩多个文件,文件存放至一个文件夹中
*/
public static void ZipDirectory(String directoryPath, ServletOutputStream outputStream) {
try {
ZipOutputStream output = new ZipOutputStream(outputStream);
List<File> files = getFiles(new File(directoryPath));
for (File file : files) {
try (InputStream input = new FileInputStream(file)) {
output.putNextEntry(new ZipEntry(file.getPath().substring(directoryPath.length() + 1)));
int temp;
while ((temp = input.read()) != -1) {
output.write(temp);
}
}
}
output.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static List<File> getFiles(File file) {
List<File> files = new ArrayList<>();
for (File subFile : Objects.requireNonNull(file.listFiles())) {
if (subFile.isDirectory()) {
List<File> subFiles = getFiles(subFile);
files.addAll(subFiles);
} else {
files.add(subFile);
}
}
return files;
}
}
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GeneratorApplication {
/**
* 测试的时候添加参数 -h 127.0.0.1 -P 3306 -d db_tl_sale -u root -p 123456 -m sale -g base -t
* t_xs_sale_contract,t_xs_sale_contract_detail
*
* @param args
*/
public static void main(String[] args) {
SpringApplication.run(GeneratorApplication.class, args);
}
}
定义的模板文件如下图所示:
除了上述代码,还有一些工具类,以及公共组件,这里就不一一介绍了,我会在下篇文章中详细介绍这些基础代码,大致内容如下图所示:
包括请求日志记录、返回对象封装、全局异常捕获等等。
启动项目后,直接访问 http://localhost:8525/swagger-ui.html#/。
传入参数根据个人需要按照如下格式整理信息:
{
"database": "db_tl_sale",
"flat": true,
"type": "mybatis",
"group": "base",
"host": "127.0.0.1",
"module": "sale",
"password": "123456",
"port": 3306,
"table": [
"t_xs_sale_contract"
],
"username": "root",
"tableStartIndex":"5"
}
type 属性可以设置为 common、mybatis、mybatisplus、jpa,后三个属性值对应不同的 orm 框架。
然后点击执行,执行成功后点击下载,将生成好的代码下载到本地。文件结构如下图所示:
这里截取一部分代码图片,首先是实体类:
然后是查询实体类:
接着是 Service 接口:
以及对应的实现类:
最后是 controller:
resultMap
元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets
数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap
能够代替实现同等功能的数千行代码。ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
需求:
目前订单类详情查询返回的结果中,除了包含订单类的全部信息,还需要返回多个订单子项的数据,也就是我们常说的一对多关系,那么在实际开发中如何操作呢?
首先我们看一下代码案例:
1、订单类
public class OmsOrder implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "订单id")
private Long id;
private Long memberId;
private Long couponId;
@ApiModelProperty(value = "订单编号")
private String orderSn;
........
}
2、订单子项类
public class OmsOrderItem implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
@ApiModelProperty(value = "订单id")
private Long orderId;
@ApiModelProperty(value = "订单编号")
private String orderSn;
private Long productId;
........
}
3、前端返回类
public class OmsOrderDetail extends OmsOrder {
@Getter
@Setter
@ApiModelProperty("订单商品列表")
private List<OmsOrderItem> orderItemList;
}
4、OmsOrderMapper.xml 文件中自定义 SQL 语句
<resultMap id="orderDetailResultMap" type="com.macro.mall.dto.OmsOrderDetail" extends="com.macro.mall.mapper.OmsOrderMapper.BaseResultMap">
<collection property="orderItemList" resultMap="com.macro.mall.mapper.OmsOrderItemMapper.BaseResultMap" columnPrefix="item_"/>
resultMap>
<select id="getDetail" resultMap="orderDetailResultMap">
SELECT o.*,
oi.id item_id,
oi.product_id item_product_id,
oi.product_sn item_product_sn,
oi.product_pic item_product_pic,
oi.product_name item_product_name,
oi.product_brand item_product_brand,
oi.product_price item_product_price,
oi.product_quantity item_product_quantity,
oi.product_attr item_product_attr
FROM
oms_order o
LEFT JOIN oms_order_item oi ON o.id = oi.order_id
WHERE
o.id = #{id}
ORDER BY oi.id ASC DESC
select>
其中 com.macro.mall.mapper.OmsOrderItemMapper.BaseResultMap
是引用自 OmsOrderItemMapper.xml 文件中的定义,
<resultMap id="BaseResultMap" type="com.macro.mall.model.OmsOrderItem">
<id column="id" property="id" />
<result column="order_id" property="orderId" />
<result column="order_sn" property="orderSn" />
<result column="product_id" jdbcType="BIGINT" property="productId" />
<result column="product_pic" jdbcType="VARCHAR" property="productPic" />
<result column="product_name" jdbcType="VARCHAR" property="productName" />
<result column="product_brand" jdbcType="VARCHAR" property="productBrand" />
<result column="product_sn" jdbcType="VARCHAR" property="productSn" />
<result column="product_price" jdbcType="DECIMAL" property="productPrice" />
<result column="product_quantity" jdbcType="INTEGER" property="productQuantity" />
<result column="product_sku_id" jdbcType="BIGINT" property="productSkuId" />
<result column="product_sku_code" jdbcType="VARCHAR" property="productSkuCode" />
<result column="product_category_id" jdbcType="BIGINT" property="productCategoryId" />
<result column="promotion_name" jdbcType="VARCHAR" property="promotionName" />
<result column="promotion_amount" jdbcType="DECIMAL" property="promotionAmount" />
<result column="coupon_amount" jdbcType="DECIMAL" property="couponAmount" />
<result column="integration_amount" jdbcType="DECIMAL" property="integrationAmount" />
<result column="real_amount" jdbcType="DECIMAL" property="realAmount" />
<result column="gift_integration" jdbcType="INTEGER" property="giftIntegration" />
<result column="gift_growth" jdbcType="INTEGER" property="giftGrowth" />
<result column="product_attr" jdbcType="VARCHAR" property="productAttr" />
resultMap>
5、执行效果
这种查询方式相较于先查主表,再根据主表字段关联查询子表信息,减少了 IO 连接查询次数,效率更高一些。
通过上述代码我们可知,实现一对多关联查询的关键在于定义子项数据(多)的 resultMap 定义,既然我们通过代码生成器生成了基本的项目代码,那么是否可以生成 resultMap 呢?说干就干,代码如下:
1、定义模板 ftl 文件
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${package}.mapper.${pascalName}Mapper">
<resultMap id="BaseResultMap" type="${package}.model.${pascalName}">
<#list columns as column>
<#if column.isPrimaryKey>
<id column="${column.fieldName}" property="${column.camelName}" />
<#else>
<result column="${column.fieldName}" property="${column.camelName}" />
#if>
#list>
resultMap>
mapper>
2、编写服务类 XmlGenerateService
@Service
public class XmlGenerateService extends BaseService {
private static final Logger logger = LoggerFactory.getLogger(XmlGenerateService.class);
@Autowired
private FreemarkerService freemarkerService;
/**
* @param tableName 数据库表名
* @param parameter 模块名
* @param uuid
* @throws Exception
*/
public void generate(String tableName, GenerateParameter parameter, String uuid) throws Exception {
// 各模块包名,比如 com.msdn.sale 或 com.msdn.finance
String packagePrefix = "com.msdn." + parameter.getModule();
// 分组
if (!StringUtils.isEmpty(parameter.getGroup())) {
packagePrefix = packagePrefix + "." + parameter.getGroup();
}
// 根据项目设计的表名获取到表名,比如表名叫做:t_sale_contract_detail
// 现在表名截取起始索引该由参数配置
// int index = tableName.indexOf("_", 2);
Integer index = new Integer(parameter.getTableStartIndex());
// 驼峰命名,首字母小写,比如:contractDetail
String camelName = StringUtils.underscoreToCamel(tableName.substring(index));
Map<String, Object> dataModel = new HashMap<>();
//获取表中字段的具体信息,包括字段名,字段类型,备注等,排除指定字段
List<Column> columns = getColumns(tableName, parameter, null);
Column primaryColumn = columns.stream().filter(Column::getIsPrimaryKey).findFirst().orElse(null);
dataModel.put("package", packagePrefix);
dataModel.put("camelName", camelName);
// 首字母转大写,作为实体类名称等
dataModel.put("pascalName", StringUtils.capitalize(camelName));
dataModel.put("moduleName", parameter.getModule());
dataModel.put("tableName", tableName);
// 表描述
dataModel.put("tableComment", getTableComment(tableName, parameter));
dataModel.put("columns", columns);
dataModel.put("primaryColumn", primaryColumn);
dataModel.put("tempId", uuid);
dataModel.put("author", Config.Author);
dataModel.put("date", DateUtil.now());
dataModel.put("type", parameter.getType());
logger.info("准备生成模板代码的表名为:" + tableName + ",表描述为:" + dataModel.get("tableComment"));
// 生成模板代码
logger.info("**********开始生成Model模板文件**********");
generateXML(dataModel, parameter);
}
/**
* 生成 controller 模板代码
*
* @param dataModel
* @param generateParameter
* @throws Exception
*/
private void generateXML(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception {
String path = "resources" + File.separator + "xml" + File.separator + dataModel.get("pascalName") + "Mapper.xml";
freemarkerService.write("mybatis-xml", dataModel, path, generateParameter);
}
}
3、服务接口
@PostMapping("/generator/buildXml")
public void buildXml(@RequestBody GenerateParameter parameter, HttpServletResponse response) throws Exception {
logger.info("**********欢迎使用基于FreeMarker的模板文件生成器**********");
logger.info("************************************************************");
String uuid = UUID.randomUUID().toString();
for (String table : parameter.getTable()) {
xmlGenerateService.generate(table, parameter, uuid);
}
logger.info("**********模板文件生成完毕,准备下载**********");
String path = Config.OutputPath + File.separator + uuid;
//设置响应头控制浏览器的行为,这里我们下载zip
response.setHeader("Content-disposition", "attachment; filename=code.zip");
response.setHeader("Access-Control-Expose-Headers", "Content-disposition");
// 将response中的输出流中的文件压缩成zip形式
ZipDirectory(path, response.getOutputStream());
// 递归删除目录
FileSystemUtils.deleteRecursively(new File(path));
logger.info("************************************************************");
logger.info("**********模板文件下载完毕,谢谢使用**********");
}
4、通过 swagger 调用 api
5、执行结果
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.msdn.mall.mapper.OmsOrderItemMapper">
<resultMap id="BaseResultMap" type="com.msdn.mall.model.OmsOrderItem">
<id column="order_item_id" property="orderItemId" />
<result column="order_id" property="orderId" />
<result column="order_sn" property="orderSn" />
<result column="product_id" property="productId" />
<result column="product_pic" property="productPic" />
<result column="product_name" property="productName" />
<result column="product_brand" property="productBrand" />
<result column="product_sn" property="productSn" />
<result column="product_price" property="productPrice" />
<result column="purchase_amount" property="purchaseAmount" />
<result column="product_sku_id" property="productSkuId" />
<result column="product_sku_code" property="productSkuCode" />
<result column="product_category_id" property="productCategoryId" />
<result column="sp1" property="sp1" />
<result column="sp2" property="sp2" />
<result column="sp3" property="sp3" />
<result column="promotion_name" property="promotionName" />
<result column="promotion_money" property="promotionMoney" />
<result column="coupon_money" property="couponMoney" />
<result column="integration_money" property="integrationMoney" />
<result column="real_money" property="realMoney" />
<result column="gift_integration" property="giftIntegration" />
<result column="gift_growth" property="giftGrowth" />
<result column="product_attr" property="productAttr" />
<result column="is_deleted" property="isDeleted" />
<result column="create_user_code" property="createUserCode" />
<result column="create_user_name" property="createUserName" />
<result column="create_date" property="createDate" />
<result column="update_user_code" property="updateUserCode" />
<result column="update_user_name" property="updateUserName" />
<result column="update_date" property="updateDate" />
<result column="version" property="version" />
resultMap>
mapper>
在生产开发中如果还遇到好玩的东西,会不定期追加更新,希望工具越来越强大。如果有更好的建议,也可以在评论区@我。
虽然 Mybatis 和 Mybatis Plus 都有相关的代码生成器配置,但是构建器代码不容易整合,外部调用也不方便,最主要的是无法满足实际需求。为了能够一次性生成所有代码,最终选择 SpringBoot 和 FreeMarker 来构建我们专属的代码生成器。
除了可以生成 Java 相关代码,FreeMarker 还可以根据模板文件来生成前端代码,又或者是 Word 文档等,后续更多功能会根据情况逐步补充的。
感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!
ZipOutputStream相关知识
使用JCommander开发命令行交互(CLI)式JAVA程序