但是随着项目的规模逐渐庞大,每个Contoller的代码如果都自己码,不但耗时费力,也会带入很多不必要的错误。解决这个问题也不难,之前普遍使用Code Generator,根据数据库结构,自动生成代码;在现在大模型使用门槛越来越低的今天,使用大模型来自动生成代码也是一个不错的选择。今天就来介绍第一种使用MyBatis Plus Code Generator来生成分层代码。
项目地址:
https://gitee.com/obsidian15/youask/tree/v0.3/
Tag:v0.3
重构项目结构
在开始建设代码生成器前,有一部分工作需要提前先做,就是重构项目结构。这个不仅仅为为了代码生成器项目,同时也是为了后续项目有一个更好、更清晰、更好运维的项目结构和项目逻辑。
可以看到,目前的项目结构只包含一个youask-admin-api的模块,所有的代码都是在这个模块中,youask-admin-api模块本身也是一个springboot的项目,提供RESTful的api。这样的项目结构为后续项目的扩展提供了很多的不便,比如YaEntity、SysUser类似这样的系统级对象,很可能需要在多个模块之间进行交换数据,为了避免不必要的重复定义,需要将这样的实体、配置迁移到一个公用的模块中,我们把这个模块命名为youask-common。
- 创建一个模块简单的方式就是直接在项目文件夹下创建一个空白的文件夹,然后复制一些必要的项目文件和文件夹。这里比较推荐使用IntelliJ IDEA来创建模块,打开IDEA,右键点击父项目youask,依次点击New→Module,创建新模块。
- 在New Module中填写新建模块信息
项目创建完成后,可以看到父模块的pom中,已经自动添加了youask-common的关联
- 修改youask-common模块的pom文件
<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
<parent>
<groupId>com.obsidian</groupId>
<artifactId>youask</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>youask-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>YouAsk Common</name>
<description>YouAsk common</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
其中比较重要的需要修改的内容包括:parent、dependencies
- youask-common模块中按照如下格式创建pakcage,config、exception、model、entity,resources/mapper
- 代码迁移,按下图中所示,将youask-admin-api中的config、exception、model中的代码迁移到common模块对应的文件夹中
- youask-admin-api改为引用common模块中的对象。
在父模块的pom中增加对youask-common模块的引用
<dependency>
<groupId>com.obsidian</groupId>
<artifactId>youask-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
注意:当youask-common模块的版本号有修改时,这里也需要同步修改。
在youask-admin-api模块的pom中增加youask-common模块的引用
<dependency>
<groupId>com.obsidian</groupId>
<artifactId>youask-common</artifactId>
</dependency>
修复youask-admin-api模块中有引用错误的地方,改为使用common模块中的对象。
- 调整MyBatis Plus对代码的扫描
这步非常重要,但是很可能被忘记。之前因为只有一个项目,只需要扫描这一个项目中的文件就可以了。现在有了2个项目,且包名是不一样的,分别为com.obsidian.youask.admin和
com.obsidian.youask.common,这样就需要重新设置扫描的范围。
在
YouAskAdminApiApplication.java文件中,增加MapperScan的范围
@SpringBootApplication(scanBasePackages = "com.obsidian.youask")
@MapperScans({
@MapperScan("com.obsidian.youask.admin.mapper"),
@MapperScan("com.obsidian.youask.common.mapper")
})
public class YouAskAdminApiApplication {
public static void main(String[] args) {
SpringApplication.run(YouAskAdminApiApplication.class, args);
}
}
修改youask-admin-api模块中的application.yaml文件,调整mapper-locations
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
classpath*,表示可以加载所有符合模式的资源,即使它们来自不同的目录或JAR文件。
- 重新编译项目,并启动调试,正常运行。
实现代码生成器
- 命名为youask-codegen,加入到当前项目。添加完成后的项目结构如下。
- 使用MyBatis-Plus Generator需要在youask-codegen模块中引入
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.12</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.34</version>
</dependency>
这里需要说明一下,因为代码生成器只有在youask-codegen模块中使用,所以没有在父项目中添加依赖。
- entity目录中创建YaCodeGeneratorParameter对象,用于保存代码生成过程中需要使用到的参数
package com.obsidian.youask.codegen.model.entity;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class YaCodeGeneratorParameter {
/**
* 表名列表
*/
@Schema(description = "表名列表")
private List<String> tableNames;
/**
* 是否生成Controller
*/
@Schema(description = "是否生成Controller")
private Boolean generateController;
/**
* 是否生成Service
*/
@Schema(description = "是否生成Service")
private Boolean generateService;
/**
* 是否生成Mapper
*/
@Schema(description = "是否生成Mapper")
private Boolean generateMapper;
/**
* 是否生成Entity
*/
@Schema(description = "是否生成Entity")
private Boolean generateEntity;
/**
* 父包名
*/
@Schema(description = "父包名", defaultValue="com.obsidian.youask")
private String parentPackageName;
}
- 创建ICodeGenerateService接口定义
package com.obsidian.youask.codegen.service;
import com.obsidian.youask.codegen.model.entity.YaCodeGeneratorParameter;
public interface ICodeGenerateService {
/**
* 生成所有代码
* @param param 代码生成参数
* @return 生成成功返回true, 否则返回false
*/
Boolean generate(YaCodeGeneratorParameter param);
}
- 创建CodeGenerateService服务实现
除了entity,其他代码文件的文件名都自定义了生成规则,以Controller为例:
......
private Service.Builder initServiceStrategy(StrategyConfig.Builder builder, YaCodeGeneratorParameter param) {
var serviceBuilder = builder.serviceBuilder();
if (!param.getGenerateService()) {
serviceBuilder.disable();
} else {
serviceBuilder
.enableFileOverride()
.serviceTemplate("/templates/service.java")
.serviceImplTemplate("/templates/serviceImpl.java")
.convertServiceFileName((tableName) -> "I" + tableName.replace("Sys", "").replace("Ya", "") + "Service")
.convertServiceImplFileName((tableName) -> tableName.replace("Sys", "").replace("Ya", "") + "ServiceImpl");
}
return serviceBuilder;
}
......
数据表会带有前缀,sys表示系统数据表,ya表示业务表;除了实体以外,其他部份代码都不希望包含这些前缀(这个是个人习惯,entity包含前缀主要是为了避免重复,entity很有可能在多个模块中使用,而controller、service相对于来说只会在自己的模块中使用,没有其他特别原因),所以需要对生成的文件名进行格式化,具体做法就是把文件名前面的Sys和Ya删除。
.convertFileName((tableName) -> tableName.replace("Sys", "")
.replace("Ya", "") + "Controller");
- 增加controller模板
MyBatis-Plus Generato默认生成的controller、service、mapper、xml都是空白的,只有类的定义,内部不包含方法,下面是service生成的示例
所以需要自己编写模板,生成的时候把必要的方法都一起生成,这样可以减少很多重复开发工作。
在resources/templates中增加controller.java.flt文件,这个是freemaker的模板文件
package ${package.Controller};
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.obsidian.youask.YaConst;
import com.obsidian.youask.entity.common.YaPageResult;
import com.obsidian.youask.entity.common.YaResult;
import ${package.Service}.${table.serviceName};
import ${package.Entity}.${entity};
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PutMapping;
/**
* @author ${author}
* @since ${date}
*/
@Tag(name = "${table.controllerName}", description = "")
@Slf4j
@RestController
@RequestMapping("${table.controllerName?replace('Controller','')?lower_case}s")
public class ${table.controllerName} {
@Autowired
private ${table.serviceName} service;
@GetMapping()
public YaResult<List<${entity}>> getAll() {
return YaResult.success(service.list());
}
@GetMapping("{id}")
public YaResult<${entity}> getById(@PathVariable Long id) {
return YaResult.success(service.getById(id));
}
@PostMapping("query")
public YaPageResult<List<${entity}>> queryByCondition(@RequestBody Map<String, Object> condition,
@RequestParam(defaultValue = "1") Integer pageIndex, @RequestParam(defaultValue = "20") Integer pageSize) {
Page<${entity}> page = new Page<>(pageIndex, pageSize);
IPage<${entity}> userPage = service.page(page, new LambdaQueryWrapper<${entity}>());
return YaPageResult.success(userPage.getRecords(), pageIndex, pageSize, userPage.getTotal());
}
@ResponseStatus(HttpStatus.CREATED)
@PostMapping()
public YaResult<${entity}> add(@NotNull @RequestBody ${entity} obj) {
if (service.save(obj)) {
return YaResult.success(obj);
}
return YaResult.failed();
}
@PutMapping("{id}")
public YaResult<${entity}> modify(@PathVariable Long id, @NotNull @RequestBody ${entity} obj) {
obj.setUserId(id);
if (service.updateById(obj)) {
return YaResult.success(obj);
}
return YaResult.failed();
}
@PutMapping("deactive/{id}")
public YaResult<${entity}> deactivate(@PathVariable Long id) {
${entity} obj = service.getById(id);
obj.setStatus(YaConst.STATUS_DEACTIVE);
if (service.updateById(obj)) {
return YaResult.success(obj);
}
return YaResult.failed();
}
@PutMapping("active/{id}")
public YaResult<${entity}> activate(@PathVariable Long id) {
${entity} obj = service.getById(id);
obj.setStatus(YaConst.STATUS_ACTIVE);
if (service.updateById(obj)) {
return YaResult.success(obj);
}
return YaResult.failed();
}
@DeleteMapping("{id}")
public YaResult<Boolean> remove(@PathVariable Long id) {
if (service.removeById(id)) {
return YaResult.success(true);
}
return YaResult.failed();
}
}
freemaker的模板使用${}来引用外部变量,方法调用使用“?”符号。
网络上找了下,还真没有找到mybatis plus模版里变量的具体说明,这里我把使用到的变量整理了下:
- ${table}【具体可以参考代码:TableInfo.java - https://gitee.com/baomidou/mybatis-plus/blob/3.0/mybatis-plus-generator/src/main/java/com/baomidou/mybatisplus/generator/config/po/TableInfo.java】
变量 | 作用 |
${table.name} | 表名称 |
${table.comment} | 表注释 |
${table.entityName} | 实体名称 |
${table.mapperName} | mapper名称 |
${table.xmlName} | xml名称 |
${table.serviceName} | service名称 |
${table.serviceImplName} | serviceImpl名称 |
${table.controllerName} | controller名称 |
${table.havePrimaryKey} | 是否有主键 |
${table.fieldNames} | 字段名称集(就是select后面的所有字段) |
${table.fields} | 表字段 |
${table.commonFields} | 公共字段 |
${table.schemaName} | schema名称 |
- ${pakcage}【具体可以参考代码:PackageConfig.java - https://gitee.com/baomidou/mybatis-plus/blob/3.0/mybatis-plus-generator/src/main/java/com/baomidou/mybatisplus/generator/config/PackageConfig.java】
变量 | 作用 |
${pakcage.Entity} | 获取实体信息 |
${pakcage.Mapper} | 获取Mapper信息 |
${pakcage.Xml} | 获取Mapper Xml信息 |
${pakcage.ServiceImpl} | 获取ServiceImpl信息 |
${pakcage.Service} | 获取Service定义信息 |
${pakcage.Controller} | 获取Controller信息 |
${pakcage.ModuleName} | 获取包的模块名称 |
${pakcage.Parent} | 获取父包名 |
- ${package.Controller}【具体可以参考代码:Controller.java】
- ${package.Service}【具体可以参考代码:Service.java】
- ${package.Mapper}【具体可以参考代码:Mapper.java】
- ${package.Entity}【具体可以参考代码:Entity.java】
和模版相关的主要是renderData方法(
https://gitee.com/baomidou/mybatis-plus/tree/3.0/mybatis-plus-generator/src/main/java/com/baomidou/mybatisplus/generator/config/builder)
- 添加Service模板
package ${package.Service};
import ${package.Entity}.${entity};
import ${superServiceClassPackage};
/**
* <p>
* ${table.comment!} 服务定义
* </p>
*
* @author ${author}
* @since ${date}
*/
<#if kotlin>
interface ${table.serviceName} : ${superServiceClass}<${entity}>
<#else>
public interface ${table.serviceName} extends ${superServiceClass}<${entity}> {
/**
* 根据条件查询${table.comment!}
* @param condition 查询条件
* @param page 分页信息
* @return 返回用户列表
*/
public YaPageResult<${entity}> queryByCondition(Map<String, Object> condition, Integer pageIndex, Integer pageSize);
}
</#if>
- 增加ServiceImpl模板
package ${package.ServiceImpl};
import ${package.Entity}.${entity};
import ${package.Mapper}.${table.mapperName};
<#if generateService>
import ${package.Service}.${table.serviceName};
</#if>
import ${superServiceImplClassPackage};
import org.springframework.stereotype.Service;
/**
* <p>
* ${table.comment!} 服务实现类
* </p>
*
* @author ${author}
* @since ${date}
*/
@Service
public class ${table.serviceImplName} extends ${superServiceImplClass}<${table.mapperName}, ${entity}><#if generateService> implements ${table.serviceName}</#if> {
@Override
public YaPageResult<${entity}> queryByCondition(Map<String, Object> condition, Integer pageIndex, Integer pageSize) {
IPage<${entity}> page = new Page<>(pageIndex, pageSize);
IPage<${entity}> result = baseMapper.selectByCondition(page, condition);
return YaPageResult.success(result.getRecords(), pageIndex, pageSize, result.getTotal());
}
}
- 增加Mapper模板
package ${package.Mapper};
<#list importMapperFrameworkPackages as pkg>
import ${pkg};
</#list>
<#if importMapperJavaPackages?size !=0>
<#list importMapperJavaPackages as pkg>
import ${pkg};
</#list>
</#if>
/**
* <p>
* ${table.comment!} Mapper 接口
* </p>
*
* @author ${author}
* @since ${date}
*/
<#if mapperAnnotationClass??>
@${mapperAnnotationClass.simpleName}
</#if>
public interface ${table.mapperName} extends ${superMapperClass}<${entity}> {
/**
* 根据条件查询${table.comment!}
* @param condition 查询条件
* @return 返回用户列表
*/
IPage<${entity}> selectByCondition(IPage<entity> page, Map<String, Object> condition);
<#list mapperMethodList as m>
/**
* generate by ${m.indexName}
*
<#list m.tableFieldList as f>
* @param ${f.propertyName} ${f.comment}
</#list>
*/
${m.method}
</#list>
}
- 增加mapper.xml模板
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${package.Mapper}.${table.mapperName}">
<#if baseResultMap>
<!-- 通用查询映射结果 -->
<resultMap id="${table.mapperName?replace('Mapper','Map')}"
extends="com.obsidian.youask.common.mapper.EntityMapper.EntityMap"
type="${package.Entity}.${entity}">
<#list table.fields as field>
<#if field.keyFlag><#--生成主键排在第一位-->
<id column="${field.name}" property="${field.propertyName}" />
</#if>
</#list>
<#list table.fields as field>
<#if !field.keyFlag><#--生成普通字段 -->
<result column="${field.name}" property="${field.propertyName}" />
</#if>
</#list>
</resultMap>
</#if>
<select id="selectByCondition" resultMap="${table.mapperName?replace('Mapper','Map')}" parameterType="java.util.Map">
select * from ${table.name} where is_deleted = 0
<!--动态查询条件-->
</select>
<#if baseColumnList>
<!-- 通用查询结果列 -->
<sql id="${table.mapperName?replace('Mapper','ColumnList')}">
${table.fieldNames}
</sql>
</#if>
</mapper>
- 这里没有增加entity的模板,暂时还不需要,如果有修改的需求,可以参考MyBatis-Plus的模板:entity.java.ftl
- 修改项目appliation.yaml,将端口改为8011,并增加文件输出路径·
server:
port: 8011
youask:
codegen:
output_dir: /Users/root/git/obsidian/generated_codes
- 启动youask-codegen的调试,在OpenAPI页面上找到generate方法,并输入如下参数:
{
"tableNames": ["sys_user"],
"generateController": true,
"generateService": true,
"generateMapper": true,
"generateEntity": true,
"parentPackageName": "com.obsidian.youask"
}
点击发送即可在输出文件夹中看到生成的代码。