基于SpringBoot、Mybatis-Generator实现数据库表自动生成全套后台代码

Source

背景

我们在日常开发过程,大多数都是使用主流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格式),通过解析元数据动态生成对应的后台文件。这种方式可以动态适应开发者的开发自行添加需要的文件内容。

分享至此,欢迎讨论分享优化案例。