从源码到抽象语法树

64 阅读4分钟

前言

当你使用Lombok的@Data注解时,是否曾好奇为什么编译器会自动生成getter/setter方法? 当你在IDE中编码时,是否曾疑惑它如何实时提示语法错误? 当静态检查工具报出代码问题时,是否想知道它如何"理解"你的代码?

这一切神奇功能的背后,都离不开一个共同的核心技术——AST(抽象语法树)

Lombok的实现解密

// 你写的代码
@Data
public class User {
    private String name;
    private int age;
}

// 编译后实际的效果(Lombok通过修改AST自动添加)
public class User {
    private String name;
    private int age;
    
    // 自动生成的方法
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    // ... 还有equals、hashCode、toString等方法

Lombok正是在编译过程中拦截了AST,在语法树层面为你"偷偷"添加了这些方法,然后编译器再基于修改后的AST生成字节码。这种"魔法"让我们的编码更高效,而理解AST就是理解这种魔法背后的科学。

什么是AST?

一个比喻

想象你要组装一个乐高模型:

  • 源代码:就像一堆散落的乐高积木
  • AST:就像乐高说明书,用树状结构告诉你如何组装
  • 编译器:就像按照说明书组装模型的你

正式定义

AST(Abstract Syntax Tree,抽象语法树) 是源代码语法结构的一种树状表示。它:

  • 抽象:忽略空格、注释、括号等细节
  • 语法:严格对应代码的语法结构
  • :层次化的节点结构

AST在Java编译过程中的位置

完整的Java编译流水线

要理解AST的重要性,我们需要先了解Java代码从.java文件到.class文件的完整旅程,也就是Java编译:

你编写源代码 (.java) 
→ 词法分析 (Lexical Analysis)	 // 字符流 → 标记流
→ 标记流 (Tokens) 				 // 标记流 → AST  
→ 语法分析 (Syntax Analysis)    // AST → 带语义信息的AST
→ 语义分析 (Semantic Analysis)	// 带语义信息的AST → 字节码
→ 字节码 (.class)

逐步详解每个阶段

词法分析 - 从字符到标记

将源代码的字符流分解成有意义的"单词"(标记)

// 源代码
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello AST!");
    }
}

// 词法分析后的标记流(简化版):
[public, class, Hello, {, public, static, void, main, (, String, [, ], args, ), 
 {, System, ., out, ., println, (, "Hello AST!", ), }, }]

关键特点

  • 识别关键字(publicclass等)
  • 识别标识符(Hellomainargs等)
  • 识别字面量("Hello AST!"
  • 识别运算符和分隔符({}()等)
  • 忽略:空格、换行、注释

语法分析 - 从标记到AST(AST诞生)

根据Java语法规则,将标记流组织成树状结构

// 基于上面的标记流,构建出这样的AST结构:
CompilationUnit
└── ClassDeclaration: Hello
    └── MethodDeclaration: main
        ├── Modifier: public
        ├── Modifier: static  
        ├── ReturnType: void
        ├── Parameter: String[] args
        └── BlockStmt
            └── ExpressionStmt
                └── MethodCallExpr
                    ├── Scope: System.out.println
                    └── Argument: "Hello AST!"

语法分析的关键任务

  • 检查语法是否正确(括号匹配、语句结构等)
  • 构建层次化的节点关系
  • 建立父子节点连接
  • 此时报错:语法错误(如缺少分号、括号不匹配)

语义分析 - 丰富AST的语义信息

为AST节点添加类型信息、进行符号解析等

// 语义分析前:
MethodCallExpr: System.out.println("Hello AST!")
├── Scope: System.out.println
└── Argument: "Hello AST!"

// 语义分析后(添加了类型信息和符号解析):
MethodCallExpr
├── Scope: System.out.println
│   ├── Type: PrintStream
│   └── Symbol: out字段引用
├── Argument: "Hello AST!"
│   └── Type: String
└── ReturnType: void

语义分析的主要工作

  • 类型检查:验证表达式类型是否兼容

  • 符号解析:将标识符关联到具体的声明

  • 访问权限检查:验证private、protected等访问控制

  • 常量折叠:在编译期计算常量表达式

    // 编译前
    int x = 10 + 20 * 3;
    
    // 常量折叠后
    int x = 70;  // 10 + 60 = 70
    

字节码生成 - 从AST到字节码

遍历AST,生成对应的JVM字节码指令

// AST节点:return a + b;
// 生成的字节码:
iload_1    // 加载变量a到操作数栈
iload_2    // 加载变量b到操作数栈  
iadd       // 执行加法运算
ireturn    // 返回结果

可视化编译过程实例

让我们通过一个具体例子来看整个流程:

// 源代码:SimpleDemo.java
public class SimpleDemo {
    public int calculate(int x, int y) {
        return x + y;
    }
}

编译过程分解

阶段输入输出关键动作
词法分析public class SimpleDemo { ... }[public, class, SimpleDemo, {, ... }]分词、去空白
语法分析标记流AST树结构构建语法树
语义分析原始AST带类型AST类型检查、符号解析
字节码生成丰富后的AST.class文件生成字节码指令

从开发者视角看编译过程

编码时

IDE实时进行词法+语法分析

public class User {
    // 当你输入到这里时,IDE已经在构建AST
    private String name;
    // ↑ 红色波浪线:语义分析发现类型错误
}

编译时

javac执行完整流程 词法分析 → 语法分析 → 语义分析 → 字节码生成

// 词法分析 → 语法分析 → 语义分析 → 字节码生成
$ javac User.java

工具处理时

注解处理器介入AST

@Data  // Lombok在语义分析后修改AST
public class User {
    private String name;
}

生命周期

AST 只在编译过程中存在,编译结束就销毁了