代码生成器研发与低代码平台引擎
代码生成器架构
元数据驱动
代码生成器的核心设计思想是元数据驱动:通过解析数据源中的数据库结构信息,提取表、列、索引、外键等元数据,生成对应的代码文件。元数据的流转链路如下:
数据源 → 表结构 → 列信息 → 关系 → 代码上下文 → 模板渲染 → 文件输出元数据模型定义:
java
// 数据源配置
public class DataSourceConfig {
private String url;
private String username;
private String password;
private String driverClassName; // com.mysql.cj.jdbc.Driver
private String dbType; // mysql, oracle, postgresql
}
// 表元数据
public class TableMetaData {
private String tableName;
private String tableComment;
private String schema;
private String catalog;
private String primaryKey; // 主键列名
private List<ColumnMetaData> columns;
private List<TableIndex> indexes;
private List<ForeignKey> foreignKeys;
}
// 列元数据
public class ColumnMetaData {
private String columnName;
private String columnComment;
private String jdbcType; // VARCHAR, INTEGER, DATE
private String javaType; // String, Integer, LocalDate
private int length;
private int precision;
private int scale;
private boolean nullable;
private boolean primaryKey;
private boolean autoIncrement;
private String defaultValue;
}
// 外键关系
public class ForeignKey {
private String fkName;
private String columnName;
private String referencedTable;
private String referencedColumn;
private String deleteRule; // CASCADE, SET NULL, RESTRICT
}元数据读取示例:
java
public class MetaDataReader {
public List<TableMetaData> readTables(DataSource dataSource) throws SQLException {
List<TableMetaData> tables = new ArrayList<>();
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
try (ResultSet rs = metaData.getTables(null, null, "%", new String[]{"TABLE"})) {
while (rs.next()) {
TableMetaData table = new TableMetaData();
table.setTableName(rs.getString("TABLE_NAME"));
table.setTableComment(getTableComment(metaData, table.getTableName()));
table.setColumns(readColumns(metaData, table.getTableName()));
table.setForeignKeys(readForeignKeys(metaData, table.getTableName()));
tables.add(table);
}
}
return tables;
}
private List<ColumnMetaData> readColumns(DatabaseMetaData metaData, String tableName) throws SQLException {
List<ColumnMetaData> columns = new ArrayList<>();
try (ResultSet rs = metaData.getColumns(null, null, tableName, "%")) {
while (rs.next()) {
ColumnMetaData col = new ColumnMetaData();
col.setColumnName(rs.getString("COLUMN_NAME"));
col.setJdbcType(rs.getString("TYPE_NAME"));
col.setLength(rs.getInt("COLUMN_SIZE"));
col.setNullable(rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable);
col.setDefaultValue(rs.getString("COLUMN_DEF"));
col.setColumnComment(rs.getString("REMARKS"));
col.setJavaType(jdbcTypeToJavaType(col.getJdbcType()));
columns.add(col);
}
}
return columns;
}
private String jdbcTypeToJavaType(String jdbcType) {
Map<String, String> mapping = new HashMap<>();
mapping.put("VARCHAR", "String");
mapping.put("CHAR", "String");
mapping.put("INTEGER", "Integer");
mapping.put("BIGINT", "Long");
mapping.put("DECIMAL", "BigDecimal");
mapping.put("DATE", "LocalDate");
mapping.put("DATETIME", "LocalDateTime");
mapping.put("TIMESTAMP", "LocalDateTime");
mapping.put("TINYINT", "Boolean");
mapping.put("TEXT", "String");
mapping.put("BLOB", "byte[]");
return mapping.getOrDefault(jdbcType, "String");
}
}模板引擎选型
代码生成器的核心是将元数据与模板文件结合,生成目标代码。常见的模板引擎对比如下:
| 特性 | Freemarker | Velocity | Thymeleaf | Mustache |
|---|---|---|---|---|
| 语法简洁度 | 中等 | 简单 | 较复杂(XML风格) | 极简 |
| 类型安全 | 弱类型 | 弱类型 | 较强 | 无类型 |
| 自定义指令 | 支持(TemplateDirectiveModel) | 支持(EventHandler) | 支持(Dialect) | 有限 |
| 空值处理 | 内置 ?. 语法 | 需配置 | 内置 | 默认不输出 |
| 宏/函数 | 支持 macro | 支持 macro | 支持 fragment | 支持 partial |
| 性能 | 高(编译缓存) | 高 | 中等 | 高 |
| Spring 整合 | 完善 | 完善 | 原生 Spring Boot | 完善 |
| 社区活跃度 | 高 | 低(已退役) | 高 | 中 |
| 推荐场景 | Java 后端代码生成 | 兼容旧项目 | 前端模板/邮件 | 简单替换场景 |
推荐选型:Freemarker 是代码生成器场景的首选,原因如下:
- 成熟的 Java 模板引擎,社区生态完善
- 支持空值安全处理(
${fieldName!}) - 强大的自定义指令和宏机制,便于复用模板片段
- 编译缓存机制,批量生成时性能优异
- 对纯文本输出无额外标签污染(Thymeleaf 的 XML 标签在代码生成中引入额外复杂度)
生成流程
代码生成的标准流程分为四个阶段:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 读取元数据 │ → │ 加载模板 │ → │ 上下文填充 │ → │ 文件输出 │
│ │ │ │ │ │ │ │
│ DB→Table │ │ .ftl解析 │ │ 注入变量 │ │ write+格式化│
└──────────┘ └──────────┘ └──────────┘ └──────────┘生成引擎核心实现:
java
public class CodeGeneratorEngine {
private Configuration freemarkerConfig;
private MetaDataReader metaDataReader;
private GeneratorConfig config;
public void generate(DataSource dataSource) {
// 1. 读取元数据
List<TableMetaData> tables = metaDataReader.readTables(dataSource);
for (TableMetaData table : tables) {
// 2. 构建上下文
TemplateContext context = buildContext(table);
// 3. 按生成策略渲染模板
List<GeneratorTask> tasks = config.getStrategy().resolveTasks(table);
for (GeneratorTask task : tasks) {
// 4. 模板渲染
String output = renderTemplate(task.getTemplateName(), context);
// 5. 文件输出(含覆盖策略处理)
writeFile(task.getOutputPath(), output, context);
}
}
}
private TemplateContext buildContext(TableMetaData table) {
TemplateContext ctx = new TemplateContext();
ctx.setTableName(table.getTableName());
ctx.setClassName(toPascalCase(table.getTableName()));
ctx.setPackageName(config.getPackageName());
ctx.setFields(table.getColumns().stream()
.map(this::toFieldMeta)
.collect(Collectors.toList()));
ctx.setImports(resolveImports(table.getColumns()));
ctx.setPrimaryKey(findPrimaryKey(table));
return ctx;
}
private String renderTemplate(String templateName, TemplateContext context) {
try {
Template template = freemarkerConfig.getTemplate(templateName);
StringWriter writer = new StringWriter();
template.process(context, writer);
return writer.toString();
} catch (Exception e) {
throw new GenerationException("模板渲染失败: " + templateName, e);
}
}
}代码生成器配置
YAML 配置示例:
yaml
generator:
# 数据源配置
datasource:
url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf-8
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
schema: public
# 包名结构
package:
parent: com.example.project
module: system
entity: entity
mapper: mapper
service: service
serviceImpl: service.impl
controller: controller
dto: dto
vo: vo
converter: converter
# 生成策略
strategy:
# 包含的表(支持通配符)
include-tables:
- sys_user
- sys_role
- sys_menu
# 排除的表
exclude-tables:
- flyway_schema_history
- quartz_*
# 要生成的模块
modules:
- entity
- mapper
- service
- controller
- vue
# 文件覆盖规则
file-override:
entity: skip # 已有实体不覆盖
mapper: override # Mapper 直接覆盖
service: backup # 备份后覆盖
controller: merge # 差异合并
vue: skip
# 表前缀移除(生成类名时自动去除)
table-prefix:
- sys_
- t_
# 模板路径
template:
base-path: templates/codegen
mappings:
entity: Entity.java.ftl
mapper: Mapper.java.ftl
mapperXml: Mapper.xml.ftl
service: Service.java.ftl
serviceImpl: ServiceImpl.java.ftl
controller: Controller.java.ftl
dto: DTO.java.ftl
vo: VO.java.ftl
converter: Converter.java.ftl
vueIndex: index.vue.ftl
vueApi: api.js.ftl模板设计
分层模板体系
代码生成器按分层架构设计模板文件,覆盖后端完整分层和前端页面:
templates/codegen/
├── java/ # Java 后端模板
│ ├── Entity.java.ftl
│ ├── Mapper.java.ftl
│ ├── Mapper.xml.ftl
│ ├── Service.java.ftl
│ ├── ServiceImpl.java.ftl
│ ├── Controller.java.ftl
│ ├── DTO.java.ftl # 数据传输对象(新增/修改请求)
│ ├── VO.java.ftl # 视图对象(列表/详情响应)
│ └── Converter.java.ftl # 实体与 VO/DTO 转换器
├── vue/ # Vue 前端模板
│ ├── index.vue.ftl # 列表页面
│ ├── search-form.vue.ftl # 搜索表单组件
│ ├── dialog-form.vue.ftl # 新增/编辑对话框
│ └── api.js.ftl # API 接口封装
└── common/ # 公共片段
├── import-macro.ftl # 导入语句宏
├── field-macro.ftl # 字段遍历宏
└── annotation-macro.ftl # 注解生成宏模板变量设计
上下文变量注入规范:
java
public class TemplateContext {
// -------- 包名相关 --------
private String parentPackage; // com.example.project
private String modulePackage; // system
private String entityPackage; // com.example.project.system.entity
private String mapperPackage;
private String servicePackage;
private String controllerPackage;
private String dtoPackage;
private String voPackage;
private String converterPackage;
// -------- 类名相关 --------
private String className; // SysUser
private String classNameLower; // sysUser
private String classNameSnake; // sys_user
private String classNamePlural; // SysUserList (集合类名)
// -------- 表相关 --------
private String tableName; // sys_user
private String tableComment; // 系统用户表
// -------- 字段相关 --------
private List<FieldMeta> fields; // 所有字段
private FieldMeta primaryKey; // 主键字段
private List<String> imports; // 需要导入的 Java 类型
// -------- JPA/MyBatis-Plus 相关 --------
private String primaryKeyType; // Long
private String primaryKeyName; // id
private boolean hasLocalDate; // 是否包含日期类型
private boolean hasBigDecimal; // 是否包含 BigDecimal
}
public class FieldMeta {
private String fieldName; // userName
private String fieldNameUpper; // UserName
private String columnName; // user_name
private String fieldType; // String
private String jdbcType; // VARCHAR
private String fieldComment; // 用户名
private int length; // 50
private boolean nullable; // 是否可为空
private boolean primaryKey; // 是否主键
private boolean autoIncrement; // 是否自增
private String defaultValue; // 默认值
private boolean queryable; // 是否参与查询
private boolean listVisible; // 列表是否显示
private boolean formVisible; // 表单是否显示
}CRUD 模板示例
Entity.java.ftl:
java
package ${entityPackage};
<#list imports as import>
import ${import};
</#list>
/**
* ${tableComment}
*/
<#if tableName??>
@TableName("${tableName}")
</#if>
public class ${className} implements Serializable {
private static final long serialVersionUID = 1L;
<#list fields as field>
<#if field.fieldComment??>
/** ${field.fieldComment} */
</#if>
<#if field.primaryKey>
@TableId(value = "${field.columnName}", type = IdType.${field.autoIncrement?then('AUTO', 'INPUT')})
<#else>
@TableField("${field.columnName}")
</#if>
private ${field.fieldType} ${field.fieldName};
</#list>
<#list fields as field>
public ${field.fieldType} get${field.fieldNameUpper}() {
return ${field.fieldName};
}
public void set${field.fieldNameUpper}(${field.fieldType} ${field.fieldName}) {
this.${field.fieldName} = ${field.fieldName};
}
</#list>
}Mapper.java.ftl:
java
package ${mapperPackage};
import ${entityPackage}.${className};
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* ${tableComment} Mapper 接口
*/
@Mapper
public interface ${className}Mapper extends BaseMapper<${className}> {
}Service.java.ftl:
java
package ${servicePackage};
import ${entityPackage}.${className};
import com.baomidou.mybatisplus.extension.service.IService;
/**
* ${tableComment} 服务接口
*/
public interface ${className}Service extends IService<${className}> {
}ServiceImpl.java.ftl:
java
package ${serviceImplPackage};
import ${entityPackage}.${className};
import ${mapperPackage}.${className}Mapper;
import ${servicePackage}.${className}Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* ${tableComment} 服务实现
*/
@Service
public class ${className}ServiceImpl extends ServiceImpl<${className}Mapper, ${className}>
implements ${className}Service {
}Controller.java.ftl:
java
package ${controllerPackage};
import ${entityPackage}.${className};
import ${dtoPackage}.${className}DTO;
import ${voPackage}.${className}VO;
import ${converterPackage}.${className}Converter;
import ${servicePackage}.${className}Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
/**
* ${tableComment} 控制器
*/
@RestController
@RequestMapping("/<#if modulePackage??>${modulePackage}/</#if>${classNameLower}")
@RequiredArgsConstructor
public class ${className}Controller {
private final ${className}Service ${classNameLower}Service;
@GetMapping("/page")
public Result<IPage<${className}VO>> page(${className}DTO dto, PageParam pageParam) {
Page<${className}> page = ${classNameLower}Service.page(pageParam.toPage());
return Result.success(page.convert(${className}Converter.INSTANCE::toVO));
}
@GetMapping("/{id}")
public Result<${className}VO> get(@PathVariable ${primaryKeyType} id) {
${className} entity = ${classNameLower}Service.getById(id);
return Result.success(${className}Converter.INSTANCE.toVO(entity));
}
@PostMapping
public Result<Void> add(@Valid @RequestBody ${className}DTO dto) {
${className} entity = ${className}Converter.INSTANCE.toEntity(dto);
${classNameLower}Service.save(entity);
return Result.success();
}
@PutMapping("/{id}")
public Result<Void> update(@PathVariable ${primaryKeyType} id, @Valid @RequestBody ${className}DTO dto) {
${className} entity = ${className}Converter.INSTANCE.toEntity(dto);
entity.set${primaryKey.fieldNameUpper}(id);
${classNameLower}Service.updateById(entity);
return Result.success();
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable ${primaryKeyType} id) {
${classNameLower}Service.removeById(id);
return Result.success();
}
@GetMapping("/list")
public Result<List<${className}VO>> list(${className}DTO dto) {
LambdaQueryWrapper<${className}> wrapper = buildQueryWrapper(dto);
List<${className}> list = ${classNameLower}Service.list(wrapper);
return Result.success(${className}Converter.INSTANCE.toVOList(list));
}
private LambdaQueryWrapper<${className}> buildQueryWrapper(${className}DTO dto) {
LambdaQueryWrapper<${className}> wrapper = new LambdaQueryWrapper<>();
<#list fields as field>
<#if field.queryable>
<#if field.fieldType == 'String'>
wrapper.like(dto.get${field.fieldNameUpper}() != null, ${className}::get${field.fieldNameUpper}, dto.get${field.fieldNameUpper}());
<#else>
wrapper.eq(dto.get${field.fieldNameUpper}() != null, ${className}::get${field.fieldNameUpper}, dto.get${field.fieldNameUpper}());
</#if>
</#if>
</#list>
return wrapper;
}
}DTO.java.ftl:
java
package ${dtoPackage};
<#list imports as import>
import ${import};
</#list>
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* ${tableComment} 数据传输对象
*/
@Data
@Schema(description = "${tableComment}数据传输对象")
public class ${className}DTO {
<#list fields as field>
<#if field.fieldComment??>
@Schema(description = "${field.fieldComment}")
</#if>
<#if !field.nullable && !field.primaryKey>
@NotBlank<#if field.fieldType != 'String'>(message = "${field.fieldComment}不能为空")</#if>
</#if>
private ${field.fieldType} ${field.fieldName};
</#list>
}VO.java.ftl:
java
package ${voPackage};
<#list imports as import>
import ${import};
</#list>
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* ${tableComment} 视图对象
*/
@Data
@Schema(description = "${tableComment}视图对象")
public class ${className}VO {
<#list fields as field>
<#if field.fieldComment??>
@Schema(description = "${field.fieldComment}")
</#if>
private ${field.fieldType} ${field.fieldName};
</#list>
}Converter.java.ftl:
java
package ${converterPackage};
import ${entityPackage}.${className};
import ${dtoPackage}.${className}DTO;
import ${voPackage}.${className}VO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* ${tableComment} 对象转换器
*/
@Mapper
public interface ${className}Converter {
${className}Converter INSTANCE = Mappers.getMapper(${className}Converter.class);
${className} toEntity(${className}DTO dto);
${className}VO toVO(${className} entity);
List<${className}VO> toVOList(List<${className}> entityList);
}前端模板示例
index.vue.ftl:
<template>
<div class="${classNameLower}-container">
<SearchForm :model="queryParams" @search="handleSearch" @reset="handleReset" />
<div class="table-wrapper">
<div class="table-header">
<div class="title">${tableComment}</div>
<el-button type="primary" @click="handleAdd">新增</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column type="index" label="序号" width="60" align="center" />
<#list fields as field>
<#if field.listVisible>
<el-table-column prop="${field.fieldName}" label="${field.fieldComment}" <#if field.fieldType == 'LocalDateTime'>width="180"</#if> />
</#if>
</#list>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.size"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="getList"
@current-change="getList"
/>
</div>
<DialogForm
v-model="dialogVisible"
:title="dialogTitle"
:form-data="formData"
@success="getList"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { get${className}Page, delete${className} } from '@/api/${classNameLower}'
import SearchForm from './components/search-form.vue'
import DialogForm from './components/dialog-form.vue'
import type { ${className}DTO, ${className}VO } from './types'
const loading = ref(false)
const tableData = ref<${className}VO[]>([])
const total = ref(0)
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formData = ref<${className}DTO>({})
const queryParams = ref({
page: 1,
size: 10,
<#list fields as field>
<#if field.queryable>
${field.fieldName}: undefined,
</#if>
</#list>
})
const getList = async () => {
loading.value = true
try {
const res = await get${className}Page(queryParams.value)
tableData.value = res.data.records
total.value = res.data.total
} finally {
loading.value = false
}
}
const handleSearch = () => {
queryParams.value.page = 1
getList()
}
const handleReset = () => {
queryParams.value = {
page: 1,
size: 10,
<#list fields as field>
<#if field.queryable>
${field.fieldName}: undefined,
</#if>
</#list>
}
getList()
}
const handleAdd = () => {
dialogTitle.value = '新增${tableComment}'
formData.value = {}
dialogVisible.value = true
}
const handleEdit = (row: ${className}VO) => {
dialogTitle.value = '编辑${tableComment}'
formData.value = { ...row }
dialogVisible.value = true
}
const handleDelete = async (row: ${className}VO) => {
await delete${className}(row.id)
getList()
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.${classNameLower}-container {
padding: 16px;
}
.table-wrapper {
background: #fff;
border-radius: 8px;
padding: 16px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.title {
font-size: 16px;
font-weight: 600;
}
</style>search-form.vue.ftl:
<template>
<el-form :model="model" layout="inline" class="search-form">
<#list fields as field>
<#if field.queryable>
<el-form-item label="${field.fieldComment}">
<#if field.fieldType == 'String'>
<el-input v-model="model.${field.fieldName}" placeholder="请输入${field.fieldComment}" clearable />
<#elseif field.fieldType == 'LocalDate' || field.fieldType == 'LocalDateTime'>
<el-date-picker v-model="model.${field.fieldName}" type="date" placeholder="选择${field.fieldComment}" clearable />
<#else>
<el-input-number v-model="model.${field.fieldName}" placeholder="请输入${field.fieldComment}" />
</#if>
</el-form-item>
</#if>
</#list>
<el-form-item>
<el-button type="primary" @click="$emit('search')">查询</el-button>
<el-button @click="$emit('reset')">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
defineProps<{
model: Record<string, any>
}>()
defineEmits<{
search: []
reset: []
}>()
</script>
<style scoped>
.search-form {
background: #fff;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
</style>dialog-form.vue.ftl:
<template>
<el-dialog v-model="visible" :title="title" width="600px">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<#list fields as field>
<#if field.formVisible>
<el-form-item label="${field.fieldComment}" prop="${field.fieldName}">
<#if field.fieldType == 'String'>
<el-input v-model="formData.${field.fieldName}" placeholder="请输入${field.fieldComment}" />
<#elseif field.fieldType == 'LocalDate' || field.fieldType == 'LocalDateTime'>
<el-date-picker v-model="formData.${field.fieldName}" type="date" placeholder="选择${field.fieldComment}" style="width: 100%" />
<#elseif field.fieldType == 'Integer' || field.fieldType == 'Long'>
<el-input-number v-model="formData.${field.fieldName}" placeholder="请输入${field.fieldComment}" style="width: 100%" />
<#elseif field.fieldType == 'BigDecimal'>
<el-input-number v-model="formData.${field.fieldName}" :precision="2" placeholder="请输入${field.fieldComment}" style="width: 100%" />
<#else>
<el-input v-model="formData.${field.fieldName}" placeholder="请输入${field.fieldComment}" />
</#if>
</el-form-item>
</#if>
</#list>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">确认</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { FormInstance } from 'element-plus'
import { add${className}, update${className} } from '@/api/${classNameLower}'
import type { ${className}DTO } from '../types'
const props = defineProps<{
modelValue: boolean
title: string
formData: ${className}DTO
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
success: []
}>()
const visible = ref(false)
const submitting = ref(false)
const formRef = ref<FormInstance>()
watch(() => props.modelValue, (val) => {
visible.value = val
})
watch(visible, (val) => {
emit('update:modelValue', val)
})
const rules: Record<string, any[]> = {
<#list fields as field>
<#if !field.nullable && !field.primaryKey>
${field.fieldName}: [
{ required: true, message: '请输入${field.fieldComment}', trigger: 'blur' }
],
</#if>
</#list>
}
const handleSubmit = async () => {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitting.value = true
try {
if (props.formData.id) {
await update${className}(props.formData.id, props.formData)
} else {
await add${className}(props.formData)
}
visible.value = false
emit('success')
} finally {
submitting.value = false
}
}
</script> break
case 'openDialog':
// 打开对话框
break
case 'callApi':
// 调用 API
break
case 'showMessage':
// 显示消息
break
case 'confirm':
// 确认弹窗
break
}
}自定义生成策略
配置化模板选择
按不同粒度配置模板映射关系,支持三级覆盖:
java
public class TemplateSelectionStrategy {
private Map<String, String> globalTemplates; // 全局默认模板
private Map<String, Map<String, String>> moduleTemplates; // 按模块
private Map<String, Map<String, String>> tableTemplates; // 按表
/**
* 解析指定表的模板映射
* 优先级:按表 > 按模块 > 全局
*/
public Map<String, String> resolveTemplates(String tableName, String moduleName) {
Map<String, String> result = new HashMap<>(globalTemplates);
// 模块级覆盖
if (moduleTemplates.containsKey(moduleName)) {
result.putAll(moduleTemplates.get(moduleName));
}
// 表级覆盖
if (tableTemplates.containsKey(tableName)) {
result.putAll(tableTemplates.get(tableName));
}
return result;
}
}配置示例:
yaml
generator:
template:
# 全局默认模板
global:
entity: templates/default/Entity.java.ftl
mapper: templates/default/Mapper.java.ftl
service: templates/default/Service.java.ftl
controller: templates/default/Controller.java.ftl
vue: templates/default/index.vue.ftl
# 按模块覆盖
modules:
system:
entity: templates/system/Entity.java.ftl # 系统模块使用含审计字段的实体模板
report:
controller: templates/report/Controller.java.ftl # 报表模块使用只读控制器
# 按表覆盖
tables:
sys_config:
controller: templates/config/Controller.java.ftl # 配置表的自定义控制器
sys_log:
service: templates/log/Service.java.ftl # 日志表只读服务文件命名与路径策略
路径解析器实现:
java
public class FilePathResolver {
public String resolveOutputPath(String module, String templateType, TemplateContext context) {
String packagePath = context.getPackageName().replace('.', '/');
switch (templateType) {
case "entity":
return String.format(
"%s/%s/%s.java",
packagePath, context.getEntityPackage(), context.getClassName()
);
case "mapper":
return String.format(
"%s/%s/%sMapper.java",
packagePath, context.getMapperPackage(), context.getClassName()
);
case "service":
return String.format(
"%s/%s/%sService.java",
packagePath, context.getServicePackage(), context.getClassName()
);
case "controller":
return String.format(
"%s/%s/%sController.java",
packagePath, context.getControllerPackage(), context.getClassName()
);
case "vue":
return String.format(
"src/views/%s/%s/index.vue",
module, context.getClassNameLower()
);
case "vueApi":
return String.format(
"src/api/%s/%s.ts",
module, context.getClassNameLower()
);
default:
throw new IllegalArgumentException("未知模板类型: " + templateType);
}
}
public String resolveOutputPathMultiModule(String module, String templateType,
TemplateContext context) {
// 多模块场景:每个模块有独立的 src 目录
String basePath = module + "/src/main/java";
String packagePath = context.getParentPackage().replace('.', '/');
return String.format(
"%s/%s/%s/%s/%s.java",
basePath, packagePath, module, templateType + "s", context.getClassName()
);
}
}覆盖策略
代码生成器的文件覆盖策略决定了当目标文件已存在时的处理方式:
java
public enum OverrideStrategy {
SKIP, // 跳过:保留已有文件,不生成
OVERRIDE, // 覆盖:直接覆盖已有文件
BACKUP, // 备份:将原文件重命名为 .bak 后再生成
MERGE // 差异合并:保留用户修改区域,仅覆盖生成区域
}覆盖策略处理器:
java
public class FileWriteHandler {
private static final Logger log = LoggerFactory.getLogger(FileWriteHandler.class);
public void writeFile(File targetFile, String content, OverrideStrategy strategy) {
if (!targetFile.exists()) {
FileUtils.writeStringToFile(targetFile, content, StandardCharsets.UTF_8);
return;
}
switch (strategy) {
case SKIP:
log.info("跳过已存在文件: {}", targetFile.getPath());
break;
case OVERRIDE:
FileUtils.writeStringToFile(targetFile, content, StandardCharsets.UTF_8);
log.info("覆盖文件: {}", targetFile.getPath());
break;
case BACKUP:
File backupFile = new File(targetFile.getPath() + ".bak."
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
FileUtils.copyFile(targetFile, backupFile);
FileUtils.writeStringToFile(targetFile, content, StandardCharsets.UTF_8);
log.info("已备份至: {}, 重新生成: {}", backupFile.getPath(), targetFile.getPath());
break;
case MERGE:
mergeWithUserCode(targetFile, content);
break;
}
}
/**
* 差异合并:保留用户手动修改的区域
* 通过标记注释 <!-- begin-generated --> / <!-- end-generated --> 来界定可覆盖区域
*/
private void mergeWithUserCode(File targetFile, String newContent) {
try {
String existingContent = FileUtils.readFileToString(targetFile, StandardCharsets.UTF_8);
String merged = performMerge(existingContent, newContent);
FileUtils.writeStringToFile(targetFile, merged, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new GenerationException("合并文件失败: " + targetFile.getPath(), e);
}
}
private String performMerge(String existing, String generated) {
// 使用自定义区域标记进行合并
// 用户代码放在 @Customize @EndCustomize 注释块之间,不会被覆盖
Pattern pattern = Pattern.compile(
"// @Customize[\\s\\S]*?// @EndCustomize",
Pattern.MULTILINE
);
Matcher matcher = pattern.matcher(existing);
List<String> userBlocks = new ArrayList<>();
while (matcher.find()) {
userBlocks.add(matcher.group());
}
if (userBlocks.isEmpty()) {
return generated; // 无用户自定义代码,直接覆盖
}
// 将用户代码块插入到生成的代码末尾
StringBuilder merged = new StringBuilder(generated);
merged.append("\n\n");
merged.append(" // ===== 用户自定义代码 =====\n");
for (String block : userBlocks) {
merged.append(block).append("\n");
}
return merged.toString();
}
}低代码平台引擎
数据模型设计
低代码平台的数据模型是表单、页面、流程的基础,用 JSON Schema 描述实体定义。
实体定义模型:
json
{
"entityCode": "sys_user",
"entityName": "系统用户",
"tableName": "sys_user",
"fields": [
{
"fieldCode": "username",
"fieldName": "用户名",
"fieldType": "string",
"componentType": "input",
"maxLength": 50,
"required": true,
"unique": true,
"defaultValue": null,
"placeholder": "请输入用户名",
"rules": [
{ "type": "required", "message": "用户名不能为空" },
{ "type": "minLength", "value": 2, "message": "用户名至少2个字符" },
{ "type": "pattern", "value": "^[a-zA-Z0-9_]+$", "message": "仅支持字母数字下划线" }
]
},
{
"fieldCode": "email",
"fieldName": "邮箱",
"fieldType": "string",
"componentType": "input",
"maxLength": 100,
"required": false,
"rules": [
{ "type": "pattern", "value": "^\\w+@\\w+\\.\\w+$", "message": "邮箱格式不正确" }
]
},
{
"fieldCode": "age",
"fieldName": "年龄",
"fieldType": "integer",
"componentType": "number",
"minValue": 0,
"maxValue": 150,
"required": false
},
{
"fieldCode": "birthday",
"fieldName": "生日",
"fieldType": "date",
"componentType": "datePicker",
"required": false
},
{
"fieldCode": "status",
"fieldName": "状态",
"fieldType": "integer",
"componentType": "select",
"required": true,
"defaultValue": 1,
"options": [
{ "label": "启用", "value": 1 },
{ "label": "禁用", "value": 0 }
]
},
{
"fieldCode": "deptId",
"fieldName": "所属部门",
"fieldType": "integer",
"componentType": "treeSelect",
"required": false,
"relation": {
"type": "manyToOne",
"targetEntity": "sys_dept",
"targetField": "deptName",
"foreignKey": "dept_id"
}
}
],
"relations": [
{
"relationType": "oneToMany",
"targetEntity": "sys_user_role",
"targetField": "userId",
"cascade": "ALL"
}
]
}Java 实体定义类:
java
public class EntityDefinition {
private String id;
private String entityCode; // 实体编码
private String entityName; // 实体名称
private String tableName; // 数据库表名
private String description; // 描述
private List<FieldDefinition> fields;
private List<RelationDefinition> relations;
private JSONSchema schema; // JSON Schema 描述
}
public class FieldDefinition {
private String fieldCode; // 字段编码
private String fieldName; // 字段名称
private FieldType fieldType; // string, integer, number, boolean, date, datetime, array, object
private String componentType; // input, number, select, datePicker, radio, checkbox, switch, treeSelect, upload
private boolean required;
private boolean unique;
private Object defaultValue;
private String placeholder;
private List<ValidateRule> rules; // 校验规则
private Integer maxLength;
private Integer minValue;
private Integer maxValue;
private List<OptionItem> options; // 选项列表(select/radio/checkbox 使用)
private RelationInfo relation; // 关联关系
private FieldLayout layout; // 布局信息
}
public enum FieldType {
STRING, INTEGER, NUMBER, BOOLEAN, DATE, DATETIME, ARRAY, OBJECT, FILE, IMAGE
}
public class ValidateRule {
private String type; // required, minLength, maxLength, pattern, min, max, custom
private Object value;
private String message;
}
public class RelationInfo {
private RelationType type; // oneToOne, manyToOne, oneToMany, manyToMany
private String targetEntity;
private String targetField;
private String foreignKey;
private String cascade; // ALL, PERSIST, MERGE, REMOVE, NONE
}JSON Schema 生成:
java
public class JsonSchemaGenerator {
public JSONSchema generateSchema(EntityDefinition entity) {
JSONSchema schema = new JSONSchema();
schema.setTitle(entity.getEntityName());
schema.setType("object");
schema.setProperties(new HashMap<>());
for (FieldDefinition field : entity.getFields()) {
JSONSchemaProperty prop = new JSONSchemaProperty();
switch (field.getFieldType()) {
case STRING:
prop.setType("string");
if (field.getMaxLength() != null) {
prop.setMaxLength(field.getMaxLength());
}
if (field.getPlaceholder() != null) {
prop.setDescription(field.getPlaceholder());
}
break;
case INTEGER:
prop.setType("integer");
if (field.getMinValue() != null) prop.setMinimum(field.getMinValue());
if (field.getMaxValue() != null) prop.setMaximum(field.getMaxValue());
break;
case NUMBER:
prop.setType("number");
break;
case BOOLEAN:
prop.setType("boolean");
break;
case DATE:
prop.setType("string");
prop.setFormat("date");
break;
case DATETIME:
prop.setType("string");
prop.setFormat("date-time");
break;
case ARRAY:
prop.setType("array");
prop.setItems(new JSONSchemaProperty());
break;
}
if (field.getRules() != null) {
for (ValidateRule rule : field.getRules()) {
switch (rule.getType()) {
case "required":
schema.addRequired(field.getFieldCode());
break;
case "pattern":
prop.setPattern((String) rule.getValue());
break;
case "minLength":
prop.setMinLength((Integer) rule.getValue());
break;
case "maxLength":
prop.setMaxLength((Integer) rule.getValue());
break;
}
}
}
schema.getProperties().put(field.getFieldCode(), prop);
}
return schema;
}
}