代码生成器实现过程

原创
2013/09/17 22:36
阅读数 3.8K

本文是《轻量级 Java Web 框架架构设计》的系列博文。

在《对代码生成器的一点想法》这篇文章里,我简单地谈了一下为什么要做这个代码生成器,以及如何使用它。今天有必要与大家分享一下这个代码生成器的实现过程。

简单说来,就是将表结构定义写在 Excel 文件中,然后通过代码生成器读取这份文件,最终生成对应的表结构 SQL 语句与实体类 Java 代码。

这份 Excel 文件看起来是这样的:

  

这是一张 product 表,定义了列名及其相关信息。Excel 文件中分多个工作表,每个工作表就相当于一张数据表。 

从该结构来看,有必要抽象几个 JavaBean 出来:

  1. Table:用于封装表相关信息,例如:表名、主键等。
  2. Column:用于封装列相关信息,例如:列名、类型等。其实就是抽象以上 Excel 文件中的每行数据。
  3. 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() 方法,该方法需传入三个参数:

  1. vmPath:模板文件路径(模板根目录)
  2. dataMap:需要传入模板的数据
  3. 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 类。

这种方式,在数据库设计完毕,准备搭建项目框架的时候,是非常有用的,可一定程度上加快项目开发速度。

如果您也有类似的想法或有这方面的经验,非常欢迎您的点评!

展开阅读全文
加载中
点击加入讨论🔥(6) 发布并加入讨论🔥
打赏
6 评论
14 收藏
5
分享
返回顶部
顶部