前言:一个被重复代码折磨的程序员的觉醒
还记得那个阳光明媚的下午吗?
你刚入职银行项目组,领导拍着你的肩膀说:"小伙子,这个项目有200多个接口要对接,都是类似的,应该很快能写完。"
于是你信心满满地打开了接口文档,发现确实很类似——每个接口都需要写Request类、Response类、Service类、Controller方法...然后你算了一下:
200个接口 × 4个类 × 平均50行代码 = 40000行代码你的眼前一黑。
更可怕的是,当你写到第50个接口的时候,产品经理走过来告诉你:"那个,接口规范改了,所有的Response要统一加一个timestamp字段。"
你看着已经写好的49个Response类,感觉自己的青春在流逝。
是时候做出改变了。
一、为什么要自己写代码生成器?
1.1 现有工具的局限性
"不是有MyBatis Generator吗?不是有各种脚手架吗?"
是的,但是:
现有工具的问题在于:
| 痛点 | 表现 |
|---|---|
| 场景单一 | 只能生成数据库相关代码,接口对接、前端页面就无能为力了 |
| 模板死板 | 生成的代码风格和项目规范不匹配,还得手动调整 |
| 不支持追加 | 表结构改了,要么重新生成覆盖,要么手动改,都很痛苦 |
| 语言限制 | Java的生成器不能生成Python,Go的生成器不能生成Java |
1.2 我们需要什么样的代码生成器?
Java/Python/Go/前端都行"] F2["📝 模板可定制
想生成啥样就啥样"] F3["➕ 支持追加
改了配置不用重写"] F4["🖥️ 操作友好
命令行+图形界面"] F5["🏠 本地运行
代码不外泄"] end F1 & F2 & F3 & F4 & F5 --> RESULT["一键生成,下班提前 🎉"] style RESULT fill:#90EE90
简单说就是:给我一个数据源,我告诉你要生成什么样的代码,你就给我批量生成,而且改了还能追加。
这不就是程序员应该干的事吗?把重复劳动自动化!
二、代码生成器的核心设计
2.1 整体架构
别被"代码生成器"这个名字吓到,本质上它就是一个模板引擎的应用:
JSON/Excel/API文档"] B["📄 模板文件
Jinja2模板"] C["⚙️ 配置文件
生成规则"] end subgraph ENGINE["生成引擎"] E1["解析数据源"] E2["加载模板"] E3["变量替换"] E4["代码格式化"] end subgraph OUTPUT["输出"] O["📁 生成的代码文件"] end A --> E1 B --> E2 C --> E1 & E2 E1 --> E3 E2 --> E3 E3 --> E4 E4 --> O
核心公式:数据 + 模板 = 代码
就这么简单。复杂的是如何让这个过程足够灵活、足够易用。
2.2 核心模块设计
DataParser"] TEMPLATE["📄 模板引擎
TemplateEngine"] GENERATOR["⚙️ 生成器
CodeGenerator"] APPENDER["➕ 追加器
CodeAppender"] end subgraph UI["交互层"] CLI["💻 命令行
CLI Interface"] GUI["🖥️ 图形界面
Web UI"] end subgraph CONFIG["配置层"] PROJECT["项目配置"] TEMPLATES["模板管理"] end end CLI & GUI --> CORE CONFIG --> CORE PARSER --> TEMPLATE --> GENERATOR GENERATOR --> APPENDER
让我逐个解释:
数据解析器(DataParser):负责把各种格式的输入(JSON、Excel、YAML)转换成统一的内部数据结构。
模板引擎(TemplateEngine):基于Jinja2,负责模板的加载、渲染、变量替换。
生成器(CodeGenerator):核心逻辑,根据数据和模板生成目标代码。
追加器(CodeAppender):智能追加模式,可以在已有代码中插入新内容而不覆盖原有代码。
三、动手实现
3.1 项目结构
gaia/
├── gaia/
│ ├── __init__.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── parser.py # 数据解析
│ │ ├── engine.py # 模板引擎
│ │ ├── generator.py # 代码生成
│ │ └── appender.py # 代码追加
│ ├── templates/ # 内置模板
│ │ ├── java/
│ │ ├── python/
│ │ └── frontend/
│ ├── cli.py # 命令行入口
│ └── web/ # Web界面
│ ├── app.py
│ └── templates/
├── config/
│ └── settings.yaml # 全局配置
├── manage.py # 主入口
├── setup.py
└── README.md3.2 数据解析器实现
首先,我们需要一个能处理多种数据源的解析器:
# gaia/core/parser.py
import json
import yaml
from abc import ABC, abstractmethod
from typing import Dict, List, Any
from pathlib import Path
class DataParser(ABC):
"""数据解析器基类 - 所有解析器的爸爸"""
@abstractmethod
def parse(self, source: str) -> List[Dict[str, Any]]:
"""
解析数据源,返回统一格式的数据列表
为什么返回List?因为一个数据源可能包含多个实体(比如多个接口)
"""
pass
class JSONParser(DataParser):
"""JSON解析器 - 最常用的格式"""
def parse(self, source: str) -> List[Dict[str, Any]]:
"""
支持两种输入:
1. JSON文件路径
2. JSON字符串
"""
if Path(source).exists():
with open(source, 'r', encoding='utf-8') as f:
data = json.load(f)
else:
data = json.loads(source)
# 统一转换为列表格式
if isinstance(data, dict):
return [data]
return data
class YAMLParser(DataParser):
"""YAML解析器 - 写配置更舒服"""
def parse(self, source: str) -> List[Dict[str, Any]]:
if Path(source).exists():
with open(source, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
else:
data = yaml.safe_load(source)
if isinstance(data, dict):
return [data]
return data
class ExcelParser(DataParser):
"""Excel解析器 - 产品经理最爱"""
def parse(self, source: str) -> List[Dict[str, Any]]:
import pandas as pd
df = pd.read_excel(source)
# 将DataFrame转换为字典列表
return df.to_dict('records')
class ParserFactory:
"""
解析器工厂 - 根据文件类型自动选择解析器
设计模式这不就用上了嘛!
"""
_parsers = {
'.json': JSONParser,
'.yaml': YAMLParser,
'.yml': YAMLParser,
'.xlsx': ExcelParser,
'.xls': ExcelParser,
}
@classmethod
def get_parser(cls, source: str) -> DataParser:
"""根据文件后缀返回对应的解析器"""
suffix = Path(source).suffix.lower()
parser_class = cls._parsers.get(suffix)
if not parser_class:
# 默认尝试JSON解析
return JSONParser()
return parser_class()
@classmethod
def register(cls, suffix: str, parser_class: type):
"""支持注册自定义解析器,扩展性拉满"""
cls._parsers[suffix] = parser_class3.3 模板引擎封装
Jinja2已经很强大了,我们只需要做一层封装,加上一些便捷功能:
# gaia/core/engine.py
from jinja2 import Environment, FileSystemLoader, select_autoescape
from typing import Dict, Any, Optional
from pathlib import Path
import re
class TemplateEngine:
"""
模板引擎 - Jinja2的豪华封装版
为什么要封装?
1. 统一模板路径管理
2. 添加自定义过滤器(这是精髓!)
3. 支持模板继承和组合
"""
def __init__(self, template_dirs: list = None):
"""
Args:
template_dirs: 模板目录列表,支持多目录
"""
self.template_dirs = template_dirs or ['./templates']
self.env = Environment(
loader=FileSystemLoader(self.template_dirs),
autoescape=select_autoescape(['html', 'xml']),
trim_blocks=True, # 去除块后的第一个换行
lstrip_blocks=True, # 去除块前的空白
)
# 注册自定义过滤器 - 这是代码生成的灵魂
self._register_filters()
def _register_filters(self):
"""注册一堆好用的过滤器"""
# 命名转换过滤器
self.env.filters['camel_case'] = self._to_camel_case
self.env.filters['pascal_case'] = self._to_pascal_case
self.env.filters['snake_case'] = self._to_snake_case
self.env.filters['kebab_case'] = self._to_kebab_case
# 类型转换过滤器
self.env.filters['java_type'] = self._to_java_type
self.env.filters['python_type'] = self._to_python_type
self.env.filters['ts_type'] = self._to_typescript_type
# 字符串处理
self.env.filters['pluralize'] = self._pluralize
self.env.filters['singularize'] = self._singularize
self.env.filters['first_lower'] = lambda s: s[0].lower() + s[1:] if s else s
self.env.filters['first_upper'] = lambda s: s[0].upper() + s[1:] if s else s
@staticmethod
def _to_camel_case(s: str) -> str:
"""user_name -> userName"""
components = s.split('_')
return components[0] + ''.join(x.title() for x in components[1:])
@staticmethod
def _to_pascal_case(s: str) -> str:
"""user_name -> UserName"""
return ''.join(x.title() for x in s.split('_'))
@staticmethod
def _to_snake_case(s: str) -> str:
"""userName -> user_name"""
return re.sub(r'(?<!^)(?=[A-Z])', '_', s).lower()
@staticmethod
def _to_kebab_case(s: str) -> str:
"""userName -> user-name"""
return re.sub(r'(?<!^)(?=[A-Z])', '-', s).lower()
@staticmethod
def _to_java_type(db_type: str) -> str:
"""数据库类型转Java类型"""
type_mapping = {
'varchar': 'String',
'char': 'String',
'text': 'String',
'int': 'Integer',
'bigint': 'Long',
'decimal': 'BigDecimal',
'float': 'Float',
'double': 'Double',
'datetime': 'LocalDateTime',
'date': 'LocalDate',
'timestamp': 'LocalDateTime',
'boolean': 'Boolean',
'tinyint': 'Integer',
}
return type_mapping.get(db_type.lower(), 'String')
@staticmethod
def _to_python_type(db_type: str) -> str:
"""数据库类型转Python类型"""
type_mapping = {
'varchar': 'str',
'char': 'str',
'text': 'str',
'int': 'int',
'bigint': 'int',
'decimal': 'Decimal',
'float': 'float',
'double': 'float',
'datetime': 'datetime',
'date': 'date',
'timestamp': 'datetime',
'boolean': 'bool',
'tinyint': 'int',
}
return type_mapping.get(db_type.lower(), 'str')
@staticmethod
def _to_typescript_type(db_type: str) -> str:
"""数据库类型转TypeScript类型"""
type_mapping = {
'varchar': 'string',
'char': 'string',
'text': 'string',
'int': 'number',
'bigint': 'number',
'decimal': 'number',
'float': 'number',
'double': 'number',
'datetime': 'Date',
'date': 'Date',
'timestamp': 'Date',
'boolean': 'boolean',
'tinyint': 'number',
}
return type_mapping.get(db_type.lower(), 'string')
@staticmethod
def _pluralize(s: str) -> str:
"""简单的复数转换 user -> users"""
if s.endswith('y'):
return s[:-1] + 'ies'
elif s.endswith(('s', 'x', 'ch', 'sh')):
return s + 'es'
else:
return s + 's'
@staticmethod
def _singularize(s: str) -> str:
"""简单的单数转换 users -> user"""
if s.endswith('ies'):
return s[:-3] + 'y'
elif s.endswith('es'):
return s[:-2]
elif s.endswith('s'):
return s[:-1]
return s
def render(self, template_name: str, context: Dict[str, Any]) -> str:
"""
渲染模板
Args:
template_name: 模板文件名
context: 模板变量字典
Returns:
渲染后的字符串
"""
template = self.env.get_template(template_name)
return template.render(**context)
def render_string(self, template_str: str, context: Dict[str, Any]) -> str:
"""直接渲染字符串模板,适合简单场景"""
template = self.env.from_string(template_str)
return template.render(**context)3.4 代码生成器核心
把解析器和模板引擎组合起来:
# gaia/core/generator.py
from pathlib import Path
from typing import Dict, List, Any, Optional
from datetime import datetime
import os
from .parser import ParserFactory
from .engine import TemplateEngine
class CodeGenerator:
"""
代码生成器 - 真正干活的地方
工作流程:
1. 读取数据源
2. 解析成统一格式
3. 遍历数据,逐个渲染模板
4. 输出到目标文件
"""
def __init__(self, config: Dict[str, Any] = None):
"""
Args:
config: 生成器配置
- template_dirs: 模板目录列表
- output_dir: 输出目录
- overwrite: 是否覆盖已有文件
"""
self.config = config or {}
self.template_dirs = self.config.get('template_dirs', ['./templates'])
self.output_dir = self.config.get('output_dir', './output')
self.overwrite = self.config.get('overwrite', False)
self.engine = TemplateEngine(self.template_dirs)
self.generated_files = [] # 记录生成的文件
def generate(
self,
data_source: str,
template_name: str,
output_pattern: str,
extra_context: Dict[str, Any] = None
) -> List[str]:
"""
生成代码文件
Args:
data_source: 数据源路径
template_name: 模板文件名
output_pattern: 输出文件名模式,支持变量替换
如 "{name}Service.java"
extra_context: 额外的模板变量
Returns:
生成的文件路径列表
Example:
generator.generate(
data_source='api_list.json',
template_name='java/service.java.j2',
output_pattern='{name}Service.java',
extra_context={'author': 'Joey', 'package': 'com.example'}
)
"""
# 1. 解析数据
parser = ParserFactory.get_parser(data_source)
data_list = parser.parse(data_source)
generated = []
# 2. 遍历生成
for data in data_list:
# 构建上下文
context = {
**data,
**(extra_context or {}),
'_meta': {
'generated_at': datetime.now().isoformat(),
'generator': 'Gaia Code Generator',
'template': template_name,
}
}
# 3. 渲染模板
content = self.engine.render(template_name, context)
# 4. 生成输出文件名
output_name = self._resolve_output_name(output_pattern, data)
output_path = Path(self.output_dir) / output_name
# 5. 写入文件
self._write_file(output_path, content)
generated.append(str(output_path))
self.generated_files.extend(generated)
return generated
def generate_batch(self, tasks: List[Dict[str, Any]]) -> Dict[str, List[str]]:
"""
批量生成 - 一次配置,多种输出
Args:
tasks: 任务列表,每个任务包含:
- data_source: 数据源
- template_name: 模板
- output_pattern: 输出模式
- extra_context: 额外上下文(可选)
Returns:
{任务名: 生成的文件列表}
Example:
tasks = [
{
'name': 'entity',
'data_source': 'tables.json',
'template_name': 'java/entity.java.j2',
'output_pattern': 'entity/{name | pascal_case}.java'
},
{
'name': 'mapper',
'data_source': 'tables.json',
'template_name': 'java/mapper.java.j2',
'output_pattern': 'mapper/{name | pascal_case}Mapper.java'
},
]
generator.generate_batch(tasks)
"""
results = {}
for task in tasks:
task_name = task.get('name', task['template_name'])
generated = self.generate(
data_source=task['data_source'],
template_name=task['template_name'],
output_pattern=task['output_pattern'],
extra_context=task.get('extra_context')
)
results[task_name] = generated
return results
def _resolve_output_name(self, pattern: str, data: Dict[str, Any]) -> str:
"""
解析输出文件名模式
支持 {variable} 和 {variable | filter} 格式
"""
# 简单的模板渲染
return self.engine.render_string(pattern, data)
def _write_file(self, path: Path, content: str):
"""写入文件,自动创建目录"""
# 检查是否覆盖
if path.exists() and not self.overwrite:
print(f"⚠️ 跳过已存在的文件: {path}")
return
# 创建目录
path.parent.mkdir(parents=True, exist_ok=True)
# 写入
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"✅ 生成文件: {path}")
def summary(self) -> str:
"""生成摘要报告"""
return f"""
========================================
Gaia 代码生成报告
========================================
生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
输出目录: {self.output_dir}
生成文件数: {len(self.generated_files)}
----------------------------------------
文件列表:
{chr(10).join(f' - {f}' for f in self.generated_files)}
========================================
"""3.5 代码追加器(高级功能)
这是区别于其他生成器的关键功能——能在已有代码中智能追加内容:
# gaia/core/appender.py
import re
from pathlib import Path
from typing import Optional, List
from enum import Enum
class InsertPosition(Enum):
"""插入位置"""
BEFORE = 'before' # 在标记之前
AFTER = 'after' # 在标记之后
REPLACE = 'replace' # 替换标记区域
class CodeAppender:
"""
代码追加器 - 在已有代码中插入新内容
工作原理:
1. 在代码中预留标记点(Magic Comment)
2. 追加时找到标记点,在指定位置插入新代码
3. 支持多种插入策略
标记格式:
- 单行标记: // @gaia:insert:name
- 区域标记: // @gaia:start:name ... // @gaia:end:name
"""
# 标记正则
SINGLE_MARKER_PATTERN = r'[#/]+\s*@gaia:insert:(\w+)'
REGION_START_PATTERN = r'[#/]+\s*@gaia:start:(\w+)'
REGION_END_PATTERN = r'[#/]+\s*@gaia:end:(\w+)'
def __init__(self):
self.modified_files = []
def append(
self,
file_path: str,
marker_name: str,
content: str,
position: InsertPosition = InsertPosition.AFTER,
deduplicate: bool = True
) -> bool:
"""
在文件的标记位置追加内容
Args:
file_path: 目标文件路径
marker_name: 标记名称
content: 要追加的内容
position: 插入位置
deduplicate: 是否去重(避免重复追加)
Returns:
是否成功追加
Example:
# 在 UserService.java 中有这样的标记:
# // @gaia:insert:imports
# import xxx;
appender.append(
'UserService.java',
'imports',
'import com.example.NewClass;'
)
"""
path = Path(file_path)
if not path.exists():
print(f"❌ 文件不存在: {file_path}")
return False
original = path.read_text(encoding='utf-8')
# 去重检查
if deduplicate and content.strip() in original:
print(f"⏭️ 内容已存在,跳过: {marker_name}")
return False
# 尝试单行标记
modified = self._append_single_marker(
original, marker_name, content, position
)
# 如果单行标记失败,尝试区域标记
if modified == original:
modified = self._append_region_marker(
original, marker_name, content, position
)
if modified == original:
print(f"⚠️ 未找到标记: {marker_name}")
return False
# 写回文件
path.write_text(modified, encoding='utf-8')
self.modified_files.append(str(path))
print(f"✅ 追加成功: {file_path} @ {marker_name}")
return True
def _append_single_marker(
self,
content: str,
marker_name: str,
new_content: str,
position: InsertPosition
) -> str:
"""处理单行标记"""
pattern = rf'([^\n]*[#/]+\s*@gaia:insert:{marker_name}[^\n]*\n?)'
match = re.search(pattern, content)
if not match:
return content
marker_line = match.group(1)
if position == InsertPosition.BEFORE:
replacement = new_content + '\n' + marker_line
elif position == InsertPosition.AFTER:
replacement = marker_line + new_content + '\n'
else: # REPLACE
replacement = new_content + '\n'
return content[:match.start()] + replacement + content[match.end():]
def _append_region_marker(
self,
content: str,
marker_name: str,
new_content: str,
position: InsertPosition
) -> str:
"""处理区域标记"""
start_pattern = rf'([^\n]*[#/]+\s*@gaia:start:{marker_name}[^\n]*\n)'
end_pattern = rf'([^\n]*[#/]+\s*@gaia:end:{marker_name}[^\n]*)'
start_match = re.search(start_pattern, content)
end_match = re.search(end_pattern, content)
if not start_match or not end_match:
return content
if position == InsertPosition.REPLACE:
# 替换整个区域内容
before = content[:start_match.end()]
after = content[end_match.start():]
return before + new_content + '\n' + after
elif position == InsertPosition.AFTER:
# 在区域末尾追加
insert_point = end_match.start()
return content[:insert_point] + new_content + '\n' + content[insert_point:]
else: # BEFORE
# 在区域开头追加
insert_point = start_match.end()
return content[:insert_point] + new_content + '\n' + content[insert_point:]
def batch_append(
self,
file_path: str,
appends: List[dict]
) -> int:
"""
批量追加
Args:
file_path: 目标文件
appends: 追加任务列表
[{'marker': 'xxx', 'content': 'xxx', 'position': 'after'}]
Returns:
成功追加的数量
"""
success_count = 0
for item in appends:
position = InsertPosition(item.get('position', 'after'))
if self.append(file_path, item['marker'], item['content'], position):
success_count += 1
return success_count3.6 命令行接口
让工具可以从命令行调用:
# gaia/cli.py
import click
import yaml
from pathlib import Path
from .core.generator import CodeGenerator
from .core.appender import CodeAppender, InsertPosition
@click.group()
@click.version_option(version='1.0.0', prog_name='Gaia')
def cli():
"""
🌍 Gaia - 自动化代码生成器
让机器帮你搬砖,生生不息
"""
pass
@cli.command()
@click.option('--name', '-n', default='my-project', help='项目名称')
@click.option('--path', '-p', default='.', help='项目路径')
def init(name, path):
"""初始化代码生成项目"""
project_path = Path(path) / name
# 创建目录结构
dirs = ['templates', 'data', 'output', 'config']
for d in dirs:
(project_path / d).mkdir(parents=True, exist_ok=True)
# 创建示例配置文件
config = {
'project_name': name,
'template_dirs': ['./templates'],
'output_dir': './output',
'overwrite': False,
'tasks': [
{
'name': 'example',
'data_source': './data/example.json',
'template_name': 'example.j2',
'output_pattern': '{name}.txt'
}
]
}
config_file = project_path / 'config' / 'gaia.yaml'
with open(config_file, 'w') as f:
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
# 创建示例模板
example_template = '''# {{ name }}
Generated by Gaia at {{ _meta.generated_at }}
## Description
{{ description | default('No description') }}
## Fields
{% for field in fields %}
- {{ field.name }}: {{ field.type }}
{% endfor %}
'''
template_file = project_path / 'templates' / 'example.j2'
template_file.write_text(example_template)
# 创建示例数据
example_data = '''[
{
"name": "User",
"description": "用户实体",
"fields": [
{"name": "id", "type": "int"},
{"name": "username", "type": "string"},
{"name": "email", "type": "string"}
]
}
]
'''
data_file = project_path / 'data' / 'example.json'
data_file.write_text(example_data)
click.echo(f"""
✨ 项目初始化完成!
📁 项目结构:
{name}/
├── config/
│ └── gaia.yaml # 配置文件
├── templates/
│ └── example.j2 # 示例模板
├── data/
│ └── example.json # 示例数据
└── output/ # 输出目录
🚀 下一步:
cd {name}
gaia generate --config config/gaia.yaml
""")
@cli.command()
@click.option('--config', '-c', required=True, help='配置文件路径')
@click.option('--task', '-t', default=None, help='指定任务名(不指定则执行全部)')
@click.option('--overwrite', is_flag=True, help='覆盖已有文件')
def generate(config, task, overwrite):
"""根据配置生成代码"""
config_path = Path(config)
if not config_path.exists():
click.echo(f"❌ 配置文件不存在: {config}")
return
with open(config_path) as f:
cfg = yaml.safe_load(f)
# 覆盖配置
if overwrite:
cfg['overwrite'] = True
generator = CodeGenerator(cfg)
tasks = cfg.get('tasks', [])
if task:
tasks = [t for t in tasks if t.get('name') == task]
if not tasks:
click.echo(f"❌ 未找到任务: {task}")
return
results = generator.generate_batch(tasks)
click.echo(generator.summary())
@cli.command()
@click.option('--file', '-f', required=True, help='目标文件')
@click.option('--marker', '-m', required=True, help='标记名称')
@click.option('--content', '-c', required=True, help='追加内容')
@click.option('--position', '-p', default='after',
type=click.Choice(['before', 'after', 'replace']))
def append(file, marker, content, position):
"""向已有文件追加代码"""
appender = CodeAppender()
pos = InsertPosition(position)
success = appender.append(file, marker, content, pos)
if success:
click.echo("✅ 追加完成!")
else:
click.echo("❌ 追加失败,请检查标记是否存在")
@cli.command()
def templates():
"""列出内置模板"""
built_in = Path(__file__).parent / 'templates'
click.echo("\n📄 内置模板列表:\n")
for category in built_in.iterdir():
if category.is_dir():
click.echo(f" 📁 {category.name}/")
for template in category.glob('*.j2'):
click.echo(f" - {template.name}")
click.echo()
if __name__ == '__main__':
cli()四、模板编写指南
模板是代码生成器的灵魂,写好模板能让你事半功倍。
4.1 模板语法速成
Jinja2模板语法非常简单:
{# 这是注释 #}
{# 变量输出 #}
{{ variable }}
{# 带过滤器的变量 #}
{{ name | pascal_case }}
{# 条件判断 #}
{% if condition %}
do something
{% elif other_condition %}
do other thing
{% else %}
default
{% endif %}
{# 循环 #}
{% for item in items %}
{{ item.name }}
{% endfor %}
{# 设置变量 #}
{% set full_name = first_name + ' ' + last_name %}4.2 实战模板示例
Java实体类模板:
{# templates/java/entity.java.j2 #}
package {{ package }}.entity;
import lombok.Data;
import java.time.LocalDateTime;
{% if has_decimal %}
import java.math.BigDecimal;
{% endif %}
/**
* {{ description | default(name + '实体类') }}
*
* @author {{ author | default('Gaia Generator') }}
* @date {{ _meta.generated_at[:10] }}
*/
@Data
public class {{ name | pascal_case }} {
{% for field in fields %}
/**
* {{ field.comment | default(field.name) }}
*/
private {{ field.type | java_type }} {{ field.name | camel_case }};
{% endfor %}
}Spring Controller模板:
{# templates/java/controller.java.j2 #}
package {{ package }}.controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import {{ package }}.service.{{ name | pascal_case }}Service;
import {{ package }}.entity.{{ name | pascal_case }};
/**
* {{ name | pascal_case }} 控制器
*/
@RestController
@RequestMapping("/api/{{ name | kebab_case }}")
public class {{ name | pascal_case }}Controller {
@Autowired
private {{ name | pascal_case }}Service {{ name | camel_case }}Service;
@GetMapping("/{id}")
public {{ name | pascal_case }} getById(@PathVariable Long id) {
return {{ name | camel_case }}Service.getById(id);
}
@PostMapping
public {{ name | pascal_case }} create(@RequestBody {{ name | pascal_case }} entity) {
return {{ name | camel_case }}Service.save(entity);
}
@PutMapping("/{id}")
public {{ name | pascal_case }} update(@PathVariable Long id,
@RequestBody {{ name | pascal_case }} entity) {
entity.setId(id);
return {{ name | camel_case }}Service.update(entity);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
{{ name | camel_case }}Service.deleteById(id);
}
// @gaia:insert:methods
}前端Vue组件模板:
{# templates/vue/list.vue.j2 #}
<template>
<div class="{{ name | kebab_case }}-list">
<el-table :data="tableData" border>
{% for field in fields %}
<el-table-column
prop="{{ field.name | camel_case }}"
label="{{ field.comment | default(field.name) }}"
{% if field.width %}width="{{ field.width }}"{% endif %}
/>
{% endfor %}
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { get{{ name | pascal_case }}List } from '@/api/{{ name | kebab_case }}'
const tableData = ref([])
onMounted(async () => {
const res = await get{{ name | pascal_case }}List()
tableData.value = res.data
})
const handleEdit = (row) => {
// TODO: 编辑逻辑
}
const handleDelete = (row) => {
// TODO: 删除逻辑
}
</script>
<style scoped>
.{{ name | kebab_case }}-list {
padding: 20px;
}
</style>4.3 数据源格式建议
好的数据源格式能让模板更简单。推荐格式:
{
"name": "user",
"description": "用户信息表",
"author": "Joey",
"fields": [
{
"name": "id",
"type": "bigint",
"comment": "主键ID",
"primary": true
},
{
"name": "user_name",
"type": "varchar",
"comment": "用户名",
"length": 50,
"nullable": false
},
{
"name": "email",
"type": "varchar",
"comment": "邮箱",
"length": 100
},
{
"name": "created_at",
"type": "datetime",
"comment": "创建时间"
}
]
}五、项目如何使用
5.1 安装
# 方式1:从源码安装
git clone https://github.com/underestimatedme/gaia.git
cd gaia
python3 setup.py install
# 方式2:pip安装(如果已发布)
pip install gaia-generator5.2 快速开始
# 1. 初始化项目
gaia init --name my-codegen
# 2. 进入项目目录
cd my-codegen
# 3. 编辑配置和模板(按需修改)
vim config/gaia.yaml
vim templates/example.j2
# 4. 准备数据源
vim data/example.json
# 5. 生成代码
gaia generate --config config/gaia.yaml
# 6. 查看生成结果
ls output/5.3 配置文件详解
# config/gaia.yaml
# 项目基本信息
project_name: bank-api-generator
# 模板目录(支持多个)
template_dirs:
- ./templates
- /shared/templates
# 输出目录
output_dir: ./output
# 是否覆盖已有文件
overwrite: false
# 全局变量(所有模板都可使用)
globals:
author: Joey
company: AstraTech
package: com.astratech.bank
# 生成任务列表
tasks:
# 任务1:生成实体类
- name: entity
data_source: ./data/tables.json
template_name: java/entity.java.j2
output_pattern: "entity/{{ name | pascal_case }}.java"
extra_context:
lombok: true
# 任务2:生成Mapper
- name: mapper
data_source: ./data/tables.json
template_name: java/mapper.java.j2
output_pattern: "mapper/{{ name | pascal_case }}Mapper.java"
# 任务3:生成Service
- name: service
data_source: ./data/tables.json
template_name: java/service.java.j2
output_pattern: "service/{{ name | pascal_case }}Service.java"
# 任务4:生成Controller
- name: controller
data_source: ./data/tables.json
template_name: java/controller.java.j2
output_pattern: "controller/{{ name | pascal_case }}Controller.java"5.4 命令行参考
# 查看帮助
gaia --help
# 初始化项目
gaia init --name <project-name> --path <path>
# 生成代码(全部任务)
gaia generate --config <config-file>
# 生成代码(指定任务)
gaia generate --config <config-file> --task entity
# 生成代码(覆盖模式)
gaia generate --config <config-file> --overwrite
# 追加代码
gaia append --file <file> --marker <marker-name> --content <content>
# 查看内置模板
gaia templates六、高级玩法
6.1 从数据库自动读取表结构
# 扩展数据源解析器
from gaia.core.parser import DataParser
import pymysql
class MySQLSchemaParser(DataParser):
"""直接从MySQL数据库读取表结构"""
def __init__(self, host, user, password, database):
self.conn = pymysql.connect(
host=host, user=user,
password=password, database=database
)
def parse(self, table_pattern: str = '%'):
cursor = self.conn.cursor()
# 获取表列表
cursor.execute(f"SHOW TABLES LIKE '{table_pattern}'")
tables = [row[0] for row in cursor.fetchall()]
result = []
for table in tables:
# 获取表结构
cursor.execute(f"DESCRIBE {table}")
fields = []
for row in cursor.fetchall():
fields.append({
'name': row[0],
'type': row[1].split('(')[0],
'nullable': row[2] == 'YES',
'primary': row[3] == 'PRI',
'default': row[4],
})
result.append({
'name': table,
'fields': fields
})
return result
# 注册到工厂
from gaia.core.parser import ParserFactory
ParserFactory.register('.mysql', MySQLSchemaParser)6.2 从Swagger/OpenAPI生成接口代码
# 解析OpenAPI文档
class OpenAPIParser(DataParser):
"""从OpenAPI文档生成接口定义"""
def parse(self, source: str):
import yaml
with open(source) as f:
spec = yaml.safe_load(f)
apis = []
for path, methods in spec.get('paths', {}).items():
for method, detail in methods.items():
apis.append({
'path': path,
'method': method.upper(),
'operation_id': detail.get('operationId'),
'summary': detail.get('summary'),
'parameters': detail.get('parameters', []),
'request_body': detail.get('requestBody'),
'responses': detail.get('responses'),
})
return apis6.3 集成到CI/CD流程
# .gitlab-ci.yml
generate-code:
stage: build
script:
- pip install gaia-generator
- gaia generate --config codegen/gaia.yaml
- git add .
- git commit -m "Auto-generated code" || true
- git push
only:
changes:
- codegen/data/**
- codegen/templates/**七、关于项目的后续
项目取名为Gaia,Gaia(盖亚)是希腊神话中的大地女神,子女为泰坦巨人,数量众多且强大,此处取其生生不息之意。
后续规划:
- Web界面:提供可视化操作界面,拖拖拽拽就能配置生成任务
- 在线服务:整合到"开发工具箱",无需本地安装
- AI辅助:结合LLM,根据自然语言描述生成模板和代码
- 模板市场:分享和复用社区模板
如果AI能根据一句"帮我生成一个用户管理的CRUD接口"就把代码写好,那才是真正的解放生产力!
八、总结
写代码生成器这件事,本身就是在用代码消灭代码。
笔者认为,若重复的代码写多了就会降低编写者的价值,而且这样编写者也有很大的痛苦。
代码生成器不是什么高深的技术,核心就是:数据 + 模板 = 代码。
但它带来的价值是巨大的:
- ⏱️ 省时间:200个接口从手写2周变成生成2分钟
- 🎯 少出错:机器不会漏改参数、复制错变量
- 📏 统一规范:所有生成的代码风格一致
- 🔄 易维护:改模板就能批量更新
当然,代码生成器也不是银弹。它适合的是那些结构相似、数量众多的代码。对于需要创造性思考的业务逻辑,还是得靠我们程序员的大脑。
最后送大家一句话:
能自动化的绝不手动,能偷懒的绝不勤快。
这不是懒,这是工程师思维。
参考资料
项目链接
- Gaia GitHub - 项目源码
- 在线Demo - 在线体验
技术文档
- Jinja2官方文档 - 模板引擎
- Click文档 - 命令行框架
- Django - Web框架参考
同类工具
- MyBatis Generator - Java ORM代码生成
- Yeoman - 前端脚手架工具
- Cookiecutter - Python项目模板