背景
我们在日常开发过程,大多数都是使用主流MVC架构,如下图所示。
从图中可以看出,我们主要的业务代码基本都是从Controller->Service->Dao/Mapper,由Dao/Mapper则通过Mybatis连接数据库连接池的方式与数据库进行指令数据交互。
因此,可以知道一张业务表一般可以对应上述一套后台代码,如果业务表众多就会对应多套后台代码,很明显这是重复且麻烦的代码生成工作。基于此,我们考虑能否实现一个与Ideaj代码生成工具有相似功能的工具类,根据我们定义的后台代码模板,来帮助我们个性化生成我们想要的后台代码。
技术路线
我们基于Springboot Gradle构建工程,将工程分为两部分模块。
(1)代码生成器模块:主要基于Freemarker和Velocity模板引擎,对需要的后台代码内容和风格自定义ftl模板。在Generator的主程序中连接数据库,加载模板文件,执行引擎方法,生成数据库表对应的后台代码到指定包路径中;
(2)服务发布模块:基于已生成的后台代码,使用SpringBoot进行发布,发布后通过浏览器访问,验证生成的后台代码是否满足指定业务表的增删改查(全量和分页查询)。
模块解析
(1)公共配置依赖:主要为build.gradle和application.properties配置
build.gradle:
plugins {
id 'java'
id 'org.springframework.boot' version '2.1.3.RELEASE'
}
group = 'com.example.mybatis'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
tasks.withType(JavaCompile) {
options.encoding = "utf-8"
}
repositories {
mavenCentral()
}
configurations {
compileOnly { //仅在编译时依赖,此处为lombok使用,相关注解在编译时生成getter、setter等
extendsFrom annotationProcessor
}
}
dependencies {
compile 'com.alibaba:druid:1.2.8'
compile 'mysql:mysql-connector-java:5.1.47'
compile (group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: '1.1.1'){
exclude group:'org.springframework.boot', module:'spring-boot-autoconfigure'
exclude group:'org.springframework.boot', module:'spring-boot-starter-logging'
exclude group:'org.springframework.boot', module: 'spring-boot-starter'
}
compile (group:'com.baomidou', name:'mybatis-plus-boot-starter', version:'3.5.1'){
exclude group:'org.springframework.boot', module:'spring-boot-starter-jdbc'
exclude group:'org.springframework.boot', module:'spring-boot-autoconfigure'
exclude group:'org.springframework.boot', module: 'spring-boot-starter'
}
compile group: 'org.springframework.boot', name: 'spring-boot-starter-parent', version: '2.0.7.RELEASE'
compile group: 'org.springframework.boot', name: 'spring-boot-starter', version: '2.0.7.RELEASE'
compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.0.7.RELEASE'
compileOnly 'org.projectlombok:lombok:1.18.6'
compile group: 'junit', name: 'junit', version: '4.12'
compile (group:'com.github.pagehelper', name:'pagehelper-spring-boot-starter', version:'1.3.1'){
exclude group:'org.mybatis.spring.boot', module: 'mybatis-spring-boot-starter'
exclude group:'org.springframework.boot', module:'spring-boot-autoconfigure'
exclude group:'org.springframework.boot', module: 'spring-boot-starter'
}
compile 'org.apache.commons:commons-io:1.3.2'
compile group: 'io.springfox', name: 'springfox-swagger2', version:'2.9.2'
compile group: 'io.springfox', name: 'springfox-swagger-ui', version:'2.9.2'
compile('com.baomidou:mybatis-plus-generator:3.5.1')
compile 'org.apache.velocity:velocity-engine-core:2.2'
compile 'org.freemarker:freemarker:2.3.28'
}
application.properties:
spring.application.name=mybatis-generator-application
server.port=8080
# druid
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# druid参数调优(可选)
# 初始化大小,最小,最大
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# 配置获取连接等待超时的时间
spring.datasource.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
# 测试连接
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
# 配置监控统计拦截的filters
spring.datasource.filters=stat
# asyncInit是1.1.4中新增加的配置,如果有initialSize数量较多时,打开会加快应用启动时间
spring.datasource.asyncInit=true
#数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url= jdbc:mysql://localhost:3306/mysql?serverTimeZone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=
#分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。 你可以配置helperDialect 属性来指定分页插件使用哪种方言。
pagehelper.helper-dialect=mysql
#分页合理化参数,默认值为 false 。当该参数设置为 true 时, pageNum<=0 时会查询第一页, pageNum>pages (超过总数时),会查询最后一页。
pagehelper.reasonable=true
#支持通过Mapper接口参数传递page参数,默认值为falset
pagehelper.support-methods-arguments=true
#默认值为 false ,当该参数设置为 true 时,如果 pageSize=0 或者 RowBounds.limit =0 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page 类型)。
pagehelper.pageSizeZero=true
#为了支持 startPage(Object params) 方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值
pagehelper.params=count=countSql
公共包
BaseController.java:
package com.common;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class BaseController extends AbstractController {
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
return null;
}
/**
* 响应返回结果
* 参数result 类型可自行更换
* 如:
* boolean result -> return result == true ? success() : error();
* int result -> return result > 0 ? success() : error();
*/
protected AjaxResult toAjax(boolean result) {
return result == true ? success() : error();
}
protected AjaxResult toAjax(int result) {
return result > 0 ? success() : error();
}
/**
* 返回成功
*/
public AjaxResult success() {
return AjaxResult.success();
}
/**
* 返回失败
*/
public AjaxResult error() {
return AjaxResult.error();
}
/**
* 返回成功消息
*/
public AjaxResult success(String message)
{
return AjaxResult.success(message);
}
/**
* 返回失败消息
*/
public AjaxResult error(String message)
{
return AjaxResult.error(message);
}
/**
* 返回错误码消息
*/
public AjaxResult error(int code, String message)
{
return AjaxResult.error(code, message);
}
}
AjaxResult.java:
package com.common;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
/**
* 操作消息提醒
*
*
*/
public class AjaxResult extends HashMap<String, Object>
{
private static final long serialVersionUID = 1L;
/** 状态码 */
public static final String CODE_TAG = "code";
/** 返回内容 */
public static final String MSG_TAG = "msg";
/** 数据对象 */
public static final String DATA_TAG = "data";
/**
* 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
*/
public AjaxResult()
{
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
*/
public AjaxResult(int code, String msg)
{
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
* @param data 数据对象
*/
public AjaxResult(int code, String msg, Object data)
{
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (null != data)
{
super.put(DATA_TAG, data);
}
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static AjaxResult success()
{
return AjaxResult.success("操作成功");
}
/**
* 返回成功数据
*
* @return 成功消息
*/
public static AjaxResult success(Object data)
{
return AjaxResult.success("操作成功", data);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @return 成功消息
*/
public static AjaxResult success(String msg)
{
return AjaxResult.success(msg, null);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static AjaxResult success(String msg, Object data)
{
return new AjaxResult(HttpStatus.SUCCESS, msg, data);
}
/**
* 返回错误消息
*
* @return
*/
public static AjaxResult error()
{
return AjaxResult.error("操作失败");
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult error(String msg)
{
return AjaxResult.error(msg, null);
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static AjaxResult error(String msg, Object data)
{
return new AjaxResult(HttpStatus.ERROR, msg, data);
}
/**
* 返回错误消息
*
* @param code 状态码
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult error(int code, String msg)
{
return new AjaxResult(code, msg, null);
}
/**
* 方便链式调用
*
* @param key 键
* @param value 值
* @return 数据对象
*/
@Override
public AjaxResult put(String key, Object value)
{
super.put(key, value);
return this;
}
}
BaseEntity.java:
package com.common;
import java.io.Serializable;
public class BaseEntity implements Serializable {
private int page;
private int limit;
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getLimit() {
return limit;
}
public void setLimit(int limit) {
this.limit = limit;
}
@Override
public String toString() {
return "BaseEntity{" +
"page=" + page +
", limit=" + limit +
'}';
}
}
HttpStatus.java:
package com.common;
/**
* 返回状态码
*
*
*/
public class HttpStatus
{
/**
* 操作成功
*/
public static final int SUCCESS = 200;
/**
* 对象创建成功
*/
public static final int CREATED = 201;
/**
* 请求已经被接受
*/
public static final int ACCEPTED = 202;
/**
* 操作已经执行成功,但是没有返回数据
*/
public static final int NO_CONTENT = 204;
/**
* 资源已被移除
*/
public static final int MOVED_PERM = 301;
/**
* 重定向
*/
public static final int SEE_OTHER = 303;
/**
* 资源没有被修改
*/
public static final int NOT_MODIFIED = 304;
/**
* 参数列表错误(缺少,格式不匹配)
*/
public static final int BAD_REQUEST = 400;
/**
* 未授权
*/
public static final int UNAUTHORIZED = 401;
/**
* 访问受限,授权过期
*/
public static final int FORBIDDEN = 403;
/**
* 资源,服务未找到
*/
public static final int NOT_FOUND = 404;
/**
* 不允许的http方法
*/
public static final int BAD_METHOD = 405;
/**
* 资源冲突,或者资源被锁
*/
public static final int CONFLICT = 409;
/**
* 不支持的数据,媒体类型
*/
public static final int UNSUPPORTED_TYPE = 415;
/**
* 系统内部错误
*/
public static final int ERROR = 500;
/**
* 接口未实现
*/
public static final int NOT_IMPLEMENTED = 501;
}
(2)代码生成器模块:该模块核心是定义ftl文件,然后主程序加载并执行模板引擎。
Generator.java
package com.generator;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import com.baomidou.mybatisplus.generator.fill.Column;
import com.baomidou.mybatisplus.generator.fill.Property;
import java.io.File;
import java.util.*;
/**
* mybatis-plus
*/
public class Generator {
//输出文件的作者注释
public static final String auth = "test";
//项目路径
public static final String finalProjectPath = "F:\\workspaces4ideaj";
//输出.java文件的根目录
public static final String javaRootPath = "/src/main/java";
//输出java文件的包目录
public static final String parent = "com.test";
//输出mapper.xml文件的包目录
public static final String mapperPath = "/src/main/resources/mapper";
//数据库信息
public static final String url = "jdbc:mysql://127.0.0.1:3306/mysql?useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2b8&useSSL=false";
public static final String username = "root";
public static final String password = "";
public static void main(String[] args) {
//创建生成器,连接数据库
FastAutoGenerator.create(url, username, password)
//全局配置
.globalConfig(builder -> {
builder.author(auth)
//swagger模式
.enableSwagger()
//关闭覆盖已生成文件功能,解决自定义模板被覆盖问题
//.fileOverride()
.outputDir(finalProjectPath + javaRootPath) // 指定输出目录
.disableOpenDir();
})
.packageConfig((scanner, builder) ->
builder
.other("vo")
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, finalProjectPath + mapperPath))
.parent(parent + "." + scanner.apply("Please enter a package name:")
))
//策略配置
.strategyConfig((scanner, builder) -> builder.addInclude(
getTables(scanner.apply("Please enter a table name separated by dot, all input all: "))
)
// 设置过滤表前缀
.addTablePrefix("sys_")
.controllerBuilder()
.serviceBuilder()
.mapperBuilder()
.entityBuilder()
.disableSerialVersionUID()
.enableLombok() // 开启Lombok
.enableTableFieldAnnotation()
.addTableFills(new Column("create_time", FieldFill.INSERT))
.addTableFills(new Property("update_time", FieldFill.INSERT_UPDATE))
.idType(IdType.AUTO)
.build()
)
// 自定义注入模
// .vm为velocity引擎的,.ftl为freemarker引擎的
.injectionConfig(consumer -> {
Map<String, String> customFile = new HashMap<>();
customFile.put("Controller.java", "/templates/ftl/controller.java.ftl");
customFile.put("Service.java", "/templates/ftl/service.java.ftl");
customFile.put("ServiceImpl.java", "/templates/ftl/serviceImpl.java.ftl");
customFile.put("Mapper.java", "/templates/ftl/mapper.java.ftl");
customFile.put(".java", "/templates/ftl/entity.java.ftl");
customFile.put("Mapper.xml", "/templates/ftl/mapper.xml.ftl");
customFile.put("Vo.java", "/templates/ftl/vo.java.ftl");
consumer.customFile(customFile);
})
/*//自定义模板模式
.templateConfig(builder -> {
//禁止所有模板
builder.disable(TemplateType.ENTITY);
builder.disable(TemplateType.XML);
builder.disable(TemplateType.MAPPER);
builder.disable(TemplateType.SERVICE);
builder.disable(TemplateType.SERVICEIMPL);
builder.disable(TemplateType.CONTROLLER);
})
*/
//使用Framemarker引擎模板,默认Velocity引擎模板
.templateEngine(new EnhanceFreemarkerTemplateEngine()).execute();
}
//处理输入为all所有表
protected static List<String> getTables(String tables) {
return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));
}
/**
* 代码生成器支持自定义[DTO\VO等]模版
*/
public final static class EnhanceFreemarkerTemplateEngine extends FreemarkerTemplateEngine {
@Override
protected void outputCustomFile(Map<String, String> customFile, TableInfo tableInfo, Map<String, Object> objectMap) {
String entityName = tableInfo.getEntityName();
String otherPath = this.getPathInfo(OutputFile.other);
String controller = this.getPathInfo(OutputFile.controller);
String service = this.getPathInfo(OutputFile.service);
String serviceImpl = this.getPathInfo(OutputFile.serviceImpl);
String mapper = this.getPathInfo(OutputFile.mapper);
String entity = this.getPathInfo(OutputFile.entity);
String mapperXml = this.getPathInfo(OutputFile.mapperXml);
customFile.forEach((key, value) -> {
//指定输出文件路径
if ("Controller.java".equals(key)) {
String fileName = String.format(controller + File.separator + entityName + "%s", key);
outputFile(new File(fileName), objectMap, value);
}
if ("Service.java".equals(key)) {
String fileName = String.format(service + File.separator + "I" + entityName + "%s", key);
outputFile(new File(fileName), objectMap, value);
}
if ("ServiceImpl.java".equals(key)) {
String fileName = String.format(serviceImpl + File.separator + entityName + "%s", key);
outputFile(new File(fileName), objectMap, value);
}
if ("Mapper.java".equals(key)) {
String fileName = String.format(mapper + File.separator + entityName + "%s", key);
outputFile(new File(fileName), objectMap, value);
}
if (".java".equals(key)) {
String fileName = String.format(entity + File.separator + entityName + "%s", key);
outputFile(new File(fileName), objectMap, value);
}
if ("Mapper.xml".equals(key)) {
String fileName = String.format(mapperXml + File.separator + entityName + "%s", key);
outputFile(new File(fileName), objectMap, value);
}
// 过滤掉非vo或dto对象自定义文件
if ("Vo.java".equals(key)) {
String fileName = String.format(otherPath + File.separator + entityName + "%s", key);
outputFile(new File(fileName), objectMap, value);
}
if ("Dto.java".equals(key)) {
String fileName = String.format(otherPath + File.separator + entityName + "%s", key);
outputFile(new File(fileName), objectMap, value);
}
});
}
}
}
src/main/resources/mybatis-generator.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration PUBLIC
"-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" >
<generatorConfiguration>
<!-- 本地数据库驱动程序jar包的全路径 -->
<!--<classPathEntry location="F:\repository\mysql\mysql-connector-java\5.1.44\mysql-connector-java-5.1.44.jar"/>-->
<context id="context" targetRuntime="MyBatis3Simple">
<!--java代码编码格式-->
<property name="javaFileEncoding" value="UTF-8"/>
<!--格式化java代码-->
<property name="javaFormatter" value="org.mybatis.generator.api.dom.DefaultJavaFormatter"/>
<!--格式化xml代码-->
<property name="xmlFormatter" value="org.mybatis.generator.api.dom.DefaultXmlFormatter"/>
<commentGenerator>
<!-- 是否取消注释 -->
<property name="suppressAllComments" value="true"/>
<!-- 是否生成注释代时间戳-->
<property name="suppressDate" value="true"/>
</commentGenerator>
<!-- 数据库的相关配置 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://127.0.0.1:3306/mysql?useUnicode=true"
userId="root"
password=""/>
<javaTypeResolver>
<!-- 是否使用bigDecimal, false可自动转化以下类型(Long, Integer, Short, etc.) -->
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- 实体类生成的位置 -->
<javaModelGenerator targetPackage="com.core" targetProject="src/main/java">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false"/>
<!-- 从数据库返回的值被清理前后的空格 -->
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- *Mapper.xml 文件的位置 -->
<sqlMapGenerator targetPackage="mapping" targetProject="src/main/resources">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false"/>
</sqlMapGenerator>
<!-- Mapper 接口文件的位置,注意type的值这里是熟悉的xml格式 -->
<!--
type:选择怎么生成mapper接口(在MyBatis3/MyBatis3Simple下):
1,ANNOTATEDMAPPER:会生成使用Mapper接口+Annotation的方式创建(SQL生成在annotation中),不会生成对应的XML;
2,MIXEDMAPPER:使用混合配置,会生成Mapper接口,并适当添加合适的Annotation,但是XML会生成在XML中;
3,XMLMAPPER:会生成Mapper接口,接口完全依赖XML;
注意,如果context是MyBatis3Simple:只支持ANNOTATEDMAPPER和XMLMAPPER
-->
<javaClientGenerator targetPackage="com.core" targetProject="src/main/java" type="XMLMAPPER">
<property name="enableSubPackages" value="false"/>
</javaClientGenerator>
<!-- 相关表的配置 -->
<table tableName="t_order" enableCountByExample="false" enableDeleteByExample="false"
enableSelectByExample="false"
enableUpdateByExample="false" selectByExampleQueryId="true"/>
</context>
</generatorConfiguration>
src/main/resources/templates/ftl/文件
controller.java.ftl:
package ${package.Controller};
import ${package.Entity}.${entity};
import ${package.Other}.${entity}Vo;
import ${package.Service}.${table.serviceName};
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.*;
import io.swagger.annotations.*;
import com.common.AjaxResult;
import com.common.BaseController;
import java.util.List;
import java.util.Map;
<#if superControllerClassPackage??>
import ${superControllerClassPackage};
</#if>
/**
* ${table.comment!} 前端控制器
* @author ${author}
* @since ${date}
*/
@Api(tags = "${table.comment!}文档")
@RestController
@RequestMapping("<#if package.ModuleName?? && package.ModuleName != "">/${package.ModuleName}</#if>/<#if controllerMappingHyphenStyle??>${controllerMappingHyphen}<#else>${table.entityPath}</#if>")
public class ${table.controllerName} extends BaseController{
@Resource(name="${entity?lower_case}Service")
private ${table.serviceName} ${entity?lower_case}Service;
/**
*
* 分页查詢列表
* @return page
*/
@ApiOperation(value = "分页查询列表", httpMethod = "GET", notes = "分页查询列表")
@ApiImplicitParam(paramType = "body", name = "${entity?lower_case}Vo", value = "对象", dataType = "${entity}Vo")
@ApiResponses({
@ApiResponse(code = 200, message = "响应对象", response = ${entity}.class)
})
@GetMapping("/queryListByPagging")
public AjaxResult queryListByPagging(${entity}Vo ${entity?lower_case}Vo) {
Map<String,Object> map = ${entity?lower_case}Service.select${entity}ListByPagging(${entity?lower_case}Vo);
return AjaxResult.success(map);
}
/**
*
* 查詢列表
* @return page
*/
@ApiOperation(value = "查询列表", httpMethod = "GET", notes = "查询列表")
@ApiImplicitParam(paramType = "body", name = "${entity?lower_case}Vo", value = "对象", dataType = "${entity}Vo")
@ApiResponses({
@ApiResponse(code = 200, message = "响应对象", response = ${entity}.class)
})
@GetMapping("/queryList")
public AjaxResult queryList(${entity}Vo ${entity?lower_case}Vo) {
List<${entity}> list = ${entity?lower_case}Service.select${entity}List(${entity?lower_case}Vo);
return AjaxResult.success(list);
}
/**
* 新增
*/
@ApiOperation(value = "新增", httpMethod = "POST")
@ApiImplicitParam(paramType = "body", name = "${entity?lower_case}", value = "对象", dataType = "${entity}")
@PostMapping("/insert${entity}")
public AjaxResult insert${entity}(@RequestBody ${entity} ${entity?lower_case}){
return toAjax(${entity?lower_case}Service.insert${entity}(${entity?lower_case}));
}
/**
* 修改
*/
@ApiOperation(value = "修改", httpMethod = "POST")
@ApiImplicitParam(paramType = "body", name = "${entity?lower_case}", value = "对象", dataType = "${entity}")
@PostMapping("/update${entity}")
public AjaxResult update${entity}(@RequestBody ${entity} ${entity?lower_case}){
return toAjax(${entity?lower_case}Service.update${entity}(${entity?lower_case}));
}
/**
* 删除
*/
@ApiOperation(value = "删除", httpMethod = "POST")
@ApiImplicitParam(paramType = "query", name = "id", value = "主键id")
@PostMapping("/delete${entity}")
public AjaxResult delete${entity}(@RequestParam("id") Integer id){
return toAjax(${entity?lower_case}Service.delete${entity}(id));
}
}
service.java.ftl:
package ${package.Service};
import ${package.Entity}.${entity};
import ${package.Other}.${entity}Vo;
import ${superServiceClassPackage};
import java.util.List;
import java.util.Map;
/**
* ${table.comment!} 服务类
* @author ${author}
* @since ${date}
*/
public interface ${table.serviceName} extends ${superServiceClass}<${entity}> {
/**
* 查询${table.comment!}数量
*/
public Integer select${entity}Count(${entity}Vo ${entity?lower_case}Vo);
/**
* 查詢${table.comment!}列表
* @return page
*/
List<${entity}> select${entity}List(${entity}Vo ${entity?lower_case}Vo);
/**
* 分页查詢${table.comment!}列表
* @return page
*/
Map<String,Object> select${entity}ListByPagging(${entity}Vo ${entity?lower_case}Vo);
/**
* 新增${table.comment!}
*/
int insert${entity}(${entity} ${entity?lower_case});
/**
* 修改${table.comment!}
*/
int update${entity}(${entity} ${entity?lower_case});
/**
* 删除${table.comment!}
*/
int delete${entity}(Integer id);
}
serviceImpl.java.ftl:
package ${package.ServiceImpl};
import ${package.Entity}.${entity};
import ${package.Mapper}.${table.mapperName};
import ${package.Service}.${table.serviceName};
import ${superServiceImplClassPackage};
import ${package.Other}.${entity}Vo;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
/**
* ${table.comment!}服务实现类
* @author ${author}
* @since ${date}
*/
@Slf4j
@Service("${entity?lower_case}Service")
public class ${table.serviceImplName} extends ${superServiceImplClass}<${table.mapperName}, ${entity}> implements ${table.serviceName} {
@Autowired
private ${table.mapperName} ${entity?lower_case}Mapper;
/**
* 查询${table.comment!}数量
*/
@Override
public Integer select${entity}Count(${entity}Vo ${entity?lower_case}Vo) {
return ${entity?lower_case}Mapper.queryCount(${entity?lower_case}Vo);
}
/**
*
* 查詢${table.comment!}列表
*
* @return page
*/
@Override
public List<${entity}> select${entity}List(${entity}Vo ${entity?lower_case}Vo) {
return ${entity?lower_case}Mapper.queryList(${entity?lower_case}Vo);
}
/**
* 分页查詢${table.comment!}列表
* @return Map<String,Object>
*/
public Map<String,Object> select${entity}ListByPagging(${entity}Vo ${entity?lower_case}Vo){
Integer count = select${entity}Count(${entity?lower_case}Vo);
Map<String,Object> map = new HashMap<String,Object>();
List<${entity}> list = null;
if(null != count && count > 0){
PageInfo pageInfo = null;
try {
PageHelper.startPage(${entity?lower_case}Vo.getPage(), ${entity?lower_case}Vo.getLimit());
list = select${entity}List(${entity?lower_case}Vo);
}finally {
PageHelper.clearPage();
}
}
map.put("count", count);
map.put("list", list);
return map;
}
/**
* 新增${table.comment!}
*
* @return 结果
*/
@Override
public int insert${entity}(${entity} ${entity?lower_case}) {
return ${entity?lower_case}Mapper.insert(${entity?lower_case});
}
/**
* 修改${table.comment!}
*
* @return 结果
*/
@Override
public int update${entity}(${entity} ${entity?lower_case}) {
return ${entity?lower_case}Mapper.updateById(${entity?lower_case});
}
/**
* 删除${table.comment!}
*
* @return 结果
*/
@Override
public int delete${entity}(Integer id) {
return ${entity?lower_case}Mapper.deleteById(id);
}
}
entity.java.ftl:
package ${package.Entity};
<#list table.importPackages as pkg>
import ${pkg};
</#list>
<#if swagger>
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
</#if>
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
/**
* ${table.comment!}
* @author ${author}
* @since ${date}
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
<#if chainModel>
@Accessors(chain = true)
</#if>
<#if table.convert>
@TableName("${schemaName}${table.name}")
</#if>
<#if swagger>
@ApiModel(value = "${entity}对象", description = "${table.comment!}")
</#if>
public class ${entity} implements Serializable {
private static final long serialVersionUID = 1L;
<#-- ---------- BEGIN 字段循环遍历 ---------->
<#list table.fields as field>
<#if field.keyFlag>
<#assign keyPropertyName="${field.propertyName}"/>
</#if>
<#if field.comment!?length gt 0>
<#if swagger>
@ApiModelProperty("${field.comment}")
<#else>
/**
* ${field.comment}
*/
</#if>
</#if>
<#if field.keyFlag>
<#-- 主键 -->
<#if field.keyIdentityFlag>
@TableId(value = "${field.annotationColumnName}", type = IdType.AUTO)
<#elseif idType??>
@TableId(value = "${field.annotationColumnName}", type = IdType.${idType})
<#elseif field.convert>
@TableId("${field.annotationColumnName}")
</#if>
<#-- 普通字段 -->
<#elseif field.fill??>
<#-- ----- 存在字段填充设置 ----->
<#if field.convert>
@TableField(value = "${field.annotationColumnName}", fill = FieldFill.${field.fill})
<#else>
@TableField(fill = FieldFill.${field.fill})
</#if>
<#elseif field.convert>
@TableField("${field.annotationColumnName}")
</#if>
<#-- 乐观锁注解 -->
<#if field.versionField>
@Version
</#if>
<#-- 逻辑删除注解 -->
<#if field.logicDeleteField>
@TableLogic
</#if>
private ${field.propertyType} ${field.propertyName};
</#list>
<#------------ END 字段循环遍历 ---------->
<#if !entityLombokModel>
<#list table.fields as field>
<#if field.propertyType == "boolean">
<#assign getprefix="is"/>
<#else>
<#assign getprefix="get"/>
</#if>
public ${field.propertyType} ${getprefix}${field.capitalName}() {
return ${field.propertyName};
}
<#if chainModel>
public ${entity} set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
<#else>
public void set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
</#if>
this.${field.propertyName} = ${field.propertyName};
<#if chainModel>
return this;
</#if>
}
</#list>
</#if>
<#if entityColumnConstant>
<#list table.fields as field>
public static final String ${field.name?upper_case} = "${field.name}";
</#list>
</#if>
<#if activeRecord>
@Override
public Serializable pkVal() {
<#if keyPropertyName??>
return this.${keyPropertyName};
<#else>
return null;
</#if>
}
</#if>
<#if !entityLombokModel>
@Override
public String toString() {
return "${entity}{" +
<#list table.fields as field>
<#if field_index==0>
"${field.propertyName}=" + ${field.propertyName} +
<#else>
", ${field.propertyName}=" + ${field.propertyName} +
</#if>
</#list>
"}";
}
</#if>
}
vo.java.ftl:
package ${package.Other};
<#list table.importPackages as pkg>
import ${pkg};
</#list>
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.swagger.annotations.ApiModel;
import com.common.BaseEntity;
/**
* <p>
* ${table.comment!}
* </p>
*
* @author ${author}
* @since ${date}
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "${entity}对象", description = "${table.comment!}")
public class ${entity}Vo extends BaseEntity {
private static final long serialVersionUID = 1L;
<#list table.fields as field>
private ${field.propertyType} ${field.propertyName};
</#list>
@Override
public String toString() {
return "TOrderVo{" +
<#list table.fields as field>
"${field.propertyName}=" + ${field.propertyName} + "," +
</#list>
"}";
}
}
mapper.java.ftl:
package ${package.Mapper};
import ${package.Entity}.${entity};
import ${package.Other}.${entity}Vo;
import ${superMapperClassPackage};
import java.util.List;
<#if mapperAnnotation>
import org.apache.ibatis.annotations.Mapper;
</#if>
/**
* ${table.comment!} Mapper 接口
* @author ${author}
* @since ${date}
*/
<#if mapperAnnotation>
@Mapper
</#if>
public interface ${table.mapperName} extends ${superMapperClass}<${entity}> {
/**
* 查詢${table.comment!}数量
* @return Integer
*/
Integer queryCount(${entity}Vo ${entity?lower_case}Vo);
/**
* 查詢${table.comment!}列表
* @return List<${entity}>
*/
List<${entity}> queryList(${entity}Vo ${entity?lower_case}Vo);
}
mapper.xml.ftl:
<?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 enableCache>
<!-- 开启二级缓存 -->
<cache type="${cacheClassName}"/>
</#if>
<!-- 通用查询映射结果 -->
<resultMap id="ResultMap" type="${package.Entity}.${entity}">
<#list table.fields as field>
<#if field.keyFlag><#--生成主键排在第一位-->
<id column="${field.name}" property="${field.propertyName}" />
</#if>
</#list>
<#list table.commonFields as field><#--生成公共字段 -->
<result column="${field.name}" property="${field.propertyName}" />
</#list>
<#list table.fields as field>
<#if !field.keyFlag><#--生成普通字段 -->
<result column="${field.name}" property="${field.propertyName}" />
</#if>
</#list>
</resultMap>
<!-- 通用查询结果列 -->
<sql id="BaseColumnList">
<#list table.commonFields as field>
${field.columnName},
</#list>
${table.fieldNames}
</sql>
<!-- 通用查询过滤列 -->
<sql id="Base_Column_Query_List">
<#list table.fields as field>
<if test="${field.propertyName} != null and ${field.propertyName} != ''">
AND ${field.name} = #${r"{"}${field.propertyName}${r"}"}
</if>
</#list>
<#list table.commonFields as field><#--生成公共字段 -->
<if test="${field.propertyName} != null and ${field.propertyName} != ''">
AND ${field.name} = #${r"{"}${field.propertyName}${r"}"}
</if>
</#list>
</sql>
<select id="queryCount" resultType="java.lang.Integer">
select COUNT(1)
from ${table.name}
<where>
<include refid="Base_Column_Query_List"/>
</where>
</select>
<select id="queryList" resultMap="ResultMap">
select
<include refid="BaseColumnList"/>
from ${table.name}
<where>
<include refid="Base_Column_Query_List"/>
</where>
</select>
</mapper>
然后启动mysql服务,创建一张t_bill表,并新增4条记录。
CREATE TABLE t_bill(
bill_id INT NOT NULL AUTO_INCREMENT,
bill_title VARCHAR(100) NOT NULL,
bill_author VARCHAR(40) NOT NULL,
bill_date DATE,
PRIMARY KEY ( bill_id )
);
mysql> select * from t_bill;
+---------+------------+-------------+-----------+
| bill_id | bill_title | bill_author | bill_date |
+---------+------------+-------------+-----------+
| 1 | haha | test | NULL |
| 2 | haha | test | NULL |
| 3 | haha | test | NULL |
| 4 | haha55 | test | NULL |
+---------+------------+-------------+-----------+
4 rows in set (0.00 sec)
执行Generator工具类,输入需要创建的子包名、表名,执行结束即可看到工程中自动生成该表的后台代码:
(3)服务发布模块:该模块启动Springboot工程,将生成的后台代码发布到tomcat后通过浏览器访问。
BootApplicationl.java:
package com;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@SpringBootApplication
@EnableSwagger2
@MapperScan(value="com.test")
public class BootApplication {
public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}
}
然后可通过浏览器访问查询/分页查询方法服务
http://localhost:8080/t-bill/queryList
返回
{"msg":"操作成功","code":200,"data":[{"billId":1,"billTitle":"haha","billAuthor":"test","billDate":null},{"billId":2,"billTitle":"haha","billAuthor":"test","billDate":null},{"billId":3,"billTitle":"haha","billAuthor":"test","billDate":null},{"billId":4,"billTitle":"haha55","billAuthor":"test","billDate":null}]}
http://localhost:8080/t-bill/queryListByPagging?page=1&limit=2
返回
{"msg":"操作成功","code":200,"data":{"count":4,"list":[{"billId":1,"billTitle":"haha","billAuthor":"test","billDate":null},{"billId":2,"billTitle":"haha","billAuthor":"test","billDate":null}]}}
http://localhost:8080/t-bill/queryListByPagging?page=2&limit=2
返回
{"msg":"操作成功","code":200,"data":{"count":4,"list":[{"billId":3,"billTitle":"haha","billAuthor":"test","billDate":null},{"billId":4,"billTitle":"haha55","billAuthor":"test","billDate":null}]}}
也可使用postman等工具测试insert,update,delete服务
http://localhost:8080/t-bill/insertTBill
{
"billId":888,
"billTitle":"888",
"billAuthor":"888",
"LocalDate":null
}
返回
{
"msg": "操作成功",
"code": 200
}
mysql> select * from t_bill;
+---------+------------+-------------+-----------+
| bill_id | bill_title | bill_author | bill_date |
+---------+------------+-------------+-----------+
| 1 | haha | test | NULL |
| 2 | haha | test | NULL |
| 3 | haha | test | NULL |
| 4 | haha55 | test | NULL |
| 888 | 888 | 888 | NULL |
+---------+------------+-------------+-----------+
5 rows in set (0.03 sec)
http://localhost:8080/t-bill/updateTBill
{
"billId":888,
"billTitle":"update888",
"billAuthor":"update888",
"LocalDate":null
}
返回
{
"msg": "操作成功",
"code": 200
}
mysql> select * from t_bill;
+---------+------------+-------------+-----------+
| bill_id | bill_title | bill_author | bill_date |
+---------+------------+-------------+-----------+
| 1 | haha | test | NULL |
| 2 | haha | test | NULL |
| 3 | haha | test | NULL |
| 4 | haha55 | test | NULL |
| 888 | update888 | update888 | NULL |
+---------+------------+-------------+-----------+
5 rows in set (0.00 sec)
http://localhost:8080/t-bill/deleteTBill?id=1
返回
{
"msg": "操作成功",
"code": 200
}
mysql> select * from t_bill;
+---------+------------+-------------+-----------+
| bill_id | bill_title | bill_author | bill_date |
+---------+------------+-------------+-----------+
| 2 | haha | test | NULL |
| 3 | haha | test | NULL |
| 4 | haha55 | test | NULL |
| 888 | update888 | update888 | NULL |
+---------+------------+-------------+-----------+
4 rows in set (0.00 sec)
总结
在构建工程过程中,踩了不少坑下面描述一下避免后面入坑。
1.没有安装mysql,工程中没有配置mysql相关配置,没有引入mysql相关驱动包等导致启动失败。
2.工程使用springboot2.0.7启动,引入mybatis-plus,pageHelper会修改springboot版本,应剔除
3.mybatis-plus,pageHelper的版本没有互相适配导致报jsqlparser错误
4.Application启动类中添加MapperScan扫描,且扫描路径应详细到具体包根路径,扫描引入的jar会出现bean冲突
5.mapper.xml中如果需要动态sql带有特殊字符如#的,在ftl模板中需要对#使用r"{"和r"}"进行转义,
<if test="${field.propertyName} != null and ${field.propertyName} != ''">
AND ${field.name} = #${r"{"}${field.propertyName}${r"}"}
</if>
最后生成的mapper.xml才会是我们想要的如下格式:
<if test="billId != null and billId != ''">
AND bill_id = #{billId}
</if>
优化思考
该例子是通过提前设定后台文件模板,填充数据进行生成,具有很大的局限性。
实际企业的代码开发平台大多会基于平台的元数据(包括方法名,入参,出参,sql,空方法,属性等等)。使用者在浏览器通过拖拽输入等方式自定义一系列服务方法,会以元数据的形式存储(如json格式),通过解析元数据动态生成对应的后台文件。这种方式可以动态适应开发者的开发自行添加需要的文件内容。
分享至此,欢迎讨论分享优化案例。