代码生成器调研分析以及HTCG计划

目的

介绍目前Java开发生态中,关于代码生成相关的开源项目,以及后续的HTCG规划。

若依代码生成器分析

简介

若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。

  • 前端采用Vue、Element UI。
  • 后端采用Spring Boot、Spring Security、Redis & Jwt。
  • 权限认证使用Jwt,支持多终端认证系统。
  • 支持加载动态权限菜单,多方式轻松权限控制。
  • 高效率开发,使用代码生成器可以一键生成前后端代码。

我们比较关注的是,若依的代码生成模块。

使用逻辑

1、修改代码生成配置
单应用编辑resources目录下的application.yml
多模块编辑ruoyi-generator中的resources目录下的generator.yml
author: # 开发者姓名,生成到类注释上
packageName: # 默认生成包路径
autoRemovePre: # 是否自动去除表前缀
tablePrefix: # 表前缀

2、新建数据库表结构(单表)

drop table if exists sys_student;
create table sys_student (
  student_id           int(11)         auto_increment    comment '编号',
  student_name         varchar(30)     default ''        comment '学生名称',
  student_age          int(3)          default null      comment '年龄',
  student_hobby        varchar(30)     default ''        comment '爱好(0代码 1音乐 2电影)',
  student_sex          char(1)         default '0'       comment '性别(0男 1女 2未知)',
  student_status       char(1)         default '0'       comment '状态(0正常 1停用)',
  student_birthday     datetime                          comment '生日',
  primary key (student_id)
) engine=innodb auto_increment=1 comment = '学生信息表';

2、新建数据库表结构(树表)

drop table if exists sys_product;
create table sys_product (
  product_id        bigint(20)      not null auto_increment    comment '产品id',
  parent_id         bigint(20)      default 0                  comment '父产品id',
  product_name      varchar(30)     default ''                 comment '产品名称',
  order_num         int(4)          default 0                  comment '显示顺序',
  status            char(1)         default '0'                comment '产品状态(0正常 1停用)',
  primary key (product_id)
) engine=innodb auto_increment=1 comment = '产品表';

2、新建数据库表结构(主子表)

-- ----------------------------
-- 客户表
-- ----------------------------
drop table if exists sys_customer;
create table sys_customer (
  customer_id           bigint(20)      not null auto_increment    comment '客户id',
  customer_name         varchar(30)     default ''                 comment '客户姓名',
  phonenumber           varchar(11)     default ''                 comment '手机号码',
  sex                   varchar(20)     default null               comment '客户性别',
  birthday              datetime                                   comment '客户生日',
  remark                varchar(500)    default null               comment '客户描述',
  primary key (customer_id)
) engine=innodb auto_increment=1 comment = '客户表';


-- ----------------------------
-- 商品表
-- ----------------------------
drop table if exists sys_goods;
create table sys_goods (
  goods_id           bigint(20)      not null auto_increment    comment '商品id',
  customer_id        bigint(20)      not null                   comment '客户id',
  name               varchar(30)     default ''                 comment '商品名称',
  weight             int(5)          default null               comment '商品重量',
  price              decimal(6,2)    default null               comment '商品价格',
  date               datetime                                   comment '商品时间',
  type               char(1)         default null               comment '商品种类',
  primary key (goods_id)
) engine=innodb auto_increment=1 comment = '商品表';

3、登录系统(系统工具 -> 代码生成 -> 导入对应表)

4、代码生成列表中找到需要表(可预览、修改、删除生成配置)

5、点击生成代码会得到一个ruoyi.zip执行sql文件,按照包内目录结构复制到自己的项目中即可

多模块所有代码生成的相关业务逻辑代码在ruoyi-generator模块,可以自行调整或剔除

主要界面

代码生成主页面
代码生成器调研分析以及HTCG计划

基本信息
代码生成器调研分析以及HTCG计划

可以编辑字段信息

代码生成器调研分析以及HTCG计划

导入表

代码生成器调研分析以及HTCG计划

预览图

代码生成器调研分析以及HTCG计划

生成结果
代码生成器调研分析以及HTCG计划
代码生成器调研分析以及HTCG计划
代码生成器调研分析以及HTCG计划

代码生成原理

实际的生成原理并没有多么高端和复杂,主要就是在【Velocity模板】的基础上根据数据模型进行替换。

Apache Velocity是一个基于Java模板引擎,它提供了一个模板语言去引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目,旨在确保Web应用程序在表示层和业务逻辑层之间的隔离(即MVC设计模式)。

  • Web应用程序网页设计者创建HTML页面,并为动态信息预留占位符。页面再由VelocityViewServlet或任何支持Velocity的框架处理。
  • 源代码生成:Velocity可基于模板生成Java、SQLPostScript源代码。大量的开源和商业软件包的开发就是这样利用Velocity。[1]
  • 电子邮件自动生成:许多应用程序为了账户注册、密码提醒或自动寄送报表之需自动生成电子邮件。利用Velocity,电子邮件模板可以存储在一个文本文件,而不是直接嵌入到电子邮件生成器的Java代码中。
  • 转化:Velocity提供一个Ant任务——Anakia。Anakia读取XML文件,利用Velocity模板转换成所需的文档格式。常见的应用是将某种格式的文档转换成的一个带样式的HTML文档。

详细信息可以参考https://cloud.tencent.com/developer/article/1184329

源码分析

导入

导入并持久化

/**
 * 导入表结构(保存)
 */
@PreAuthorize("@ss.hasPermi('tool:gen:import')")
@Log(title = "代码生成", businessType = BusinessType.IMPORT)
@PostMapping("/importTable")
public AjaxResult importTableSave(String tables)
{
    String[] tableNames = Convert.toStrArray(tables);
    // 查询表信息
    List<GenTable> tableList = genTableService.selectDbTableListByNames(tableNames);
    genTableService.importGenTable(tableList);
    return AjaxResult.success();
}
@Override
@Transactional
public void importGenTable(List<GenTable> tableList)
{
    String operName = SecurityUtils.getUsername();
    try
    {
        for (GenTable table : tableList)
        {
            String tableName = table.getTableName();
            GenUtils.initTable(table, operName);
            int row = genTableMapper.insertGenTable(table);
            if (row > 0)
            {
                // 保存列信息
                List<GenTableColumn> genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName);
                for (GenTableColumn column : genTableColumns)
                {
                    GenUtils.initColumnField(column, table);
                    genTableColumnMapper.insertGenTableColumn(column);
                }
            }
        }
    }
    catch (Exception e)
    {
        throw new CustomException("导入失败:" + e.getMessage());
    }
}

数据模型

package com.ruoyi.generator.domain;

import javax.validation.constraints.NotBlank;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.utils.StringUtils;

/**
 * 代码生成业务字段表 gen_table_column
 * 
 * @author ruoyi
 */
public class GenTableColumn extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 编号 */
    private Long columnId;

    /** 归属表编号 */
    private Long tableId;

    /** 列名称 */
    private String columnName;

    /** 列描述 */
    private String columnComment;

    /** 列类型 */
    private String columnType;

    /** JAVA类型 */
    private String javaType;

    /** JAVA字段名 */
    @NotBlank(message = "Java属性不能为空")
    private String javaField;

    /** 是否主键(1是) */
    private String isPk;

    /** 是否自增(1是) */
    private String isIncrement;

    /** 是否必填(1是) */
    private String isRequired;

    /** 是否为插入字段(1是) */
    private String isInsert;

    /** 是否编辑字段(1是) */
    private String isEdit;

    /** 是否列表字段(1是) */
    private String isList;

    /** 是否查询字段(1是) */
    private String isQuery;

    /** 查询方式(EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围) */
    private String queryType;

    /** 显示类型(input文本框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、image图片上传控件、upload文件上传控件、editor富文本控件) */
    private String htmlType;

    /** 字典类型 */
    private String dictType;

    /** 排序 */
    private Integer sort;
     /** getter&setter */

    public boolean isSuperColumn()
    {
        return isSuperColumn(this.javaField);
    }

    public static boolean isSuperColumn(String javaField)
    {
        return StringUtils.equalsAnyIgnoreCase(javaField,
                // BaseEntity
                "createBy", "createTime", "updateBy", "updateTime", "remark",
                // TreeEntity
                "parentName", "parentId", "orderNum", "ancestors");
    }

    public boolean isUsableColumn()
    {
        return isUsableColumn(javaField);
    }

    public static boolean isUsableColumn(String javaField)
    {
        // isSuperColumn()中的名单用于避免生成多余Domain属性,若某些属性在生成页面时需要用到不能忽略,则放在此处白名单
        return StringUtils.equalsAnyIgnoreCase(javaField, "parentId", "orderNum", "remark");
    }

    public String readConverterExp()
    {
        String remarks = StringUtils.substringBetween(this.columnComment, "(", ")");
        StringBuffer sb = new StringBuffer();
        if (StringUtils.isNotEmpty(remarks))
        {
            for (String value : remarks.split(" "))
            {
                if (StringUtils.isNotEmpty(value))
                {
                    Object startStr = value.subSequence(0, 1);
                    String endStr = value.substring(1);
                    sb.append("").append(startStr).append("=").append(endStr).append(",");
                }
            }
            return sb.deleteCharAt(sb.length() - 1).toString();
        }
        else
        {
            return this.columnComment;
        }
    }
}
package com.ruoyi.common.core.domain;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonFormat;

/**
 * Entity基类
 * 
 * @author ruoyi
 */
public class BaseEntity implements Serializable
{
    private static final long serialVersionUID = 1L;

    /** 搜索值 */
    private String searchValue;

    /** 创建者 */
    private String createBy;

    /** 创建时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    /** 更新者 */
    private String updateBy;

    /** 更新时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;

    /** 备注 */
    private String remark;

    /** 请求参数 */
    private Map<String, Object> params;
  /** getter&setter */
}

模型转换

/**
 * 初始化列属性字段
 */
public static void initColumnField(GenTableColumn column, GenTable table)
{
    String dataType = getDbType(column.getColumnType());
    String columnName = column.getColumnName();
    column.setTableId(table.getTableId());
    column.setCreateBy(table.getCreateBy());
    // 设置java字段名
    column.setJavaField(StringUtils.toCamelCase(columnName));
    // 设置默认类型
    column.setJavaType(GenConstants.TYPE_STRING);

    if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType))
    {
        // 字符串长度超过500设置为文本域
        Integer columnLength = getColumnLength(column.getColumnType());
        String htmlType = columnLength >= 500 || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType) ? GenConstants.HTML_TEXTAREA : GenConstants.HTML_INPUT;
        column.setHtmlType(htmlType);
    }
    else if (arraysContains(GenConstants.COLUMNTYPE_TIME, dataType))
    {
        column.setJavaType(GenConstants.TYPE_DATE);
        column.setHtmlType(GenConstants.HTML_DATETIME);
    }
    else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER, dataType))
    {
        column.setHtmlType(GenConstants.HTML_INPUT);

        // 如果是浮点型 统一用BigDecimal
        String[] str = StringUtils.split(StringUtils.substringBetween(column.getColumnType(), "(", ")"), ",");
        if (str != null && str.length == 2 && Integer.parseInt(str[1]) > 0)
        {
            column.setJavaType(GenConstants.TYPE_BIGDECIMAL);
        }
        // 如果是整形
        else if (str != null && str.length == 1 && Integer.parseInt(str[0]) <= 10)
        {
            column.setJavaType(GenConstants.TYPE_INTEGER);
        }
        // 长整形
        else
        {
            column.setJavaType(GenConstants.TYPE_LONG);
        }
    }

    // 插入字段(默认所有字段都需要插入)
    column.setIsInsert(GenConstants.REQUIRE);

    // 编辑字段
    if (!arraysContains(GenConstants.COLUMNNAME_NOT_EDIT, columnName) && !column.isPk())
    {
        column.setIsEdit(GenConstants.REQUIRE);
    }
    // 列表字段
    if (!arraysContains(GenConstants.COLUMNNAME_NOT_LIST, columnName) && !column.isPk())
    {
        column.setIsList(GenConstants.REQUIRE);
    }
    // 查询字段
    if (!arraysContains(GenConstants.COLUMNNAME_NOT_QUERY, columnName) && !column.isPk())
    {
        column.setIsQuery(GenConstants.REQUIRE);
    }

    // 查询字段类型
    if (StringUtils.endsWithIgnoreCase(columnName, "name"))
    {
        column.setQueryType(GenConstants.QUERY_LIKE);
    }
    // 状态字段设置单选框
    if (StringUtils.endsWithIgnoreCase(columnName, "status"))
    {
        column.setHtmlType(GenConstants.HTML_RADIO);
    }
    // 类型&性别字段设置下拉框
    else if (StringUtils.endsWithIgnoreCase(columnName, "type")
            || StringUtils.endsWithIgnoreCase(columnName, "sex"))
    {
        column.setHtmlType(GenConstants.HTML_SELECT);
    }
    // 图片字段设置图片上传控件
    else if (StringUtils.endsWithIgnoreCase(columnName, "image"))
    {
        column.setHtmlType(GenConstants.HTML_IMAGE_UPLOAD);
    }
    // 文件字段设置文件上传控件
    else if (StringUtils.endsWithIgnoreCase(columnName, "file"))
    {
        column.setHtmlType(GenConstants.HTML_FILE_UPLOAD);
    }
    // 内容字段设置富文本控件
    else if (StringUtils.endsWithIgnoreCase(columnName, "content"))
    {
        column.setHtmlType(GenConstants.HTML_EDITOR);
    }
}

下载

基本逻辑
代码生成器调研分析以及HTCG计划

下载入口

 /**
     * 生成代码(下载方式)
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:code')")
    @Log(title = "代码生成", businessType = BusinessType.GENCODE)
    @GetMapping("/download/{tableName}")
    public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) throws IOException
    {
        byte[] data = genTableService.downloadCode(tableName);
        genCode(response, data);
    }

核心代码

 /**
     * 查询表信息并生成代码
     */
    private void generatorCode(String tableName, ZipOutputStream zip)
    {
        // 查询表信息
        GenTable table = genTableMapper.selectGenTableByName(tableName);
        // 设置主子表信息
        setSubTable(table);
        // 设置主键列信息
        setPkColumn(table);

        VelocityInitializer.initVelocity();

        VelocityContext context = VelocityUtils.prepareContext(table);

        // 获取模板列表
        List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory());
        for (String template : templates)
        {
            // 渲染模板
            StringWriter sw = new StringWriter();
            Template tpl = Velocity.getTemplate(template, Constants.UTF8);
            tpl.merge(context, sw);
            try
            {
                // 添加到zip
                zip.putNextEntry(new ZipEntry(VelocityUtils.getFileName(template, table)));
                IOUtils.write(sw.toString(), zip, Constants.UTF8);
                IOUtils.closeQuietly(sw);
				zip.flush();
                zip.closeEntry();
            }
            catch (IOException e)
            {
                log.error("渲染模板失败,表名:" + table.getTableName(), e);
            }
        }
    }
public static List<String> getTemplateList(String tplCategory)
    {
        List<String> templates = new ArrayList<String>();
        templates.add("vm/java/domain.java.vm");
        templates.add("vm/java/mapper.java.vm");
        templates.add("vm/java/service.java.vm");
        templates.add("vm/java/serviceImpl.java.vm");
        templates.add("vm/java/controller.java.vm");
        templates.add("vm/xml/mapper.xml.vm");
        templates.add("vm/sql/sql.vm");
        templates.add("vm/js/api.js.vm");
        if (GenConstants.TPL_CRUD.equals(tplCategory))
        {
            templates.add("vm/vue/index.vue.vm");
        }
        else if (GenConstants.TPL_TREE.equals(tplCategory))
        {
            templates.add("vm/vue/index-tree.vue.vm");
        }
        else if (GenConstants.TPL_SUB.equals(tplCategory))
        {
            templates.add("vm/vue/index.vue.vm");
            templates.add("vm/java/sub-domain.java.vm");
        }
        return templates;
    }

顺藤摸瓜我们找到了这个template

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uRiBti2J-1623584511330)(C:%5CUsers%5Czhaoe%5COneDrive%5C%E5%88%86%E4%BA%AB%5Chtcg%5Cimage-20210613001504952.png)]

controller

package ${packageName}.controller;

import java.util.List;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import ${packageName}.domain.${ClassName};
import ${packageName}.service.I${ClassName}Service;
import com.ruoyi.common.utils.poi.ExcelUtil;
#if($table.crud || $table.sub)
import com.ruoyi.common.core.page.TableDataInfo;
#elseif($table.tree)
#end

/**
 * ${functionName}Controller
 * 
 * @author ${author}
 * @date ${datetime}
 */
@RestController
@RequestMapping("/${moduleName}/${businessName}")
public class ${ClassName}Controller extends BaseController
{
    @Autowired
    private I${ClassName}Service ${className}Service;

    /**
     * 查询${functionName}列表
     */
    @PreAuthorize("@ss.hasPermi('${permissionPrefix}:list')")
    @GetMapping("/list")
#if($table.crud || $table.sub)
    public TableDataInfo list(${ClassName} ${className})
    {
        startPage();
        List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
        return getDataTable(list);
    }
#elseif($table.tree)
    public AjaxResult list(${ClassName} ${className})
    {
        List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
        return AjaxResult.success(list);
    }
#end

    /**
     * 导出${functionName}列表
     */
    @PreAuthorize("@ss.hasPermi('${permissionPrefix}:export')")
    @Log(title = "${functionName}", businessType = BusinessType.EXPORT)
    @GetMapping("/export")
    public AjaxResult export(${ClassName} ${className})
    {
        List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
        ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class);
        return util.exportExcel(list, "${functionName}数据");
    }

    /**
     * 获取${functionName}详细信息
     */
    @PreAuthorize("@ss.hasPermi('${permissionPrefix}:query')")
    @GetMapping(value = "/{${pkColumn.javaField}}")
    public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField})
    {
        return AjaxResult.success(${className}Service.select${ClassName}ById(${pkColumn.javaField}));
    }

    /**
     * 新增${functionName}
     */
    @PreAuthorize("@ss.hasPermi('${permissionPrefix}:add')")
    @Log(title = "${functionName}", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@RequestBody ${ClassName} ${className})
    {
        return toAjax(${className}Service.insert${ClassName}(${className}));
    }

    /**
     * 修改${functionName}
     */
    @PreAuthorize("@ss.hasPermi('${permissionPrefix}:edit')")
    @Log(title = "${functionName}", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@RequestBody ${ClassName} ${className})
    {
        return toAjax(${className}Service.update${ClassName}(${className}));
    }

    /**
     * 删除${functionName}
     */
    @PreAuthorize("@ss.hasPermi('${permissionPrefix}:remove')")
    @Log(title = "${functionName}", businessType = BusinessType.DELETE)
	@DeleteMapping("/{${pkColumn.javaField}s}")
    public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s)
    {
        return toAjax(${className}Service.delete${ClassName}ByIds(${pkColumn.javaField}s));
    }
}

核心的service实现类

package ${packageName}.service.impl;

import java.util.List;
#foreach ($column in $columns)
#if($column.javaField == 'createTime' || $column.javaField == 'updateTime')
import com.ruoyi.common.utils.DateUtils;
#break
#end
#end
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
#if($table.sub)
import java.util.ArrayList;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.transaction.annotation.Transactional;
import ${packageName}.domain.${subClassName};
#end
import ${packageName}.mapper.${ClassName}Mapper;
import ${packageName}.domain.${ClassName};
import ${packageName}.service.I${ClassName}Service;

/**
 * ${functionName}Service业务层处理
 * 
 * @author ${author}
 * @date ${datetime}
 */
@Service
public class ${ClassName}ServiceImpl implements I${ClassName}Service 
{
    @Autowired
    private ${ClassName}Mapper ${className}Mapper;

    /**
     * 查询${functionName}
     * 
     * @param ${pkColumn.javaField} ${functionName}ID
     * @return ${functionName}
     */
    @Override
    public ${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField})
    {
        return ${className}Mapper.select${ClassName}ById(${pkColumn.javaField});
    }

    /**
     * 查询${functionName}列表
     * 
     * @param ${className} ${functionName}
     * @return ${functionName}
     */
    @Override
    public List<${ClassName}> select${ClassName}List(${ClassName} ${className})
    {
        return ${className}Mapper.select${ClassName}List(${className});
    }

    /**
     * 新增${functionName}
     * 
     * @param ${className} ${functionName}
     * @return 结果
     */
#if($table.sub)
    @Transactional
#end
    @Override
    public int insert${ClassName}(${ClassName} ${className})
    {
#foreach ($column in $columns)
#if($column.javaField == 'createTime')
        ${className}.setCreateTime(DateUtils.getNowDate());
#end
#end
#if($table.sub)
        int rows = ${className}Mapper.insert${ClassName}(${className});
        insert${subClassName}(${className});
        return rows;
#else
        return ${className}Mapper.insert${ClassName}(${className});
#end
    }

    /**
     * 修改${functionName}
     * 
     * @param ${className} ${functionName}
     * @return 结果
     */
#if($table.sub)
    @Transactional
#end
    @Override
    public int update${ClassName}(${ClassName} ${className})
    {
#foreach ($column in $columns)
#if($column.javaField == 'updateTime')
        ${className}.setUpdateTime(DateUtils.getNowDate());
#end
#end
#if($table.sub)
        ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${className}.get${pkColumn.capJavaField}());
        insert${subClassName}(${className});
#end
        return ${className}Mapper.update${ClassName}(${className});
    }

    /**
     * 批量删除${functionName}
     * 
     * @param ${pkColumn.javaField}s 需要删除的${functionName}ID
     * @return 结果
     */
#if($table.sub)
    @Transactional
#end
    @Override
    public int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s)
    {
#if($table.sub)
        ${className}Mapper.delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaField}s);
#end
        return ${className}Mapper.delete${ClassName}ByIds(${pkColumn.javaField}s);
    }

    /**
     * 删除${functionName}信息
     * 
     * @param ${pkColumn.javaField} ${functionName}ID
     * @return 结果
     */
    @Override
    public int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField})
    {
#if($table.sub)
        ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${pkColumn.javaField});
#end
        return ${className}Mapper.delete${ClassName}ById(${pkColumn.javaField});
    }
#if($table.sub)

    /**
     * 新增${subTable.functionName}信息
     * 
     * @param ${className} ${functionName}对象
     */
    public void insert${subClassName}(${ClassName} ${className})
    {
        List<${subClassName}> ${subclassName}List = ${className}.get${subClassName}List();
        Long ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}();
        if (StringUtils.isNotNull(${subclassName}List))
        {
            List<${subClassName}> list = new ArrayList<${subClassName}>();
            for (${subClassName} ${subclassName} : ${subclassName}List)
            {
                ${subclassName}.set${subTableFkClassName}(${pkColumn.javaField});
                list.add(${subclassName});
            }
            if (list.size() > 0)
            {
                ${className}Mapper.batch${subClassName}(list);
            }
        }
    }
#end
}

package ${packageName}.mapper;

import java.util.List;
import ${packageName}.domain.${ClassName};
#if($table.sub)
import ${packageName}.domain.${subClassName};
#end

/**
 * ${functionName}Mapper接口
 * 
 * @author ${author}
 * @date ${datetime}
 */
public interface ${ClassName}Mapper 
{
    /**
     * 查询${functionName}
     * 
     * @param ${pkColumn.javaField} ${functionName}ID
     * @return ${functionName}
     */
    public ${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});

    /**
     * 查询${functionName}列表
     * 
     * @param ${className} ${functionName}
     * @return ${functionName}集合
     */
    public List<${ClassName}> select${ClassName}List(${ClassName} ${className});

    /**
     * 新增${functionName}
     * 
     * @param ${className} ${functionName}
     * @return 结果
     */
    public int insert${ClassName}(${ClassName} ${className});

    /**
     * 修改${functionName}
     * 
     * @param ${className} ${functionName}
     * @return 结果
     */
    public int update${ClassName}(${ClassName} ${className});

    /**
     * 删除${functionName}
     * 
     * @param ${pkColumn.javaField} ${functionName}ID
     * @return 结果
     */
    public int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});

    /**
     * 批量删除${functionName}
     * 
     * @param ${pkColumn.javaField}s 需要删除的数据ID
     * @return 结果
     */
    public int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s);
#if($table.sub)

    /**
     * 批量删除${subTable.functionName}
     * 
     * @param customerIds 需要删除的数据ID
     * @return 结果
     */
    public int delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaType}[] ${pkColumn.javaField}s);
    
    /**
     * 批量新增${subTable.functionName}
     * 
     * @param ${subclassName}List ${subTable.functionName}列表
     * @return 结果
     */
    public int batch${subClassName}(List<${subClassName}> ${subclassName}List);
    

    /**
     * 通过${functionName}ID删除${subTable.functionName}信息
     * 
     * @param ${pkColumn.javaField} ${functionName}ID
     * @return 结果
     */
    public int delete${subClassName}By${subTableFkClassName}(${pkColumn.javaType} ${pkColumn.javaField});
#end
}

<?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="${packageName}.mapper.${ClassName}Mapper">
    
    <resultMap type="${ClassName}" id="${ClassName}Result">
#foreach ($column in $columns)
        <result property="${column.javaField}"    column="${column.columnName}"    />
#end
    </resultMap>
#if($table.sub)

    <resultMap id="${ClassName}${subClassName}Result" type="${ClassName}" extends="${ClassName}Result">
        <collection property="${subclassName}List" notNullColumn="sub_${subTable.pkColumn.columnName}" javaType="java.util.List" resultMap="${subClassName}Result" />
    </resultMap>

    <resultMap type="${subClassName}" id="${subClassName}Result">
#foreach ($column in $subTable.columns)
        <result property="${column.javaField}"    column="sub_${column.columnName}"    />
#end
    </resultMap>
#end

    <sql id="select${ClassName}Vo">
        select#foreach($column in $columns) $column.columnName#if($velocityCount != $columns.size()),#end#end from ${tableName}
    </sql>

    <select id="select${ClassName}List" parameterType="${ClassName}" resultMap="${ClassName}Result">
        <include refid="select${ClassName}Vo"/>
        <where>  
#foreach($column in $columns)
#set($queryType=$column.queryType)
#set($javaField=$column.javaField)
#set($javaType=$column.javaType)
#set($columnName=$column.columnName)
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#if($column.query)
#if($column.queryType == "EQ")
            <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName = #{$javaField}</if>
#elseif($queryType == "NE")
            <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName != #{$javaField}</if>
#elseif($queryType == "GT")
            <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName &gt; #{$javaField}</if>
#elseif($queryType == "GTE")
            <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName &gt;= #{$javaField}</if>
#elseif($queryType == "LT")
            <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName &lt; #{$javaField}</if>
#elseif($queryType == "LTE")
            <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName &lt;= #{$javaField}</if>
#elseif($queryType == "LIKE")
            <if test="$javaField != null #if($javaType == 'String' ) and $javaField.trim() != ''#end"> and $columnName like concat('%', #{$javaField}, '%')</if>
#elseif($queryType == "BETWEEN")
            <if test="params.begin$AttrName != null and params.begin$AttrName != '' and params.end$AttrName != null and params.end$AttrName != ''"> and $columnName between #{params.begin$AttrName} and #{params.end$AttrName}</if>
#end
#end
#end
        </where>
    </select>
    
    <select id="select${ClassName}ById" parameterType="${pkColumn.javaType}" resultMap="#if($table.sub)${ClassName}${subClassName}Result#else${ClassName}Result#end">
#if($table.crud || $table.tree)
        <include refid="select${ClassName}Vo"/>
        where ${pkColumn.columnName} = #{${pkColumn.javaField}}
#elseif($table.sub)
        select#foreach($column in $columns) a.$column.columnName#if($velocityCount != $columns.size()),#end#end,
           #foreach($column in $subTable.columns) b.$column.columnName as sub_$column.columnName#if($velocityCount != $subTable.columns.size()),#end#end

        from ${tableName} a
        left join ${subTableName} b on b.${subTableFkName} = a.${pkColumn.columnName}
        where a.${pkColumn.columnName} = #{${pkColumn.javaField}}
#end
    </select>
        
    <insert id="insert${ClassName}" parameterType="${ClassName}"#if($pkColumn.increment) useGeneratedKeys="true" keyProperty="$pkColumn.javaField"#end>
        insert into ${tableName}
        <trim prefix="(" suffix=")" suffixOverrides=",">
#foreach($column in $columns)
#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment)
            <if test="$column.javaField != null#if($column.javaType == 'String' && $column.required) and $column.javaField != ''#end">$column.columnName,</if>
#end
#end
         </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
#foreach($column in $columns)
#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment)
            <if test="$column.javaField != null#if($column.javaType == 'String' && $column.required) and $column.javaField != ''#end">#{$column.javaField},</if>
#end
#end
         </trim>
    </insert>

    <update id="update${ClassName}" parameterType="${ClassName}">
        update ${tableName}
        <trim prefix="SET" suffixOverrides=",">
#foreach($column in $columns)
#if($column.columnName != $pkColumn.columnName)
            <if test="$column.javaField != null#if($column.javaType == 'String' && $column.required) and $column.javaField != ''#end">$column.columnName = #{$column.javaField},</if>
#end
#end
        </trim>
        where ${pkColumn.columnName} = #{${pkColumn.javaField}}
    </update>

    <delete id="delete${ClassName}ById" parameterType="${pkColumn.javaType}">
        delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}}
    </delete>

    <delete id="delete${ClassName}ByIds" parameterType="String">
        delete from ${tableName} where ${pkColumn.columnName} in 
        <foreach item="${pkColumn.javaField}" collection="array" open="(" separator="," close=")">
            #{${pkColumn.javaField}}
        </foreach>
    </delete>
#if($table.sub)
    
    <delete id="delete${subClassName}By${subTableFkClassName}s" parameterType="String">
        delete from ${subTableName} where ${subTableFkName} in 
        <foreach item="${subTableFkclassName}" collection="array" open="(" separator="," close=")">
            #{${subTableFkclassName}}
        </foreach>
    </delete>

    <delete id="delete${subClassName}By${subTableFkClassName}" parameterType="Long">
        delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}}
    </delete>

    <insert id="batch${subClassName}">
        insert into ${subTableName}(#foreach($column in $subTable.columns) $column.columnName#if($velocityCount != $subTable.columns.size()),#end#end) values
		<foreach item="item" index="index" collection="list" separator=",">
            (#foreach($column in $subTable.columns) #{item.$column.javaField}#if($velocityCount != $subTable.columns.size()),#end#end)
        </foreach>
    </insert>
#end
</mapper>

信息合并

Template tpl = Velocity.getTemplate(template, Constants.UTF8);
tpl.merge(context, sw);

类似的项目

这个项目并没有基于Velocity实现,而是基于Freemarker实现。支持jpa,mybatis,mp等多种模板

代码生成器调研分析以及HTCG计划

主要逻辑

代码生成器调研分析以及HTCG计划

由于代码比较零散这里就不粘贴了,实际上跟其他的是一样的,只不过template换为freemarker。

也是Velocity实现

/**
 * 生成代码
 */
@SneakyThrows
public Map<String, String> generatorCode(GenConfig genConfig, Map<String, String> table,
      List<Map<String, String>> columns, ZipOutputStream zip, GenFormConf formConf) {
   // 配置信息
   Configuration config = getConfig();
   boolean hasBigDecimal = false;
   // 表信息
   TableEntity tableEntity = new TableEntity();
   tableEntity.setTableName(table.get("tableName"));

   if (StrUtil.isNotBlank(genConfig.getComments())) {
      tableEntity.setComments(genConfig.getComments());
   }
   else {
      tableEntity.setComments(table.get("tableComment"));
   }

   String tablePrefix;
   if (StrUtil.isNotBlank(genConfig.getTablePrefix())) {
      tablePrefix = genConfig.getTablePrefix();
   }
   else {
      tablePrefix = config.getString("tablePrefix");
   }

   // 表名转换成Java类名
   String className = tableToJava(tableEntity.getTableName(), tablePrefix);
   tableEntity.setCaseClassName(className);
   tableEntity.setLowerClassName(StringUtils.uncapitalize(className));

   // 列信息
   List<ColumnEntity> columnList = new ArrayList<>();
   for (Map<String, String> column : columns) {
      ColumnEntity columnEntity = new ColumnEntity();
      columnEntity.setColumnName(column.get("columnName"));
      columnEntity.setDataType(column.get("dataType"));
      columnEntity.setComments(column.get("columnComment"));
      columnEntity.setExtra(column.get("extra"));
      columnEntity.setNullable("NO".equals(column.get("isNullable")));
      columnEntity.setColumnType(column.get("columnType"));
      columnEntity.setHidden(Boolean.FALSE);
      // 列名转换成Java属性名
      String attrName = columnToJava(columnEntity.getColumnName());
      columnEntity.setCaseAttrName(attrName);
      columnEntity.setLowerAttrName(StringUtils.uncapitalize(attrName));

      // 列的数据类型,转换成Java类型
      String attrType = config.getString(columnEntity.getDataType(), "unknowType");
      columnEntity.setAttrType(attrType);
      if (!hasBigDecimal && "BigDecimal".equals(attrType)) {
         hasBigDecimal = true;
      }
      // 是否主键
      if ("PRI".equalsIgnoreCase(column.get("columnKey")) && tableEntity.getPk() == null) {
         tableEntity.setPk(columnEntity);
      }

      columnList.add(columnEntity);
   }
   tableEntity.setColumns(columnList);

   // 没主键,则第一个字段为主键
   if (tableEntity.getPk() == null) {
      tableEntity.setPk(tableEntity.getColumns().get(0));
   }

   // 设置velocity资源加载器
   Properties prop = new Properties();
   prop.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
   Velocity.init(prop);
   // 封装模板数据
   Map<String, Object> map = new HashMap<>(16);
   map.put("tableName", tableEntity.getTableName());
   map.put("pk", tableEntity.getPk());
   map.put("className", tableEntity.getCaseClassName());
   map.put("classname", tableEntity.getLowerClassName());
   map.put("pathName", tableEntity.getLowerClassName().toLowerCase());
   map.put("columns", tableEntity.getColumns());
   map.put("hasBigDecimal", hasBigDecimal);
   map.put("datetime", DateUtil.now());

   if (StrUtil.isNotBlank(genConfig.getComments())) {
      map.put("comments", genConfig.getComments());
   }
   else {
      map.put("comments", tableEntity.getComments());
   }

   if (StrUtil.isNotBlank(genConfig.getAuthor())) {
      map.put("author", genConfig.getAuthor());
   }
   else {
      map.put("author", config.getString("author"));
   }

   if (StrUtil.isNotBlank(genConfig.getModuleName())) {
      map.put("moduleName", genConfig.getModuleName());
   }
   else {
      map.put("moduleName", config.getString("moduleName"));
   }

   if (StrUtil.isNotBlank(genConfig.getPackageName())) {
      map.put("package", genConfig.getPackageName());
      map.put("mainPath", genConfig.getPackageName());
   }
   else {
      map.put("package", config.getString("package"));
      map.put("mainPath", config.getString("mainPath"));
   }

   // 渲染数据
   return renderData(genConfig, zip, formConf, tableEntity, map);
}

/**
 * 渲染数据
 * @param genConfig 配置信息
 * @param zip 流 (为空,直接返回Map)
 * @param formConf 表单信息
 * @param tableEntity 表基本信息
 * @param map 模板参数
 * @return map key-filename value-contents
 * @throws IOException
 */
private Map<String, String> renderData(GenConfig genConfig, ZipOutputStream zip, GenFormConf formConf,
      TableEntity tableEntity, Map<String, Object> map) throws IOException {
   // 设置velocity资源加载器
   Properties prop = new Properties();
   prop.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
   Velocity.init(prop);
   VelocityContext context = new VelocityContext(map);

   // 获取模板列表
   List<String> templates = getTemplates();
   Map<String, String> resultMap = new HashMap<>(8);

   for (String template : templates) {
      // 如果是crud
      if (template.contains(AVUE_CRUD_JS_VM) && formConf != null) {

         String fileName = getFileName(template, tableEntity.getCaseClassName(), map.get("package").toString(),
               map.get("moduleName").toString());
         String contents = CRUD_PREFIX + formConf.getFormInfo();

         if (zip != null) {
            zip.putNextEntry(new ZipEntry(Objects.requireNonNull(fileName)));
            IoUtil.write(zip, StandardCharsets.UTF_8, false, contents);
            zip.closeEntry();
         }

         resultMap.put(template, contents);
         continue;
      }

      // 渲染模板
      StringWriter sw = new StringWriter();
      Template tpl = Velocity.getTemplate(template, CharsetUtil.UTF_8);
      tpl.merge(context, sw);

      // 添加到zip
      String fileName = getFileName(template, tableEntity.getCaseClassName(), map.get("package").toString(),
            map.get("moduleName").toString());

      if (zip != null) {
         zip.putNextEntry(new ZipEntry(Objects.requireNonNull(fileName)));
         IoUtil.write(zip, StandardCharsets.UTF_8, false, sw.toString());
         IoUtil.close(sw);
         zip.closeEntry();
      }
      resultMap.put(template, sw.toString());
   }

   return resultMap;
}

也是基于velocity来实现的,先把表转成类,再把类转成模板内容,在找到模板,最后模板合并,核心逻辑如下所示。

/**
 * 生成代码
 */
public static void generatorCode(Map<String, String> table,
                                 List<Map<String, String>> columns, ZipOutputStream zip) {
    //配置信息
    Configuration config = getConfig();
    boolean hasBigDecimal = false;
    boolean hasList = false;
    //表信息
    TableEntity tableEntity = new TableEntity();
    tableEntity.setTableName(table.get("tableName"));
    tableEntity.setComments(table.get("tableComment"));
    //表名转换成Java类名
    String className = tableToJava(tableEntity.getTableName(), config.getStringArray("tablePrefix"));
    tableEntity.setClassName(className);
    tableEntity.setClassname(StringUtils.uncapitalize(className));

    //列信息
    List<ColumnEntity> columsList = new ArrayList<>();
    for (Map<String, String> column : columns) {
        ColumnEntity columnEntity = new ColumnEntity();
        columnEntity.setColumnName(column.get("columnName"));
        columnEntity.setDataType(column.get("dataType"));
        columnEntity.setComments(column.get("columnComment"));
        columnEntity.setExtra(column.get("extra"));

        //列名转换成Java属性名
        String attrName = columnToJava(columnEntity.getColumnName());
        columnEntity.setAttrName(attrName);
        columnEntity.setAttrname(StringUtils.uncapitalize(attrName));

        //列的数据类型,转换成Java类型
        String attrType = config.getString(columnEntity.getDataType(), columnToJava(columnEntity.getDataType()));
        columnEntity.setAttrType(attrType);


        if (!hasBigDecimal && attrType.equals("BigDecimal")) {
            hasBigDecimal = true;
        }
        if (!hasList && "array".equals(columnEntity.getExtra())) {
            hasList = true;
        }
        //是否主键
        if ("PRI".equalsIgnoreCase(column.get("columnKey")) && tableEntity.getPk() == null) {
            tableEntity.setPk(columnEntity);
        }

        columsList.add(columnEntity);
    }
    tableEntity.setColumns(columsList);

    //没主键,则第一个字段为主键
    if (tableEntity.getPk() == null) {
        tableEntity.setPk(tableEntity.getColumns().get(0));
    }

    //设置velocity资源加载器
    Properties prop = new Properties();
    prop.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
    Velocity.init(prop);
    String mainPath = config.getString("mainPath");
    mainPath = StringUtils.isBlank(mainPath) ? "io.renren" : mainPath;
    //封装模板数据
    Map<String, Object> map = new HashMap<>();
    map.put("tableName", tableEntity.getTableName());
    map.put("comments", tableEntity.getComments());
    map.put("pk", tableEntity.getPk());
    map.put("className", tableEntity.getClassName());
    map.put("classname", tableEntity.getClassname());
    map.put("pathName", tableEntity.getClassname().toLowerCase());
    map.put("columns", tableEntity.getColumns());
    map.put("hasBigDecimal", hasBigDecimal);
    map.put("hasList", hasList);
    map.put("mainPath", mainPath);
    map.put("package", config.getString("package"));
    map.put("moduleName", config.getString("moduleName"));
    map.put("author", config.getString("author"));
    map.put("email", config.getString("email"));
    map.put("datetime", DateUtils.format(new Date(), DateUtils.DATE_TIME_PATTERN));
    VelocityContext context = new VelocityContext(map);

    //获取模板列表
    List<String> templates = getTemplates();
    for (String template : templates) {
        //渲染模板
        StringWriter sw = new StringWriter();
        Template tpl = Velocity.getTemplate(template, "UTF-8");
        tpl.merge(context, sw);

        try {
            //添加到zip
            zip.putNextEntry(new ZipEntry(getFileName(template, tableEntity.getClassName(), config.getString("package"), config.getString("moduleName"))));
            IOUtils.write(sw.toString(), zip, "UTF-8");
            IOUtils.closeQuietly(sw);
            zip.closeEntry();
        } catch (IOException e) {
            throw new RRException("渲染模板失败,表名:" + tableEntity.getTableName(), e);
        }
    }
}

国外的一个代码生成框架,并非是java底层,而是node来实现的。18.5K的Star。JHipster是一个开发平台,可以快速生成,开发和部署现代Web应用程序+微服务架构。

代码生成器调研分析以及HTCG计划

生成的文件

代码生成器调研分析以及HTCG计划

单独的JDL-Studio来定义model

代码生成器调研分析以及HTCG计划

【网络是个问题。。。】

这套框架极为强大,前面的那些框架只能算是弟弟,全自动生成,专门的DSL模型编辑,甚至配有管理后台,代码质量检查,而且都是docker化的。

代码生成器调研分析以及HTCG计划

这个框架实在是太强了,后面还是单独出个专门讨论的文章吧。

缺点就是1.网络,2.全栈要求高,3.重量级

终极效果

不知道大家有没有使用过django,这个代码生成器的我能想象的最终阶段就是类似于django的模式,只改model然后自动更新全部剩余的东西。

但这又引出了一个问题,为什么django都这么nb了,也没有一统天下啊,也许因为软件开发实际上花在这种地方的时间不是特别多。

HTCG项目

希望能够在以上的代码生成框架的基础上稍加修改,实现一套符合HT自定义规范的代码生成框架。希望借此能够在项目开始阶段节省90%的开发时间。

效果:只编辑模型,输入一条指令,自动在本地生成对应的其他文件和相应结构

原则:

  • 内源,充分吸收并利用HT内部的开发力量
  • 模型先行,基于模型而不是SQL来生成
  • MVP,最快见效果

分为如下几个阶段:

  1. 在若依的基础上进行二次开发,修改代码模板,实现接口形式后台代码自动生成
  2. 开发python或npm脚本,实现本地命令行一键式生成
  3. 开发前台生成模板
  4. 支持模型修改的同步。
  5. 开发IDE插件,实现直接生成
上一篇:原生JS实现多条件筛选


下一篇:java之从字符串比较到==和equals方法区别