Skip to content

代码生成器

示例项目地址: https://git.code.tencent.com/xinzhang0618/code-generator.git

实现思路

  1. 使用velocity, 先制作模板, 然后填充数据, 生成文件即可
  2. 表结构信息在information_schema.TABLES, information_schema.COLUMNS两个表中, 查询到表结构信息后, 构建成模板所需的数据即可
  3. 要注意解决一些特殊场景, 比如枚举的处理, 实体添加自定义字段, 布尔类型的is方法, 有无模块定义(有模块目录结构会不同)等
  4. 难点在于如何灵活的将配置分离

解析

CodeGenerator

package top.xinzhang0618.code.generator;

import com.alibaba.fastjson.JSON;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import top.xinzhang0618.code.generator.config.Configuration;
import top.xinzhang0618.code.generator.config.Profile;
import top.xinzhang0618.code.generator.schema.domain.Bean;
import top.xinzhang0618.code.generator.schema.domain.Field;
import top.xinzhang0618.code.generator.service.SchemaService;
import top.xinzhang0618.code.generator.util.FileUtils;
import top.xinzhang0618.code.generator.util.StringUtils;

import java.io.StringWriter;
import java.time.LocalDate;
import java.util.*;

/**
 * @author xinzhang
 * @date 2020/11/6 9:59
 */
@Component
public class CodeGenerator {
    @Autowired
    private SchemaService schemaService;
    @Value("${spring.profiles.active}")
    private String activeProfile;

    public void run() {
        Configuration configuration = JSON.parseObject(FileUtils.read("json/configuration.json"), Configuration.class);
        Profile profile = JSON.parseObject(FileUtils.read("json/" + activeProfile + ".json"), Profile.class);

        // 初始化流程引擎
        Properties prop = new Properties();
        prop.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
        Velocity.init(prop);

        //封装模板数据
        Map<String, Object> context = new HashMap<>(6);
        context.put("basePackage", profile.getBasePackage());
        context.put("date", LocalDate.now());

        Map<String, List<String>> moduleMap = profile.getModuleMap();
        String outPutDir =
                System.getProperty("user.dir") + "/target/" + profile.getBasePackage().replace(".", "/") + "/";
        moduleMap.forEach((module, tables) -> {
            List<Bean> beans = schemaService.listBeans(profile.getDataSources(), tables);
            context.put("module", module);
            beans.forEach(bean -> {
                processBean(configuration, profile, bean);
                addExtendImport(bean, context);
                context.put("bean", bean);
                configuration.getTemplates().forEach(template -> {
                    StringWriter sw = new StringWriter();
                    Template tpl = Velocity.getTemplate(template, "UTF-8");
                    String fileName = buildFileName(outPutDir, module, template, bean.getBeanName());
                    tpl.merge(new VelocityContext(context), sw);
                    FileUtils.writeFile(fileName, sw.toString());
                });
            });

        });
    }

    private void addExtendImport(Bean bean, Map<String, Object> context) {
        if (!CollectionUtils.isEmpty(bean.getExtendFields())) {
            List<String> extendImports = new ArrayList<>();
            boolean needImportList = bean.getExtendFields().stream().anyMatch(field -> field.getJavaType().startsWith(
                    "List"));
            if (needImportList) {
                extendImports.add("java.util.List");
            }
            context.put("extendImports", extendImports);
        }
    }

    private String buildFileName(String outPutDir, String module, String template, String beanName) {
        if (template.contains("domain")) {
            return outPutDir + "domain/" + module + "/" + beanName + ".java";
        } else if (template.contains("mapper.java")) {
            return outPutDir + "mapper/" + module + "/" + beanName + "Mapper.java";
        } else if (template.contains("mapper.xml")) {
            return outPutDir + "mapper/xml/" + module + "/" + beanName + "Mapper.xml";
        } else if (template.contains("service.java")) {
            return outPutDir + "service/" + module + "/" + beanName + "Service.java";
        } else {
            return outPutDir + "service/impl/" + module + "/" + beanName + "ServiceImpl.java";
        }
    }

    public void processBean(Configuration configuration, Profile profile, Bean bean) {
        bean.setBeanName(parseBeanName(bean.getTableName(), profile.getTablePrefix()));
        bean.getFields().forEach(field -> {
            field.setFieldName(parseFieldName(field.getColumnName()));
            field.setJavaType(configuration.getTypeMap().getOrDefault(field.getJdbcType(), "String"));
            if (field.getColumnName().startsWith("is")) {
                field.setGetName("is" + StringUtils.upperFirst(field.getFieldName()));
            } else {
                field.setGetName("get" + StringUtils.upperFirst(field.getFieldName()));
            }
            field.setSetName("set" + StringUtils.upperFirst(field.getFieldName()));
        });

        if (!CollectionUtils.isEmpty(profile.getExtendMap()) && profile.getExtendMap().get(bean.getTableName()) != null) {
            Profile.ExtendInfo extendInfo = profile.getExtendMap().get(bean.getTableName());
            // 替换枚举类型
            if (!CollectionUtils.isEmpty(extendInfo.getEnumMap())) {
                Map<String, String> enumMap = extendInfo.getEnumMap();
                bean.getFields().forEach(field -> field.setJavaType(enumMap.get(field.getColumnName()) == null ?
                        field.getJavaType() : enumMap.get(field.getColumnName())));
                bean.setEnumJavaTypes(enumMap.values());
            }

            // 添加字段
            bean.setExtendFields(extendInfo.getExtendFields());
        }
    }

    private String parseBeanName(String tableName, String tablePrefix) {
        String[] words = tableName.replace(tablePrefix, "").split("_");
        StringBuilder sb = new StringBuilder();
        Arrays.stream(words).forEach(w -> sb.append(StringUtils.upperFirst(w)));
        return sb.toString();
    }

    private static String parseFieldName(String columnName) {
        String[] words = columnName.split("_");
        ArrayList<String> list = new ArrayList<>(Arrays.asList(words));
        list.remove("is");
        StringBuilder sb = new StringBuilder(list.get(0));
        for (int i = 1; i < list.size(); i++) {
            sb.append(StringUtils.upperFirst(list.get(i)));
        }
        return sb.toString();
    }
}

代码如上, 核心的步骤: ● 初始化流程引擎, 获取全局配置等 ● 封装模板数据 ○ 包目录, 作者信息 ○ 模块 ■ 表-->实体 ● 构建实体名 ● 构建字段名称, getter/setter ● 枚举处理 ● 添加额外字段 ● 添加额外导入 ● 从全局配置中拿到模板, 构建生成路径, 并生成文件

查询sql:

 <!-- 通用查询映射结果 -->
    <resultMap id="beanResultMap" type="top.xinzhang0618.code.generator.schema.domain.Bean">
        <result column="table_name" property="tableName"/>
        <result column="table_comment" property="comment"/>
        <collection property="fields" ofType="top.xinzhang0618.code.generator.schema.domain.Field">
            <result column="column_name" property="columnName"/>
            <result column="data_type" property="jdbcType"/>
            <result column="column_comment" property="comment"/>
            <result column="pk" property="pk"/>
        </collection>
    </resultMap>

    <select id="listBeans" resultMap="beanResultMap">
    SELECT
        t.table_name,
        t.table_comment,
        c.column_name,
        c.data_type,
        c.column_comment,
    IF
	( c.column_key = 'PRI', TRUE, FALSE ) as pk
    FROM
        information_schema.TABLES t
        LEFT JOIN information_schema.COLUMNS c ON t.table_name = c.table_name
    WHERE
        t.table_schema in
        <foreach item="item" index="index" collection="databases" open="(" separator="," close=")">
            #{item}
        </foreach>
        and t.table_name in
        <foreach item="item" index="index" collection="tableNames" open="(" separator="," close=")">
            #{item}
        </foreach>
    ORDER BY
        c.ordinal_position
    </select>

配置分离

为了方便配置的编写, 此处使用了json作为补充的配置文件(项目使用springboot, 其yml文件仅有数据库连接信息) 1.通用配置比如模板位置以及字段数据库类型到java数据类型的对应关系放到全局配置文件configuration.json中

{
  "templates": [
    "templates/domain.java.vm",
    "templates/mapper.java.vm",
    "templates/mapper.xml.vm",
    "templates/service.java.vm",
    "templates/serviceImpl.java.vm"
  ],
  "typeMap": {
    "unique identifier": "String",
    "bit": "Boolean",
    "date": "LocalDate",
    "timestamp": "LocalDateTime",
    "datetime": "LocalDateTime",
    "varchar": "String",
    "nvarchar": "String",
    "mediumtext": "String",
    "char": "String",
    "bigint": "Long",
    "bigint unsigned": "Long",
    "int": "Integer",
    "int unsigned": "Integer",
    "double": "Double",
    "double unsigned": "Double",
    "decimal": "Double",
    "decimal unsigned": "Double",
    "tinyint": "Boolean",
    "tinyint unsigned": "Integer",
    "time": "LocalTime",
    "smallint": "Integer",
    "smallint unsigned": "Integer"
  }
}

2.针对每个项目的配置单独建立该项目的配置文件, 如下demo.json

{
  "basePackage": "top.xinzhang0618.oa",
  "dataSources": [
    "oa_admin_dev",
    "oa_biz_dev"
  ],
  "tablePrefix": "oa_",
  "moduleMap": {
    "base": [
      "oa_company",
      "oa_data_dict",
      "oa_department",
      "oa_menu",
      "oa_message",
      "oa_privilege",
      "oa_role",
      "oa_user",
      "oa_user_role"
    ],
    "test": [
      "oa_user"
    ]
  },
  "extendMap": {
    "oa_data_dict": {
      "enumMap": {
        "data_dict_type": "DataDictType"
      },
      "extendFields": [
        {
          "fieldName": "testField",
          "javaType": "String",
          "comment": "测试添加额外字段",
          "setName": "setTestField",
          "getName": "getTestField"
        }
      ]
    }
  }
}