项目工具:使用freemarker实现业务代码生成器

开发原因

之前使用的开源框架里面,有个代码生成器,可以一键生成的重复业务代码(包括:controller、service、mapper、entity),虽然很好用,但是生成器属于框架定制化开发工具,要是其他项目要是想使用这个生成器,需要同时启动前后端的服务,在页面上配置表和导出的路径,这样操作就非常的不方便,需要参考了这块的逻辑,结合自己需求,基于freemarker开发了一个简单业务代码生成器

Apache FreeMarker 是一个模板引擎:一个基于模板和变化数据生成文本输出(HTML网页、电子邮件、配置文件、源代码等)的Java库。模板是用FreeMarker模板语言(FTL)编写的,它是一种简单的、专门的语言(不是像PHP那样的全面的编程语言)。通常,一个通用的编程语言(如Java)被用来准备数据(发出数据库查询,进行商业计算)。然后,Apache FreeMarker使用模板显示这些准备好的数据。在模板中,你关注的是如何呈现数据,而在模板外,你关注的是要呈现什么数据。

流程图

image-20210801184529968

项目结构

```
  '        |-- java',
  '        |   |-- ren',
  '        |       |-- kura',
  '        |           |-- config',
  '        |           |   |-- OutPathConfig.java',      输出文件的配置类
  '        |           |-- core',           
  '        |           |   |-- codemake',
  '        |           |   |   |-- CodeMake.java',       生成代码主要类
  '        |           |   |-- constants',               读取ftl路径的静态变量目录
  '        |           |   |-- dto',                     替换信息类目录
  '        |           |   |-- enums',                   生成类型文件的枚举目录
  '        |           |   |-- utils',                   工具类(生成文件、mysql查询、驼峰转换)
  '        |           |-- create',
  '        |               |-- MysqlCodeCreate.java',    主启动类
  '        |-- resources',
  '        |   |-- create_info.properties',              配置文件
  '        |   |-- make',                                ftl配置文件目录
  '        |-- out',                                     默认输出目录
```

源码分析

生成代码

虽然MyBatis-Plus提供了AutoGenerator的代码生成器,但是我个人觉得实现个性化生成的还是有点繁琐,所以,我这边做了一些简化的操作

  1. 首先,代码生成器会去读取resources目录下的配置create_info.properties获取到,数据库连接配置、导出目录、包名、类名等
url=jdbc:mysql://localhost:3306/cloud_test?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
user_name=root
password=123456
table_name=t_order_info
out_path=
entity_name=OrderInfo
package_name=ren.kura.modules.sysuser

如果不设置out_path默认将文件生成在当前路径的out目录下

ResourceBundle bundle = ResourceBundle.getBundle("create_Info", new Locale("ZH", "CN"));

//地址
String url = bundle.getString("url");

//用户名
String userName = bundle.getString("user_name");

//密码
String password = bundle.getString("password");

//表名
String tableName = bundle.getString("table_name");

//导出路径
String outPath = bundle.getString("out_path");

//导出的类名
String entityName = bundle.getString("entity_name");

//包名
String packageName = bundle.getString("package_name");

//配置导出的路径,默认导出到out目录
OutPathConfig.setOutPathPath(packageName, outPath);
  1. 由于controller、service、mapper层不需要JDBC获取表字段、描述、表描述,主启动类MysqlCodeCreate会先生成controller、service、mapper文件

image20210801230759997.png

同时生成信息(*make.java)类都有一个父类,默认每个文件都有packageName、entityName、author三个属性

@Data
public class BaseMake {
    /**
     * 包名
     */
    private String packageName;
    /**
     * 类名
     */
    private String entityName;
    /**
     * 作者
     */
    private String author;

    //赋值系统的用户名
    {
        this.author=System.getProperty("user.name");
    }

}
  1. 最后生成entity层代码,这里会调用JDBC查询表的字段、描述、类型、主键的信息

    DatabaseMetaData metaData = conn.getMetaData();
    
    String databaseName = conn.getCatalog();
    //主键
    ResultSet primaryKeys = metaData.getPrimaryKeys(databaseName, null,
            mysqlConnection.getTableName());
    Set<String> keysSet = new HashSet();
    while (primaryKeys.next()) {
        String columnName = primaryKeys.getString("COLUMN_NAME");
        keysSet.add(columnName);
    }
    
    //列名
    ResultSet rs = metaData.getColumns(databaseName, "%",
            mysqlConnection.getTableName(), "%");
    while (rs.next()) {
        String columnName = rs.getString("COLUMN_NAME");
        String columnType = rs.getString("TYPE_NAME");
        String remarks = rs.getString("REMARKS");
    }
    
    //表描述
    ResultSet tables = metaData.getTables(databaseName, null, mysqlConnection.getTableName(), null);
    while (tables.next()) {
      description = tables.getString("REMARKS");
    }
    
    

配置文件

由于controller、service、mapper层生成文件替换值类似,其中controller层较为复杂,这里只贴出controller部分代码

controller

@Api(tags="${controller.entityName}Controller")
@RestController
@RequestMapping("/${controller.entityName?lower_case}")
@Slf4j
public class ${controller.entityName}Controller {
   @Autowired
   private I${controller.entityName}Service ${controller.entityName?uncap_first}Service;
   
   /**
    * 分页列表查询
    *
    * @param ${controller.entityName?uncap_first}
    * @param pageNo 页数
    * @param pageSize 条数
    * @param req
    * @return
    */
   @ApiOperation(value="${controller.entityName}-分页列表查询", notes="${controller.entityName}-分页列表查询")
   @GetMapping(value = "/list")
   public Result<?> queryPageList(${controller.entityName} ${controller.entityName?uncap_first},
                           @RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
                           @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
                           HttpServletRequest req) {
      QueryWrapper<${controller.entityName}> queryWrapper = new QueryWrapper(${controller.entityName?uncap_first})
      Page<${controller.entityName}> page = new Page<${controller.entityName}>(pageNo, pageSize);
      IPage<${controller.entityName}> pageList = ${controller.entityName?uncap_first}Service.page(page, queryWrapper);
      return Result.OK(pageList);
   }
 }

使用了lower_case、uncap_first对类名进行了处理,一个全部小写、一个是首字母大写,${controller.entityName}中,controller为替换mapkey,entityNamemapvalue的属性

public static void createController(String packageName, String entityName) {
    ControllerMake controllerMake = new ControllerMake();
    controllerMake.setPackageName(packageName);
    controllerMake.setEntityName(entityName);

    String filePath = String.valueOf(MakeTypeEnum.CONTROLLER).toLowerCase(Locale.ROOT);
    Map<String, Object> makeInfo = new HashMap(16);
    makeInfo.put(filePath, controllerMake);

    FtlCreateUtil.createFileFromFtl(MakeConstant.MAKE_CONTROLLER, filePath,
            filePath, entityName + "Controller.java", makeInfo);
}

entity代码生成

entity的模版比较复杂点

@Data
@TableName("${entity.tableName}")
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="${entity.tableName}对象", description="${entity.tableDescription}")
public class ${entity.entityName} implements Serializable {

    private static final long serialVersionUID = 1L;

<#list entity.filedInfos as po>
    /**${po.filedComment}*/
    <#if po.primaryKeyField?? && po.primaryKeyField=='1'>
    @TableId(type = IdType.AUTO)
    <#else>
        <#if po.classType =='java.util.Date'>
            <#if po.fieldDbType =='date'>
    @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern="yyyy-MM-dd")
            <#elseif po.fieldDbType =='datetime'>
    @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
            </#if>
        </#if>
    </#if>
    @ApiModelProperty(value = "${po.filedComment}")
    @TableField(value ="${po.fieldDbName}")
    private <#if po.fieldDbType=='java.sql.Blob'>byte[]<#else>${po.classType}</#if> ${po.fieldName};
</#list>
}

遍历字段的集合,会去date、datetime和Blob的会进行特殊的情况处理

public static void createEntity(String packageName, String entityName, MysqlConnection mysqlConnection) {
    //查询字段
    List<EntityMake.FiledInfo> filedInfos = MysqlUtil.queryFields(mysqlConnection);
    //查询描述
    String tableDescription = MysqlUtil.getTableDescription(mysqlConnection);
    EntityMake entityMake = new EntityMake();
    entityMake.setPackageName(packageName);
    entityMake.setEntityName(entityName);
    entityMake.setTableName(mysqlConnection.getTableName());
    entityMake.setTableDescription(tableDescription);
    entityMake.setFiledInfos(filedInfos);
    String filePath = String.valueOf(MakeTypeEnum.ENTITY).toLowerCase(Locale.ROOT);

    Map<String, Object> makeInfo = new HashMap(16);
    makeInfo.put(filePath, entityMake);

    FtlCreateUtil.createFileFromFtl(MakeConstant.ENTITY_MAPPER, filePath,
            filePath, entityName + ".java", makeInfo);
}

生成之前会查询字段和表的描述在模版中进行替换,这里用到JDBC的DatabaseMetaData元数据进行操作,其中

通过getCatalog获取到当前的连接的表名

String databaseName = conn.getCatalog();

通过getPrimaryKeys获取主键

ResultSet primaryKeys = metaData.getPrimaryKeys(databaseName, null,
        mysqlConnection.getTableName());

通过getColumns获取字段

ResultSet rs = metaData.getColumns(databaseName, "%",
        mysqlConnection.getTableName(), "%");

这里每个查询都要赋值String catalog,属性,不然会查询出重复的数据

image20210801233924402.png

效果

image20210801235057491.png

image20210801235124216.png

源码地址

https://github.com/liuhao192/codeCreate-mysql

更早的文章

项目工具:摆脱环境依赖缺失的问题(docker部署redis)

问题项目需要在内网(不联网)进行部署我们的后端服务,框架涉及使用redis做成缓存,由于内网的系统为centos7,我们在部署的时候,遇到了编译redis6缺少环境的文件问题,后来把环境包导入内网环境后,还是存在各种依赖的缺失问题。使用了之前其他服务器部署的解决方案,虽然能解决掉服务的部署问题,但是…

继续阅读