本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:代码生成是提升开发效率和代码质量的重要手段,本文深入对比了Java正则表达式、Groovy语言和ANTLR三种主流代码生成方式。Java Regex适用于简单的字符串模板匹配与替换,适合基础文本处理;Groovy凭借其AST转换能力支持编译时元编程,可实现动态代码插入,适用于AOP等灵活场景;ANTLR作为专业的解析器生成工具,能够基于语法规则自动生成词法和语法分析器,适用于构建复杂语言处理系统。文章通过分析三者的技术特点与适用场景,帮助开发者根据项目需求选择最合适的代码生成方案。

代码生成的三重境界:从正则到AST,再到DSL

你有没有试过,在一个深夜加班时,盯着屏幕上那几百行几乎一模一样的 getter/setter 发呆?脑子里只有一个念头:“这玩意儿能不能自动写?”——别笑,每个 Java 程序员都曾经历过这种灵魂拷问。

而现实是,我们每天都在重复造轮子。字段多了要写访问器,接口变了要改方法签名,配置更新了还得手动同步枚举常量……这些任务机械、枯燥、极易出错。但偏偏它们又不可或缺。

于是,“代码生成”这个词开始频繁出现在架构会议中。可问题是, 到底什么是代码生成?它真的只是“把模板填上数据”那么简单吗?

如果你以为代码生成就是 String.format("private %s %s;", type, name) 这种操作,那你只看到了冰山一角。真正的代码生成技术,其实是一条从“文本替换”逐步走向“语义理解”的进化之路。它有三个层次:
👉 第一层:用 正则表达式 做字符串拼接;
👉 第二层:靠 Groovy AST 变换 在编译期注入逻辑;
👉 第三层:通过 ANTLR 构建 DSL 解析器 实现完整语言级别的控制。

这不仅是工具的选择问题,更是抽象层级的跃迁。就像你会用螺丝刀拧螺丝,但不会指望它能组装整台发动机一样——不同的场景需要不同精度的“生成武器”。


让我们先回到最原始的起点: 如何让机器帮我们写点简单的代码?

想象这样一个场景:产品同事丢过来一份 Excel 表格,里面列了 50 个用户属性( userName , age , email …),要求你马上建个 POJO 类。正常流程是你打开 IDE,一个一个敲字段,然后右键 Generate → Getter and Setter。但如果这个过程能自动化呢?

有人会说:“不就是模板填充嘛。”没错,但关键是怎么填。

最直接的想法是用占位符。比如定义一个模板:

private {{type}} {{fieldName}};

然后想办法把 {{type}} {{fieldName}} 替换成真实值。听起来很简单,对吧?但怎么替换?如果只是 .replace("{{type}}", "String") ,那遇到多个字段就傻眼了;要是 .replaceAll() 又容易误伤其他地方的双大括号。

这时候, 正则表达式 就派上用场了。

Java 的 java.util.regex.Pattern Matcher 提供了一套完整的文本匹配与替换机制。我们可以这样设计:

String template = "private {{type}} {{fieldName}};";
Pattern placeholder = Pattern.compile("\\{\\{([^}]+)}}");
Matcher matcher = placeholder.matcher(template);
StringBuffer result = new StringBuffer();

while (matcher.find()) {
    String key = matcher.group(1).trim();
    String value = context.get(key); // 从上下文中取值
    matcher.appendReplacement(result, Matcher.quoteReplacement(value));
}
matcher.appendTail(result);

看到没?这里有个小细节很多人忽略: appendReplacement() 内部会对 $ 字符做特殊处理(比如 $1 表示捕获组)。所以如果你的替换内容里含有 $ ,必须先用 Matcher.quoteReplacement() 转义,否则就会抛异常。

这就是为什么很多初学者写的模板引擎在遇到 Spring 配置 ${password} 这类字符串时直接崩溃的原因。

但这还只是第一步。真正有意思的是, 正则不仅能替换,还能提取结构信息再重组

比如说,给你一段方法名 setUserName(String userName) ,你能自动生成对应的 setter 方法体吗?

Pattern setterPattern = Pattern.compile("set(\\w+)\\((\\w+)\\s+(\\w+)\\)");
Matcher m = setterPattern.matcher("setUserName(String userName)");

if (m.matches()) {
    String prop = m.group(1).toLowerCase(); // userName
    String type = m.group(2);              // String
    String param = m.group(3);             // userName

    String methodBody = String.format(
        "public void set%s(%s %s) {\n" +
        "    this.%s = %s;\n" +
        "}",
        capitalize(prop), type, param, prop, param
    );
}

你看,我们并没有去解析整个 Java 文件语法,而是基于命名约定 + 正则提取,完成了“逆向工程式”的代码生成。这种技巧在脚本化构建阶段特别实用,比如 CI/CD 流水线中的自动化补全。

但等等,这种方法真的万无一失吗?

来试试这段代码:

List<String> tags;

你能写出一个正则准确提取它的类型和名字吗?

试试看:

(\w[<>\w]*)\s+(\w+)

好像可以匹配 List<String> tags 。但如果变成:

Map<Integer, List<String>> favoriteBooks;

你会发现,上面那个正则已经搞不定嵌套泛型了,因为它无法处理括号的层级结构。

更别说下面这种情况:

<div><span>Hello</span></div>

你想用正则判断标签是否正确闭合?抱歉, 有限状态机做不到这一点 。因为正则属于“正则文法”(Chomsky Type-3),而 HTML 是“上下文无关文法”(Type-2),前者没有记忆栈,无法计数或跟踪嵌套深度。

这就引出了正则的根本性局限: 它只能处理扁平结构,无法理解递归或嵌套语法

而且还有性能陷阱。考虑这个正则:

Pattern.compile("(a+)+$").matcher("aaaaaaaaaaaaab").matches()

看起来只是检查一串 a 后面有没有非 a 字符,但它会导致所谓的“灾难性回溯”——引擎会尝试所有可能的 a 组合方式,时间复杂度呈指数级增长,轻则卡顿,重则服务挂掉。

我在某次生产排查中就遇到过类似问题:一位同事为了“方便”,写了个通用参数校验正则 (.*?){0,10} 来匹配任意字段,结果当请求体稍大一点时,CPU 直接被打满。后来才发现,这种模糊量词加上回溯机制,简直是性能杀手。

所以结论很明确: 正则适合做轻量级、结构清晰的模板填充,但一旦涉及复杂语法或深层嵌套,就得换思路了

那下一步该往哪走?

答案是:进入编译期,操控 AST(Abstract Syntax Tree)。

你有没有用过 Lombok?那个让你不用写 getter/setter 的神器。它是怎么做到的?难道是在运行时反射生成方法?错。Lombok 是在 编译期修改语法树 ,直接把 toString() equals() 这些方法塞进你的 class 文件里。JVM 根本不知道这些方法不是你写的。

而 Groovy,作为 JVM 上最成熟的动态语言之一,早就内置了这套能力。

在 Groovy 中,你可以用 @ToString 注解一键生成 toString() 方法:

@ToString(includeNames = true, includes = 'name,age')
class Person {
    String name
    int age
}

编译后,它会变成:

public String toString() {
    return "Person(name:" + this.name + ", age:" + this.age + ")";
}

这一切发生在 .groovy 文件被转换成 .class 字节码的过程中。具体来说,Groovy 编译流程分为三步:

  1. Parsing :把源码拆成 Token,构造成原始语法树;
  2. AST Processing :将语法树标准化,并应用注册的 AST Transformation;
  3. Bytecode Generation :遍历最终 AST,输出 JVM 字节码。

重点就在第二步。只要你在类上加了 @ToString ,编译器就会激活对应的变换器,扫描字段,生成方法节点,然后插进类定义里。整个过程完全透明,运行时零开销。

这就好比你在盖房子的时候,建筑师偷偷多画了一个阳台,等你搬进去才发现:“咦,这不是我设计的,但我怎么这么喜欢?”

更酷的是,Groovy 允许你自己写 AST 变换器。比如想给所有实体类自动加上 serialVersionUID

class SerialVersionUIDAdder implements ASTTransformation {
    void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
        def classNode = nodes[1] as ClassNode
        def uidField = new FieldNode(
            'serialVersionUID',
            ACC_PRIVATE + ACC_STATIC + ACC_FINAL,
            ClassHelper.LONG_TYPE,
            classNode.module,
            new ConstantExpression(1L)
        )
        classNode.addField(uidField)
    }
}

只要把这个变换器注册为局部注解(Local Transformation),以后谁加上 @AutoSerial ,编译时就会自动补上序列化 ID。再也不用担心反序列化失败了。

不过,AST 变换也有两种模式:局部和全局。

  • 局部变换 :由注解触发,作用范围明确,像 @ToString @EqualsAndHashCode 都是典型例子;
  • 全局变换 :无需注解,通过 META-INF/services/org.codehaus.groovy.transform.ASTTransformation 自动加载,适用于统一埋点、日志注入等横切关注点。

举个真实案例:我们曾在一个微服务项目中使用全局变换,自动为所有 controller 方法添加耗时统计日志。不需要改任何业务代码,只要引入 jar 包,编译时就会悄悄插入 log.info("enter {} {}", method, startTime) log.info("exit {} {}ms", method, duration) 。上线后性能监控覆盖率瞬间拉满 😎。

但 AST 的前提是:你得有源码。也就是说,它只能在编译期干预已有类结构,不能凭空创建一门新语言。

如果你想做的不只是增强 Java,而是定义一套全新的配置语法呢?

比如让运营人员写这样的规则:

用户等级 >= 8 AND 登录频率 > 3次/周 → 推荐高价值商品

或者让算法工程师声明特征组合:

IF 年龄 < 18 THEN risk_level = 'high'
ELSE IF 收入 > 50000 THEN risk_level = 'low'

这时候,你就需要一个真正的 DSL(Domain-Specific Language)

而构建 DSL 最强大的工具之一,就是 ANTLR。

ANTLR 全称是 “Another Tool for Language Recognition”,但它干的事可不止识别。它可以帮你从零开始定义一门语言的词法和语法规则,然后自动生成对应的解析器代码。

比如你要做一个简单的算术计算器,可以这样写 .g4 文件:

grammar Expr;

expr: expr '*' expr
    | expr '+' expr
    | INT
    ;

INT : [0-9]+;
WS  : [ \t\r\n]+ -> skip;

执行命令:

antlr4 -Dlanguage=Java Expr.g4

就会生成 ExprLexer.java ExprParser.java ,接着你就可以用 Visitor 模式求值:

public class EvalVisitor extends ExprBaseVisitor<Integer> {
    @Override
    public Integer visitExprMul(ExprParser.ExprMulContext ctx) {
        return visit(ctx.expr(0)) * visit(ctx.expr(1));
    }

    @Override
    public Integer visitExprAdd(ExprParser.ExprAddContext ctx) {
        return visit(ctx.expr(0)) + visit(ctx.expr(1));
    }

    @Override
    public Integer visitExprInt(ExprParser.ExprIntContext ctx) {
        return Integer.parseInt(ctx.INT().getText());
    }
}

是不是有点像手写解释器的感觉?但 ANTLR 的强大之处在于,它能自动处理左递归、优先级、结合性等问题。比如上面的 expr '+' expr 是左递归,传统 LL 解析器会死循环,但 ANTLR v4 能自动优化成等价的右递归形式。

再来看个实际点的例子:假设你要做一个权限配置 DSL,支持如下语法:

role ADMIN {
    permission = "user:read", "user:write", "admin:*";
    maxSession = 5;
}

你可以这样定义语法规则:

config   : (roleDecl)+ ;
roleDecl : 'role' ID '{' property* '}' ;
property : 'permission' '=' STRING (',' STRING)* ';'
         | 'maxSession' '=' INT ';'
         ;

ID      : [a-zA-Z_][a-zA-Z0-9_]*;
STRING  : '"' (~["\r\n])* '"';
INT     : [0-9]+;
WS      : [ \t\r\n]+ -> skip;

然后在 Visitor 里构建对象模型:

@Override
public Role visitRoleDecl(RoleDeclContext ctx) {
    String roleName = ctx.ID().getText();
    Role role = new Role(roleName);

    for (PropertyContext p : ctx.property()) {
        if (p.permission != null) {
            for (TerminalNode str : p.STRING()) {
                role.addPermission(str.getText().replaceAll("\"", ""));
            }
        } else if (p.maxSession != null) {
            role.setMaxSession(Integer.parseInt(p.INT().getText()));
        }
    }

    return role;
}

这样一来,前端可以提供可视化编辑器,后端用 ANTLR 解析 DSL 并转换成内部策略对象。整个系统既灵活又安全,还不依赖硬编码。

当然,ANTLR 学习曲线较陡,尤其是错误恢复、符号表管理、类型推导这些高级功能需要深入掌握。但在真正复杂的配置场景下,它是唯一能扛住压力的选择。

说到这里,你应该已经看清了这条技术演进路线:

技术 层级 适用场景 是否理解语义
正则表达式 文本层 模板填充、简单替换 ❌ 字符匹配
Groovy AST 编译层 自动生成方法、字段增强 ✅ 结构感知
ANTLR DSL 语言层 规则引擎、配置语言 ✅ 完整语义

每一级都比前一级更复杂,但也更强大。

回到最初的问题: 什么时候该用哪种技术?

我总结了几条经验法则:

🟢 选正则,当你:
- 生成的是固定格式的代码片段;
- 输入结构简单、无嵌套;
- 对性能要求不高,且允许人工复查;
- 想快速做个 PoC 或脚本工具。

🔴 别用正则,当你:
- 处理 Java/C++ 等复杂语法;
- 需要解析嵌套结构(如 JSON、XML);
- 涉及类型校验或作用域分析;
- 有多人协作或长期维护需求。

🟡 上 Groovy AST,当你:
- 已有 Java/Groovy 项目,想减少样板代码;
- 需要在编译期统一增强某些类;
- 团队接受注解驱动的编程风格;
- 不介意引入 Groovy 依赖。

🛑 搬 ANTLR,当你:
- 需要定义一套独立的语言;
- 有非技术人员参与规则编写;
- 要实现表达式求值、条件判断等逻辑;
- 系统复杂度已超出模板引擎能力范围。

顺便提一句,这三个层次并不互斥。现实中往往是组合使用:

  • 用 ANTLR 解析 DSL,生成中间表示;
  • 用 Groovy AST 把中间表示编译成 Java 类;
  • 用正则做最后的格式美化或注释插入。

比如我们做过的一个报表引擎:用户用 DSL 定义指标计算逻辑 → ANTLR 解析成 AST → Groovy 动态生成计算类 → 正则调整缩进和空行 → 输出整洁的 .java 文件供编译。

整个过程全自动,开发效率提升了十倍不止 💪。

最后留个小思考题:你现在做的项目里,有哪些重复性代码是可以被生成的?是字段访问器?API 接口?还是数据库映射?

不妨试着画张图,看看它们的数据来源是什么。如果能抽象出一个稳定的输入模型,那恭喜你,已经离自动化不远了。

毕竟,程序员的价值不在“写代码”,而在“让代码自己写自己” 🤖✨

graph TD
    A[原始需求] --> B{结构是否扁平?}
    B -- 是 --> C[用正则做模板填充]
    B -- 否 --> D{是否基于Java语法?}
    D -- 是 --> E[用Groovy AST变换]
    D -- 否 --> F[用ANTLR构建DSL解析器]

    C --> G[输出Java代码片段]
    E --> G
    F --> G

    G --> H[编译进项目]
    H --> I[交付业务价值]

    style A fill:#f9f,stroke:#333
    style I fill:#bfb,stroke:#333

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:代码生成是提升开发效率和代码质量的重要手段,本文深入对比了Java正则表达式、Groovy语言和ANTLR三种主流代码生成方式。Java Regex适用于简单的字符串模板匹配与替换,适合基础文本处理;Groovy凭借其AST转换能力支持编译时元编程,可实现动态代码插入,适用于AOP等灵活场景;ANTLR作为专业的解析器生成工具,能够基于语法规则自动生成词法和语法分析器,适用于构建复杂语言处理系统。文章通过分析三者的技术特点与适用场景,帮助开发者根据项目需求选择最合适的代码生成方案。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐