搜 索

自动化代码生成器编写

  • 244阅读
  • 2022年10月02日
  • 0评论
首页 / 编程 / 正文

前言:一个被重复代码折磨的程序员的觉醒

还记得那个阳光明媚的下午吗?

你刚入职银行项目组,领导拍着你的肩膀说:"小伙子,这个项目有200多个接口要对接,都是类似的,应该很快能写完。"

于是你信心满满地打开了接口文档,发现确实很类似——每个接口都需要写Request类、Response类、Service类、Controller方法...然后你算了一下:

200个接口 × 4个类 × 平均50行代码 = 40000行代码

你的眼前一黑。

更可怕的是,当你写到第50个接口的时候,产品经理走过来告诉你:"那个,接口规范改了,所有的Response要统一加一个timestamp字段。"

你看着已经写好的49个Response类,感觉自己的青春在流逝。

是时候做出改变了。

一、为什么要自己写代码生成器?

1.1 现有工具的局限性

"不是有MyBatis Generator吗?不是有各种脚手架吗?"

是的,但是:

flowchart LR subgraph EXISTING["现有代码生成工具"] A["MyBatis Generator"] B["JPA Generator"] C["各种IDE插件"] end subgraph LIMITS["局限性"] L1["只能生成特定类型代码"] L2["模板不够灵活"] L3["无法适配项目规范"] L4["不支持增量追加"] end EXISTING --> LIMITS LIMITS --> PAIN["你还是要手动改一堆东西 😭"] style PAIN fill:#FFB6C1

现有工具的问题在于:

痛点表现
场景单一只能生成数据库相关代码,接口对接、前端页面就无能为力了
模板死板生成的代码风格和项目规范不匹配,还得手动调整
不支持追加表结构改了,要么重新生成覆盖,要么手动改,都很痛苦
语言限制Java的生成器不能生成Python,Go的生成器不能生成Java

1.2 我们需要什么样的代码生成器?

flowchart TB subgraph IDEAL["理想中的代码生成器"] F1["🌍 支持多语言
Java/Python/Go/前端都行"] F2["📝 模板可定制
想生成啥样就啥样"] F3["➕ 支持追加
改了配置不用重写"] F4["🖥️ 操作友好
命令行+图形界面"] F5["🏠 本地运行
代码不外泄"] end F1 & F2 & F3 & F4 & F5 --> RESULT["一键生成,下班提前 🎉"] style RESULT fill:#90EE90

简单说就是:给我一个数据源,我告诉你要生成什么样的代码,你就给我批量生成,而且改了还能追加。

这不就是程序员应该干的事吗?把重复劳动自动化!

二、代码生成器的核心设计

2.1 整体架构

别被"代码生成器"这个名字吓到,本质上它就是一个模板引擎的应用

flowchart LR subgraph INPUT["输入"] A["📊 数据源
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 核心模块设计

flowchart TB subgraph GAIA["Gaia 代码生成器"] subgraph CORE["核心模块"] PARSER["📊 数据解析器
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.md

3.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_class

3.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_count

3.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-generator

5.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 apis

6.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(盖亚)是希腊神话中的大地女神,子女为泰坦巨人,数量众多且强大,此处取其生生不息之意。

flowchart LR subgraph NOW["当前版本 v1.0"] N1["✅ 命令行操作"] N2["✅ 多数据源支持"] N3["✅ 模板引擎"] N4["✅ 代码追加"] end subgraph FUTURE["规划中"] F1["🔮 Web界面"] F2["🔮 在线服务"] F3["🔮 AI辅助生成"] F4["🔮 模板市场"] end NOW --> FUTURE

后续规划

  1. Web界面:提供可视化操作界面,拖拖拽拽就能配置生成任务
  2. 在线服务:整合到"开发工具箱",无需本地安装
  3. AI辅助:结合LLM,根据自然语言描述生成模板和代码
  4. 模板市场:分享和复用社区模板
如果AI能根据一句"帮我生成一个用户管理的CRUD接口"就把代码写好,那才是真正的解放生产力!

八、总结

写代码生成器这件事,本身就是在用代码消灭代码。

flowchart LR A["重复劳动"] -->|"忍受"| B["加班到秃头"] A -->|"自动化"| C["提前下班"] style B fill:#FFB6C1 style C fill:#90EE90
笔者认为,若重复的代码写多了就会降低编写者的价值,而且这样编写者也有很大的痛苦。

代码生成器不是什么高深的技术,核心就是:数据 + 模板 = 代码

但它带来的价值是巨大的:

  1. ⏱️ 省时间:200个接口从手写2周变成生成2分钟
  2. 🎯 少出错:机器不会漏改参数、复制错变量
  3. 📏 统一规范:所有生成的代码风格一致
  4. 🔄 易维护:改模板就能批量更新

当然,代码生成器也不是银弹。它适合的是那些结构相似、数量众多的代码。对于需要创造性思考的业务逻辑,还是得靠我们程序员的大脑。

最后送大家一句话:

能自动化的绝不手动,能偷懒的绝不勤快。

这不是懒,这是工程师思维


参考资料

项目链接

技术文档

同类工具

评论区
暂无评论
avatar