示例项目地址: https://git.code.tencent.com/xinzhang0618/code-generator.git
实现思路
- 使用velocity, 先制作模板, 然后填充数据, 生成文件即可
- 表结构信息在information_schema.TABLES, information_schema.COLUMNS两个表中, 查询到表结构信息后, 构建成模板所需的数据即可
- 要注意解决一些特殊场景, 比如枚举的处理, 实体添加自定义字段, 布尔类型的is方法, 有无模块定义(有模块目录结构会不同)等
- 难点在于如何灵活的将配置分离
解析
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*/@Componentpublic class CodeGenerator {@Autowiredprivate 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">SELECTt.table_name,t.table_comment,c.column_name,c.data_type,c.column_comment,IF( c.column_key = 'PRI', TRUE, FALSE ) as pkFROMinformation_schema.TABLES tLEFT JOIN information_schema.COLUMNS c ON t.table_name = c.table_nameWHEREt.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 BYc.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"}]}}}
