基于FreeMarker的自定义代码生成器
我们平时开发一些项目时,可能需要创建很多代码,比如比如Web项目创建的bean,Mybatis项目创建的若干mapper接口文件和mapper映射文件。
在建立这些文件的过程中,我们可以发现,这些接口文件和映射文件从规则上来看基本上都是千篇一律。为此,官方以及民间大佬都提供了一些代码生成器,例如mybatis-generator、Mybatis-plus的代码生成器等,但是可能跟我们平时使用习惯不一样,比如mybatis-generator生成Example类来给数据库查询添加条件,但对于我这种强迫症来说,希望这个名字时Condition之类的描述而非Example。本文将介绍如何使用FreeMarker定义自己的代码生成器。
FreeMarker简介
FreeMarker是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。FreeMarker中文官网。
最基本的,我们需要有一个通用的模板文件、模板文件中需要个性化输出的数据模型,如下所示。
定义一个HTML模板:
<html>
<head>
<title>Welcome!</title>
</head>
<body>
<h1>Welcome ${user}!</h1>
<p>Our latest product:
<a href="${latestProduct.url}">${latestProduct.name}</a>!
</body>
</html>
其中${user}、${latestProduct.name}、${latestProduct.url}等内容就是根据提供的数据模型变化的动态部分。
定义一个数据模型:
数据模型的基本结构是树状的,这棵树可以很复杂,并且可以有很大的深度,我们通常通过Map类型的数据结构存储模型。
(root)
|
+- user = "Big Joe"
|
+- latestProduct
|
+- url = "products/greenmouse.html"
|
+- name = "green mouse"
Map root = new HashMap();
root.put("user", "Big Joe");
Map latest = new HashMap();
root.put("latestProduct", latest);
latest.put("url", "products/greenmouse.html");
latest.put("name", "green mouse");
模板和数据模型结合后,生成的HTML文件如下:
<html>
<head>
<title>Welcome!</title>
</head>
<body>
<h1>Welcome Big Joe!</h1>
<p>Our latest product:
<a href="products/greenmouse.html">green mouse</a>!
</body>
</html>
关于如何通过Java将模板和模型整合到一起输出文件,以及FreeMarker的其他语法,大家可参考FreeMarker中文官网,这里不再赘述。
使用FreeMarker生成JavaBean文件
一个最基本的JavaBean文件示例:
public class Label{
private Integer labelId;
private String labelName;
public void setLabelId(Integer labelId){
this.labelId = labelId;
}
public Integer getLabelId(){
return this.labelId;
}
public void setLabelName(String labelName){
this.labelName = labelName;
}
public String getLabelName(){
return this.labelName;
}
}
数据表列操作工具类ColumnUtil
我们知道数据表的表名、列名、列类型跟JavaBean中的名称和类型是有所差别的,但也有规律可循,我们可以定义工具类ColumnUtil将数据库名称、类型信息转化成符合Java的变量名和变量类型。
我们先定义两个静态数组,分别表示数据库中常用的列类型和JavaBean变量类型。
//数据库中常用的列类型
private static String[] fieldTypes = {"tinyint", "int", "bigint", "varchar", "text", "date", "datetime", "decimal","char"};
//对应的JavaBean变量类型
private static String[] javaTypes = {"Integer", "Integer", "Long", "String", "String", "java.util.Date", "java.util.Date", "Long", "String"};
然后定义一些方法将两者对应起来,这个简单,这里不赘述。
接下来就是列名和表名转化成符合Java规范的格式了,数据库中我们命名往往采用"_"分割,而Java中往往采用驼峰的形式,以下是将表名转化成符合Java规范的示例。
//例如:TEST_JAVA_BEAN表转化成表名TestJavaBean类
public static String getClassName(String tableName){
String tempTableName = tableName.toLowerCase();
String[] tempNames = tempTableName.split("_");
StringBuilder sbud = new StringBuilder();
for(int i = 0; i < tempNames.length; i ++){
sbud.append(tempNames[i].substring(0, 1).toUpperCase()).append(tempNames[i].substring(1));
}
return sbud.toString();
}
数据表列名转化同理,但需要注意类名是从第一个单词开始大写,变量是从第二个开始大写,因此有细微差别。
获取列名对应的变量之后,再写两个方法定义setter/getter方法。
public static String getMethodName(String attrName){
return "get" + attrName.substring(0, 1).toUpperCase() + attrName.substring(1);
}
public static String setMethodName(String attrName){
return "set" + attrName.substring(0, 1).toUpperCase() + attrName.substring(1);
}
数据表列操作工具类ColumnUtil
一个数据库有多张表,一张表也有多个数据列,将数据库内容和Java内容对应之后,我们还得将表信息和列信息存储起来。
我们需要定义一个ColumnInfo存储列信息,定义一个TableInfo来存储表信息,这都非常简单。
public class ColumnInfo {
private String fieldName;//列名
private String fieldType;//列类型
private String javaType;//Java类型
private String getMethodName;//对应getter方法
private String setMethodName;//对应setter方法
private String comments;//表注释
private boolean isKey;//是否主键
}
//省略setter/getter方法
public class TableInfo {
private String tableName;//表名
private List<ColumnInfo> columnInfos;//列信息
//省略setter/getter方法
}
JavaBean模板
从开头示例我们可以找出哪些是写死在模板中,哪些是需要通过数据模型获取的内容。
- 类名——数据库的表名
- 成员变量、setter/getter方法——数据表的列名
- 变量类型、setter/getter方法返回类型——数据表各列的类型
根据以上分析,我们可以得到以下JavaBean模板:
package ${modelPackage};
public class ${className}{
<#list columns as column>
private ${column.javaType} ${column.attrName};
</#list>
<#list columns as column>
public void ${column.setMethodName}(${column.javaType} ${column.attrName}){
this.${column.attrName} = ${column.attrName};
}
public ${column.javaType} ${column.getMethodName}(){
return this.${column.attrName};
}
</#list>
}
- modelPackage表示包名,可通过配置文件获取
- className表示表名
- 通过list(FreeMarker语法,类似与for循环)生成成员变量、setter/getter方法等信息
JDBC操作类获取数据库信息
模板定义好了,数据库信息和Java内容的对应关系也好了,接下来就是获取数据库信息了。因为我们要做的是一个代码器,所以要尽可能复用,我们可以定义一个JDBC操作类JDBCUtil来操作数据库。
以下是JDBCUtil获取表名和各个表列信息的方法,其余注册数据库驱动、连接数据库写在getConnection()中。
public static List<String> getTables(String database){
Connection connection = getConnection();
List<String> tables = new ArrayList<>();
PreparedStatement pre=null;
String sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = ? ";
try {
pre = connection.prepareStatement(sql);
pre.setObject(1,database);
ResultSet res = pre.executeQuery();
while(res.next()){
tables.add(res.getString("table_name"));
}
} catch (SQLException e) {
System.out.println("查询"+database+"数据库的数据表失败!");
e.printStackTrace();
}finally{
try {
if(pre!=null)
pre.close();
if(connection!=null)
connection.close();
} catch (SQLException e) {
System.out.println("查询"+database+"表名后,关闭数据库连接失败!");
e.printStackTrace();
}
}
return tables;
}
public static TableInfo getTableInfo(String database,String tableName){
Connection connection = getConnection();
TableInfo tableInfo = new TableInfo(tableName);
List<ColumnInfo> columnInfos = new ArrayList<>();
PreparedStatement pre=null;
String sql = "SELECT COLUMN_NAME,DATA_TYPE,COLUMN_KEY,COLUMN_COMMENT FROM information_schema.COLUMNS T WHERE T.TABLE_SCHEMA=? AND T.TABLE_NAME=?";
try {
pre = connection.prepareStatement(sql);
pre.setObject(1,database);
pre.setObject(2,tableName);
ResultSet res = pre.executeQuery();
while(res.next()){
String columnName = res.getString("COLUMN_NAME");
String columnType = res.getString("DATA_TYPE");
String comments = res.getString("COLUMN_COMMENT");
String pri = res.getString("COLUMN_KEY");
ColumnInfo columnInfo = new ColumnInfo(columnName,columnType,comments,pri);
columnInfos.add(columnInfo);
}
tableInfo.setColumnInfos(columnInfos);
} catch (SQLException e) {
System.out.println("查询表"+tableName+"信息失败!");
e.printStackTrace();
}finally{
try {
if(pre!=null)
pre.close();
if(connection!=null)
connection.close();
} catch (SQLException e) {
System.out.println("查询"+tableName+"表后,关闭数据库连接失败!");
e.printStackTrace();
}
}
return tableInfo;
}
使用FreeMarker生成JavaBean
完成了数据模型的获取,模板的定义,最后一步就是生成我们的JavaBean了。
创建Configuration实例
Configuration实例是存储FreeMarker应用级设置的核心部分。同时,它也处理创建和缓存预解析模板(比如Template对象)的工作。因为Configuration是单例的,只需创建一次,所以我定义了一个FreemarkerConfiguration来生成。
public class FreemarkerConfiguration {
private static Configuration cfg = null;
static {
try {
File resourcesRoot = new File(ResourceConfiguration.getResourceRoot());//ResourceConfiguration.getResourceRoot()是自定义的获取资源文件根目录
cfg = new Configuration(Configuration.VERSION_2_3_23);
cfg.setDirectoryForTemplateLoading(resourcesRoot);
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER);
}catch (IOException ioException){
System.out.println("freemarker配置初始化失败:无法找到模板存放路径");
}
}
public static Configuration getCfg(){
return cfg;
}
}
获取数据模型
通过前面的准备工作,我们将数据模型信息都放入Map中。
//获取数据模型,其中blog是博主自己的数据库
public Map createModel() throws IOException, TemplateException {
List<String> tables = JDBCUtil.getTables("blog");
for(int i=0;i<tables.size();i++){
Map root = new HashMap();
TableInfo tableInfo = JDBCUtil.getTableInfo("blog",tables.get(i));
root.put("className", ColumnUtil.getClassName(tables.get(i)));
List<ColumnInfo> columnInfos = tableInfo.getColumnInfos();
root.put("columns",columnInfos);
root.put("modelPackage","test");//自定义包名
}
return root;
}
className、columns、数据库名、modelPackage和包名等内容如果需要更加个性化一点,不用硬编码,大家可以将这些内容放在配置文件中,读取配置文件,动态修改,这里暂时先不讲。
生成JavaBean
最后生成JavaBean,如下所示。其中templatePath是模板文件的路径,root是建好的数据模型。
public void writerCode(String templatePath,Map root) throws IOException, TemplateException {
//给Configuration实例设置模板路径
Template temp = cfg.getTemplate(templatePath);
//输出后的文件位置,这里演示设置成控制台窗口,正式用的时候可设置成自己的Java包或文件夹
Writer out = new OutputStreamWriter(System.out);
//输出内容
temp.process(root, out);
}
最后效果如下(其中一个表对应的JavaBean):
总结
大家看过FreeMarker后就知道其实这个Java库上手还是很简单的,很多第三方库都是这样,入门简单,但是如果要将它们用于平时开发中,自己真正能用起来,还是比较麻烦的,主要是细节,比如这次用FreeMarker生成JavaBean,都知道【模板+模型=输出】,但是模板的定义和数据模型的生成中间就有很多细节需要处理,处理好了就是项目,是自己以后能反复使用的工具,处理不好,就是一次普通demo。
思路分享
这次FreeMarker生成JavaBean,我的思路如下:
- 首先观察一个bean文件,总结哪些是写死的,哪些需要自己动态补充;
- 然后写一个基本模板;
- 分析模板中的各数据应该如何得到;
- 如何用同一套代码获取同类别的多个数据模型(比如本文中的生成工具类等);
- 根据模型,修改模板;
- 最后实现。
因为时间和篇幅原因,这次只介绍JavaBean的生成,后续将补充Mybatis的mapper接口文件和映射文件等内容的自定义生成,等全部完成之后,会把源码放出来,有兴趣可以对博主关注,如果平时不怎么看CSDN的关注人动态,可关注下面公众号,不定期分享一些小demo、小项目以及小工具的学习心得。
往期文章: