开发原因
之前使用的开源框架里面,有个代码生成器,可以一键生成的重复业务代码(包括:controller、service、mapper、entity),虽然很好用,但是生成器属于框架定制化开发工具,要是其他项目要是想使用这个生成器,需要同时启动前后端的服务,在页面上配置表和导出的路径,这样操作就非常的不方便,需要参考了这块的逻辑,结合自己需求,基于freemarker开发了一个简单业务代码生成器
Apache FreeMarker 是一个模板引擎:一个基于模板和变化数据生成文本输出(HTML网页、电子邮件、配置文件、源代码等)的Java库。模板是用FreeMarker模板语言(FTL)编写的,它是一种简单的、专门的语言(不是像PHP那样的全面的编程语言)。通常,一个通用的编程语言(如Java)被用来准备数据(发出数据库查询,进行商业计算)。然后,Apache FreeMarker使用模板显示这些准备好的数据。在模板中,你关注的是如何呈现数据,而在模板外,你关注的是要呈现什么数据。
流程图
项目结构
```
' |-- 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的代码生成器,但是我个人觉得实现个性化生成的还是有点繁琐,所以,我这边做了一些简化的操作
- 首先,代码生成器会去读取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);
- 由于controller、service、mapper层不需要JDBC获取表字段、描述、表描述,主启动类MysqlCodeCreate会先生成controller、service、mapper文件
同时生成信息(*make.java)类都有一个父类,默认每个文件都有packageName、entityName、author三个属性
@Data
public class BaseMake {
/**
* 包名
*/
private String packageName;
/**
* 类名
*/
private String entityName;
/**
* 作者
*/
private String author;
//赋值系统的用户名
{
this.author=System.getProperty("user.name");
}
}
-
最后生成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
为替换map
的key
,entityName
为map
的value
的属性
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,
属性,不然会查询出重复的数据