本文是《轻量级 Java Web 框架架构设计》的系列博文。
在《对代码生成器的一点想法》这篇文章里,我简单地谈了一下为什么要做这个代码生成器,以及如何使用它。今天有必要与大家分享一下这个代码生成器的实现过程。
简单说来,就是将表结构定义写在 Excel 文件中,然后通过代码生成器读取这份文件,最终生成对应的表结构 SQL 语句与实体类 Java 代码。
这份 Excel 文件看起来是这样的:
这是一张 product 表,定义了列名及其相关信息。Excel 文件中分多个工作表,每个工作表就相当于一张数据表。
从该结构来看,有必要抽象几个 JavaBean 出来:
- Table:用于封装表相关信息,例如:表名、主键等。
- Column:用于封装列相关信息,例如:列名、类型等。其实就是抽象以上 Excel 文件中的每行数据。
- Field:用于封装 Java 字段(即实体类成员变量),例如:字段名、类型等。
需要明确的是,Column 与 Field 有一个对应关系,这个对应关系也就是 ORM 映射规则,需要在代码中进行实现。
下面简单地看看以上三个 JavaBean 的代码:
public class Table {
private String name;
private String pk;
public Table(String name, String pk) {
this.name = name;
this.pk = pk;
}
...
}
public class Column {
private String name;
private String type;
private String length;
private String precision;
private String notnull;
private String pk;
private String comment;
public Column(String name, String type, String length, String precision, String notnull, String pk, String comment) {
this.name = name;
this.type = type;
this.length = length;
this.precision = precision;
this.notnull = notnull;
this.pk = pk;
this.comment = comment;
}
...
}
public class Field {
private String name;
private String type;
private String comment;
public Field(String name, String type, String comment) {
this.name = name;
this.type = type;
this.comment = comment;
}
...
}
以上代码中均已省略 getter/setter 方法,为了操作方便,均提供了带参数的构造方法。
好了,数据模型已搭建完毕。下面需要考虑如何生成代码,不过在实现这个生成器之前,有必要先写一个工具类,用它来生成代码。
我使用 Apache Velocity 作为模板引擎,所以也写了一个 VelocityUtil 类:
public class VelocityUtil {
private static final Logger logger = Logger.getLogger(Generator.class);
private static final VelocityEngine engine = new VelocityEngine();
static {
engine.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, ClassUtil.getClassPath());
engine.setProperty(Velocity.ENCODING_DEFAULT, "UTF-8");
}
public static void mergeTemplate(String vmPath, Map<String, Object> dataMap, String filePath) {
try {
Template template = engine.getTemplate(vmPath);
VelocityContext context = new VelocityContext(dataMap);
FileWriter writer = new FileWriter(filePath);
template.merge(context, writer);
writer.close();
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw new RuntimeException(e.getMessage(), e);
}
}
}
注意:在使用 Velocity 之前需要设置 VelocityEngine 的相关属性,其中最重要的莫过于 Velocity.FILE_RESOURCE_LOADER_PATH 了,这个就是 Velocity 模板文件的加载路径了,将其设置为 classpath。此外,还需要设置默认编码方式为 UTF-8。
提供了一个 static 的 mergeTemplate() 方法,该方法需传入三个参数:
- vmPath:模板文件路径(模板根目录)
- dataMap:需要传入模板的数据
- filePath:生成文件的路径(绝对路径)
如果对 Velocity 不太熟悉的朋友,可以阅读 Velocity 官网的《Developer Guide》。
现在底层工具也准备好了,是时候编写 Velocity 模板文件了。
先编写一个 table.vm 模板文件吧:
#foreach($entry in ${tableMap.entrySet()})
#set ($table = ${entry.key})
#set ($columnList = ${entry.value})
#set ($tableName = ${table.name})
#set ($pk = ${table.pk})
DROP TABLE IF EXISTS `${tableName}`;
CREATE TABLE `${tableName}` (
#foreach($column in ${columnList})
`${column.name}` ${column.type}#if(${column.length} != 0 && ${column.precision} != 0)(${column.length},${column.precision})#elseif(${column.length} != 0)(${column.length})#end #if(${column.notnull} == 1)NOT NULL#end #if(${column.pk} == 1)AUTO_INCREMENT#end #if(${column.comment} != '')COMMENT '${column.comment}'#end,
#end
PRIMARY KEY (`${pk}`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
#end
代码有些复杂,读者无需过于担心。只需了解,以上模板中,首先遍历 tableMap 变量,取出每个 table,然后生成相应的 create table 语句。语法细节请阅读 Velocity 官网的《 User Guide》。
再来一个 entity.mv 模板文件吧:
package ${packageName};
import com.smart.framework.base.BaseEntity;
public class ${entityName} extends BaseEntity {
#foreach($field in ${fieldList})
private ${field.type} ${field.name}; // ${field.comment}
#end
#foreach($field in ${fieldList})
#set($upperFieldName = $StringUtil.firstToUpper(${field.name}))
public ${field.type} get${upperFieldName}() {
return ${field.name};
}
public void set${upperFieldName}(${field.type} ${field.name}) {
this.${field.name} = ${field.name};
}
#end
}
以上是 Entity 类的模板,需要传入 packageName、entityName 以及 fieldList 变量。
模板中所出现的变量都需要放入 Velocity Context 中,这件事情是由最重要的 Generator 完成的。下面将一步步揭晓 Generator 的实现过程:
第一步,定义几个 static 变量:
public class Generator {
private static final Logger logger = Logger.getLogger(Generator.class);
private static final Properties config = FileUtil.loadPropFile("config.properties");
private static final String TABLE_VM = "vm/table.vm";
private static final String ENTITY_VM = "vm/entity.vm";
private static List<String> keywordList = new ArrayList<String>();
private static Map<String, String> typeMap = new HashMap<String, String>();
...
}
因为打算从 config.properties 文件中获取相关数据:
input_path = D:/Workspace/smart/smart-generator/db.xls
output_path = D:/Workspace/smart/smart-generator/gen
package_name = com.smart.sample.entity
第二步,通过 static 块来初始化相关 static 变量:
...
static {
keywordList = Arrays.asList(
"abstract", "assert",
"boolean", "break", "byte",
"case", "catch", "char", "class", "continue",
"default", "do", "double",
"else", "enum", "extends",
"final", "finally", "float", "for",
"if", "implements", "import", "instanceof", "int", "interface",
"long",
"native",
"new",
"package", "private", "protected", "public",
"return",
"strictfp", "short", "static", "super", "switch", "synchronized",
"this", "throw", "throws", "transient", "try",
"void", "volatile",
"while"
);
typeMap.put("bigint", "long");
typeMap.put("varchar", "String");
typeMap.put("char", "String");
typeMap.put("int", "int");
typeMap.put("text", "String");
}
...
其中,keywordList 表示所有的 Java 关键字;typeMap 表示列类型与 Java 类型的映射。
第三步,写一个方法,作为代码生成器的入口:
...
public void generator() {
String inputPath = config.getProperty("input_path");
String outputPath = config.getProperty("output_path");
String packageName = config.getProperty("package_name");
Map<Table, List<Column>> tableMap = createTableMap(inputPath);
generateSQL(tableMap, outputPath);
generateJava(tableMap, packageName, outputPath);
}
...
第四步,先看 createTableMap() 方法,它用于创建 Table 与 List<Column> 的映射关系,也就是一张表对应多个列:
...
private Map<Table, List<Column>> createTableMap(String inputPath) {
Map<Table, List<Column>> tableMap = new LinkedHashMap<Table, List<Column>>();
try {
File file = new File(inputPath);
Workbook workbook = Workbook.getWorkbook(file);
for (int i = 1; i < workbook.getNumberOfSheets(); i++) {
Sheet sheet = workbook.getSheet(i);
String tableName = sheet.getName().toLowerCase();
String tablePK = "";
List<Column> columnList = new ArrayList<Column>();
for (int row = 1; row < sheet.getRows(); row++) {
String name = sheet.getCell(0, row).getContents().trim();
String type = sheet.getCell(1, row).getContents().trim();
String length = sheet.getCell(2, row).getContents().trim();
String precision = sheet.getCell(3, row).getContents().trim();
String notnull = sheet.getCell(4, row).getContents().trim();
String pk = sheet.getCell(5, row).getContents().trim();
String comment = sheet.getCell(6, row).getContents().trim();
columnList.add(new Column(name, type, length, precision, notnull, pk, comment));
if (StringUtil.isNotEmpty(pk)) {
tablePK = name;
}
}
tableMap.put(new Table(tableName, tablePK), columnList);
}
workbook.close();
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw new RuntimeException(e.getMessage(), e);
}
return tableMap;
}
...
实际上就是使用 JExcelApi 这个类库读取 Excel 文件,遍历其中一个工作表与每一行数据,从而初始化 tableMap 变量。
第五步,获取 tableMap 后,通过两个私有方法来生成代码。先看 generateSQL():
...
private void generateSQL(Map<Table, List<Column>> tableMap, String outputPath) {
String sqlPath = outputPath + "/sql";
FileUtil.createPath(sqlPath);
Map<String, Object> dataMap = new HashMap<String, Object>();
dataMap.put("tableMap", tableMap);
VelocityUtil.mergeTemplate(TABLE_VM, dataMap, sqlPath + "/schema.sql");
}
...
创建 SQL 文件路径,并将 tableMap 变量放入 dataMap 中(其实就是放入 Velocity Context 中),最后模板加数据,生成文件。
第六步,再看 generateJava():
...
private void generateJava(Map<Table, List<Column>> tableMap, String packageName, String outputPath) {
String javaPath = outputPath + "/java";
FileUtil.createPath(javaPath);
for (Map.Entry<Table, List<Column>> entry : tableMap.entrySet()) {
Table table = entry.getKey();
String tableName = table.getName();
String entityName = StringUtil.firstToUpper(StringUtil.toCamelhump(tableName));
List<Column> columnList = entry.getValue();
List<Field> fieldList = transformFieldList(columnList);
Map<String, Object> dataMap = new HashMap<String, Object>();
dataMap.put("packageName", packageName);
dataMap.put("entityName", entityName);
dataMap.put("fieldList", fieldList);
dataMap.put("StringUtil", new StringUtil());
VelocityUtil.mergeTemplate(ENTITY_VM, dataMap, javaPath + "/" + entityName + ".java");
}
}
...
套路基本相同,不同的是,这里是去遍历 tableMap 变量,因为生成的 Java 文件有多个(而 SQL 文件只有一份)。
该类中还有三个私有方法:
...
private List<Field> transformFieldList(List<Column> columnList) {
List<Field> fieldList = new ArrayList<Field>(columnList.size());
for (Column column : columnList) {
String fieldName = this.transformFieldName(column.getName());
String fieldType = this.transformFieldType(column.getType());
String fieldComment = column.getComment();
fieldList.add(new Field(fieldName, fieldType, fieldComment));
}
return fieldList;
}
private String transformFieldName(String columnName) {
String fieldName;
if (keywordList.contains(columnName)) {
fieldName = columnName + "_";
} else {
fieldName = columnName;
}
return StringUtil.toCamelhump(fieldName);
}
private String transformFieldType(String columnType) {
String fieldType;
if (typeMap.containsKey(columnType)) {
fieldType = typeMap.get(columnType);
} else {
fieldType = "String";
}
return fieldType;
}
...
其实就是做了一些转换工作而已,包括名称转换与类型转换。
最后一步,代码生成器基本实现完毕。只需写一个 main() 方法,运行一下 Generator 类的 generator() 方法即可:
...
public static void main(String[] args) throws Exception {
new Generator().generator();
}
...
最终,生成了一份 SQL 文件,与一些 Java 文件。
只需手工执行 SQL 文件,就可以自动创建表结构;只需将相关的 Java 文件复制到 IDE 中,就可以无需手工编辑 Entity 类。
这种方式,在数据库设计完毕,准备搭建项目框架的时候,是非常有用的,可一定程度上加快项目开发速度。
如果您也有类似的想法或有这方面的经验,非常欢迎您的点评!