然然管理系统仓库地址,欢迎移步仓库点个小星星
https://gitee.com/OceanCore/ranran.git
https://github.com/qiaoting/ranran.git
一、前言
上篇我们梳理了代码生成功能的整体架构和核心模块,本篇将深入每个核心技术点,拆解关键代码的实现逻辑,同时分析开发中的踩坑点与优化方向,让你不仅能 “用”,还能 “懂” 并 “改造”。
系统截图:
二、核心点解析
1. 数据库表 / 字段查询:精准获取元数据
代码生成的前提是精准获取数据库元数据,我们基于information_schema实现,核心逻辑如下:
- 表查询逻辑:通过
GeneratorTableMapper.xml的selectAllTable方法,支持动态指定数据库名(未指定则用当前库),使用 MyBatis 的choose-when-otherwise实现条件分支:xml
<choose> <when test="dbName != null and dbName != ''"> AND table_schema = #{dbName} </when> <otherwise> AND table_schema = DATABASE() </otherwise> </choose> - 字段查询逻辑:重点是字段属性的映射(比如
isPrimaryKey/isNullable),通过case when将数据库的字符串结果(yes/no/pri)转为布尔值,适配 Java 实体类的属性类型。
踩坑点:information_schema的字段名是区分大小写的(如column_name),不同 MySQL 版本可能有字段差异。
2. 数据库类型→Java 类型:精准映射
这是代码生成的核心环节之一,错误的类型映射会导致生成的代码无法编译。我们通过GenerateConstant定义类型常量,在GeneratorTableFieldService中实现映射逻辑:
步骤 1:定义类型常量(GenerateConstant)
public static final Set<String> STRING_TYPE = Set.of("char", "varchar", "varchar2", "nvarchar"); public static final Set<String> LONG_TYPE = Set.of("bit", "bigint", "integer"); public static final Set<String> INTEGER_TYPE = Set.of("int", "tinyint", "smallint", "mediumint"); public static final Set<String> DATE_TIME_TYPE = Set.of("datetime", "time", "date", "timestamp");步骤 2:实现映射逻辑(GeneratorTableFieldService)
public List<GeneratorTableField> fetchTableFields(String tableName) { List<GeneratorTableField> generatorTableFields = generatorTableFieldMapper.selectAllField(tableName); for (GeneratorTableField field : generatorTableFields) { if (GenerateConstant.LONG_TYPE.contains(field.getColumnType())) { field.setJavaType("Long"); } else if (GenerateConstant.INTEGER_TYPE.contains(field.getColumnType())) { field.setJavaType("Integer"); } else if (GenerateConstant.DATE_TIME_TYPE.contains(field.getColumnType())) { field.setJavaType("LocalDateTime"); } else if (GenerateConstant.STRING_TYPE.contains(field.getColumnType())) { field.setJavaType("String"); } else { // 兜底:未匹配的类型默认BigDecimal(如decimal、double) field.setJavaType("BigDecimal"); } } return generatorTableFields; }优化点:可扩展支持 JSON 类型(映射为JSONObject)、Blob 类型(映射为byte[]),或通过配置文件实现类型映射,避免硬编码。
3. 表名→Java 类名:优雅的驼峰转换
表名通常是下划线格式(如sys_user),而 Java 类名是大驼峰格式(如SysUser),TableUtil实现了这一转换,还支持移除指定前缀:
核心代码(TableUtil.convert)
public static String convert(String tableName, String[] removePrefixes) { if (!StrUtil.hasText(tableName)) { throw new IllegalArgumentException("表名不能为空"); } String lowerTableName = tableName.toLowerCase(); // 移除指定前缀(如t_、sys_) if (ObjUtil.isNotNull(removePrefixes)) { for (String prefix : removePrefixes) { if (lowerTableName.startsWith(prefix.toLowerCase())) { lowerTableName = lowerTableName.substring(prefix.length()); break; } } } // 下划线转大驼峰 String[] words = lowerTableName.split("_"); StringBuilder className = new StringBuilder(); for (String word : words) { if (!word.isEmpty()) { className.append(Character.toUpperCase(word.charAt(0))) .append(word.substring(1).toLowerCase()); } } if (className.isEmpty()) { throw new RuntimeException("转换后的类名不能为空,表名:" + tableName); } return className.toString(); }关键设计:
- 先统一转小写,避免大小写不一致导致前缀匹配失败;
- 支持自定义移除前缀(比如多系统表前缀不同);
- 严格的参数校验,避免空表名 / 转换后空类名导致的异常。
踩坑点:表名全是下划线(如_test)会导致转换后类名为空,需在业务层提前校验。
4. Freemarker:模板渲染生成代码
Freemarker 是模板引擎,核心是 “数据模型 + 模板文件→生成字符串”,我们封装了FreemarkerService实现模板渲染:
步骤 1:Freemarker 初始化(PostConstruct)
@PostConstruct public void init() { configuration = new Configuration(); try { // 模板文件放在resources/ftl目录下 configuration.setClassForTemplateLoading(this.getClass(), "/ftl"); configuration.setDefaultEncoding("UTF-8"); // 解决中文乱码 configuration.setClassicCompatible(true); // 兼容空值渲染 configuration.setNumberFormat("0.##"); // 数字格式化 } catch (Exception e) { throw new RuntimeException("初始化freemarker配置失败", e); } }踩坑点:模板路径配置错误(比如写成/templates/ftl)会导致找不到模板,需确认setClassForTemplateLoading的路径是resources下的相对路径。
步骤 2:模板渲染核心方法
public String generateString(String templateName, ClassInfoDto classInfoDto) { try { Template template = configuration.getTemplate(templateName); Writer writer = new StringWriter(); template.process(classInfoDto, writer); // 数据模型注入模板 return writer.toString(); } catch (Exception e) { throw new RuntimeException("freemarker模板渲染失败:" + templateName, e); } }步骤 3:多模板渲染(GeneratorService)
public Map<String, String> generateCode(ClassInfoDto classInfoDto) { // 查询表字段并映射类型 List<GeneratorTableField> fields = generatorTableFieldService.fetchTableFields(classInfoDto.getTableName()); Map<String, String> codeMap = new HashMap<>(); // 组装类信息(模块名、类名、基础包名等) GenerateUtil.buildClassInfo(classInfoDto, fields); // 渲染不同模板,生成各类代码 codeMap.put("entity.java", generateEntity(classInfoDto)); codeMap.put("mapper.java", generateMapper(classInfoDto)); codeMap.put("service.java", generateService(classInfoDto)); codeMap.put("controller.java", generateController(classInfoDto)); return codeMap; }核心设计:ClassInfoDto作为统一的数据模型,包含表名、模块名、作者、字段列表、类名等所有模板所需的信息,实现 “一份数据模型,多模板复用”。
三、扩展与优化思路
基于当前实现,可从以下维度优化,让代码生成器更通用:
- 模板自定义:将模板文件从 jar 包内移到配置目录,支持用户上传自定义模板(如适配不同编码规范);
- 批量生成:支持前端选择多个表,批量生成代码并打包为 ZIP 返回;
- 类型映射配置化:将类型映射关系写入 YAML 配置文件,避免硬编码,示例:
yaml
generator: type-mapping: char: String varchar: String bigint: Long # 扩展JSON类型 json: JSONObject - 前缀移除优化:当前
TableUtil中移除前缀时break只移除第一个匹配的前缀,可改为循环移除所有匹配前缀(比如表名sys_t_user,移除sys_和t_); - 代码格式化:生成代码后通过
JavaFormatter格式化代码,保证代码风格统一; - 导入包自动处理:比如生成
LocalDateTime时自动导入java.time.LocalDateTime,避免手动导包。
四、踩坑与解决方案
| 问题场景 | 解决方案 |
|---|---|
| Freemarker 渲染中文注释乱码 | 初始化时设置configuration.setDefaultEncoding("UTF-8"),模板文件保存为 UTF-8 编码 |
| 表名转换后类名为空 | 增加参数校验,对特殊表名(如全下划线)抛出明确异常 |
| 数据库类型映射遗漏 | 兜底设置为BigDecimal,并在日志中打印未匹配的类型,便于后续补充 |
| 模板路径找不到 | 确认setClassForTemplateLoading的路径是resources/ftl,且模板文件存在 |
五、总结与展望
本文深入解析了然然管理系统代码生成功能的核心细节,从数据库元数据查询、类型映射、表名转换到 Freemarker 模板渲染,每个环节都体现了 “职责单一、通用可扩展” 的设计思想。