示例项目地址: 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
*/
@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"
}
]
}
}
}