Skip to content

第10章 前端编译与优化 - 代码指南

一、代码结构

1.1 目录结构

src/main/java/com/linsir/abc/core/jvm/compile/
├── annotation/
│   └── AutoToString.java              # 自动生成toString的注解定义
├── processor/
│   └── AutoToStringProcessor.java     # 注解处理器实现
├── test/
│   ├── User.java                      # 测试类(排除敏感字段)
│   ├── Product.java                   # 测试类(包含父类字段)
│   └── BaseEntity.java                # 父类测试类
├── UserToStringImpl.java              # 生成的toString实现类
└── ProductToStringImpl.java           # 生成的toString实现类

src/test/java/com/linsir/abc/core/jvm/compile/
└── AnnotationProcessorTest.java       # 测试类

1.2 类结构说明

1.2.1 AutoToString.java

  • 位置: annotation/AutoToString.java
  • 类型: 注解(Annotation)
  • 作用: 标记需要自动生成toString方法的类
  • 属性:
    • includeSuper(): 是否包含父类字段,默认false
    • exclude(): 需要排除的字段名数组,默认空数组

1.2.2 AutoToStringProcessor.java

  • 位置: processor/AutoToStringProcessor.java
  • 类型: 注解处理器(AbstractProcessor)
  • 作用: 编译期间扫描@AutoToString注解,生成toString实现类
  • 核心方法:
    • init(): 初始化处理器,获取Messager和Filer
    • process(): 处理注解,遍历被注解的元素
    • generateToString(): 生成toString实现类
    • collectFields(): 收集类字段信息
    • writeGeneratedClass(): 写入生成的类文件

1.2.3 测试实体类

  • User.java: 基础测试类,演示排除敏感字段(password)
  • Product.java: 继承测试类,演示包含父类字段
  • BaseEntity.java: 父类,包含通用字段(id, createTime, updateTime)

1.2.4 生成的实现类

  • UserToStringImpl.java: User类的toString实现
  • ProductToStringImpl.java: Product类的toString实现

二、代码作用

2.1 整体作用

本代码实现了一个插入式注解处理器(Annotation Processor),演示了Java编译期代码生成技术。通过在类上添加@AutoToString注解,编译器会自动生成对应的toString()方法实现类。

2.2 各组件作用

2.2.1 注解定义(AutoToString)

java
@Target(ElementType.TYPE)           // 只能用于类
@Retention(RetentionPolicy.SOURCE)  // 只在源码阶段保留
public @interface AutoToString {
    boolean includeSuper() default false;  // 是否包含父类字段
    String[] exclude() default {};         // 排除的字段名
}

作用说明:

  • @Target(ElementType.TYPE): 限制注解只能用于类、接口、枚举
  • @Retention(RetentionPolicy.SOURCE): 注解只在源码阶段存在,编译后丢弃
  • includeSuper: 控制是否递归收集父类的字段
  • exclude: 允许排除敏感字段(如密码、密钥等)

2.2.2 注解处理器(AutoToStringProcessor)

处理流程:

编译开始

扫描@AutoToString注解

收集类字段信息(排除static和指定字段)

生成对应的ToStringImpl类

编译结束

核心功能:

  1. 字段收集: 遍历类的所有字段,过滤static字段和排除字段
  2. 父类支持: 递归收集父类字段(当includeSuper=true时)
  3. 代码生成: 使用JavaFileObject创建新的源文件
  4. 错误处理: 使用Messager输出编译期错误和警告

2.2.3 生成的toString实现

以User类为例,生成的UserToStringImpl提供静态方法:

java
public static String toString(User obj) {
    if (obj == null) {
        return "null";
    }
    StringBuilder sb = new StringBuilder();
    sb.append("User{");
    sb.append("id=").append(obj.getId());
    sb.append(", ").append("username=").append(obj.getUsername());
    sb.append(", ").append("age=").append(obj.getAge());
    sb.append('}');
    return sb.toString();
}

输出示例:

User{id=1, username=zhangsan, age=25}

2.3 技术要点

2.3.1 JSR 269标准

注解处理器基于**JSR 269(Pluggable Annotation Processing API)**标准:

  • 在编译期间执行,不修改原始源码
  • 可以读取、修改、添加抽象语法树(AST)元素
  • 支持多轮次处理(Round Processing)

2.3.2 处理器生命周期

  1. 初始化阶段: init(ProcessingEnvironment)

    • 获取Messager(消息报告器)
    • 获取Filer(文件创建器)
    • 获取ElementUtils(元素工具)
    • 获取TypeUtils(类型工具)
  2. 处理阶段: process(Set<? extends TypeElement>, RoundEnvironment)

    • 遍历被注解的元素
    • 生成辅助代码
    • 返回true表示已处理
  3. 结束阶段: 编译器继续后续编译流程

2.3.3 与Lombok的区别

特性本实现Lombok
实现方式生成辅助类直接修改AST
API使用标准JSR 269编译器内部API
侵入性低(不修改原类)高(修改原类)
兼容性依赖编译器版本

三、测试方法

3.1 测试类位置

src/test/java/com/linsir/abc/core/jvm/compile/AnnotationProcessorTest.java

3.2 测试用例说明

3.2.1 测试User类的toString生成

测试方法: testUserToStringGeneration()

测试内容:

  • 验证生成的toString方法存在且可调用
  • 验证非排除字段包含在输出中
  • 验证被排除的password字段不包含
  • 验证静态字段不包含

执行命令:

bash
cd linsir-abc-core
mvn test -Dtest=AnnotationProcessorTest#testUserToStringGeneration

3.2.2 测试Product类的toString生成(包含父类字段)

测试方法: testProductToStringWithSuperFields()

测试内容:

  • 验证当前类字段包含在输出中
  • 验证父类字段包含在输出中(includeSuper=true)
  • 验证被排除的costPrice字段不包含

执行命令:

bash
mvn test -Dtest=AnnotationProcessorTest#testProductToStringWithSuperFields

3.2.3 测试toString处理null对象

测试方法: testToStringWithNullObject()

测试内容:

  • 验证传入null时返回字符串"null"

3.2.4 测试toString输出格式

测试方法: testToStringFormat()

测试内容:

  • 验证格式:ClassName
  • 验证以类名开头
  • 验证以}结尾
  • 验证字段间有逗号和空格分隔

3.2.5 测试BaseEntity字段在子类toString中的包含

测试方法: testBaseEntityFieldsInSubclassToString()

测试内容:

  • 验证父类id字段包含
  • 验证父类createTime字段包含
  • 验证父类updateTime字段包含

3.2.6 测试User类基本功能

测试方法: testUserBasicFunctionality()

测试内容:

  • 验证getter方法
  • 验证setter方法
  • 验证静态方法

3.2.7 测试Product类基本功能

测试方法: testProductBasicFunctionality()

测试内容:

  • 验证继承的getter方法
  • 验证自身的getter方法

3.3 运行所有测试

bash
# 运行所有测试
mvn test -Dtest=AnnotationProcessorTest

# 或运行整个编译测试套件
mvn clean test

四、代码执行预期结果

4.1 测试执行结果

执行mvn test -Dtest=AnnotationProcessorTest预期输出:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.linsir.abc.core.jvm.compile.AnnotationProcessorTest
User toString输出: User{id=1, username=zhangsan, age=25}
Null对象toString输出: null
Product toString输出: Product{id=100, createTime=2026-03-28T15:54:58.474893100, updateTime=2026-03-28T15:54:58.474893100, name=iPhone 15, description=Apple iPhone 15, price=5999.00, stock=100}
包含父类字段的toString输出: Product{id=200, createTime=2024-01-01T10:00, updateTime=2024-01-02T15:30, name=MacBook Pro, description=Apple MacBook Pro, price=14999.00, stock=50}
格式验证输出: User{id=1, username=test, age=20}
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
[INFO] -------------------------------------------------------
[INFO] BUILD SUCCESS

4.2 各测试用例预期结果

4.2.1 User类toString测试

输入:

java
User user = new User(1L, "zhangsan", "secret123", 25);
String result = UserToStringImpl.toString(user);

预期输出:

User{id=1, username=zhangsan, age=25}

验证点:

  • ✅ 包含id字段
  • ✅ 包含username字段
  • ✅ 包含age字段
  • ✅ 不包含password字段(被exclude排除)
  • ✅ 不包含DEFAULT_ROLE(static字段)

4.2.2 Product类toString测试(包含父类)

输入:

java
Product product = new Product(
    100L, now, now,
    "iPhone 15", "Apple iPhone 15",
    new BigDecimal("5999.00"),
    new BigDecimal("4999.00"),
    100
);
String result = ProductToStringImpl.toString(product);

预期输出:

Product{id=100, createTime=2026-03-28T15:54:58.474893100, updateTime=2026-03-28T15:54:58.474893100, name=iPhone 15, description=Apple iPhone 15, price=5999.00, stock=100}

验证点:

  • ✅ 包含父类id字段
  • ✅ 包含父类createTime字段
  • ✅ 包含父类updateTime字段
  • ✅ 包含自身name、description、price、stock字段
  • ✅ 不包含costPrice字段(被exclude排除)

4.2.3 null对象处理测试

输入:

java
String result = UserToStringImpl.toString(null);

预期输出:

null

验证点:

  • ✅ 返回字符串"null",不抛出异常

4.3 编译期输出

在编译时,注解处理器会输出处理日志:

[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ linsir-abc-core ---
[INFO] 已为类 User 生成toString实现
[INFO] 已为类 Product 生成toString实现
[INFO] Compiling 7 source files to target/classes

4.4 生成的文件

编译后会在target/generated-sources/annotations目录下生成:

com/linsir/abc/core/jvm/compile/
├── UserToStringImpl.java
└── ProductToStringImpl.java

4.5 实际应用场景

场景1:日志输出

java
@AutoToString(exclude = {"password", "secretKey"})
public class User {
    private String username;
    private String password;
    private String secretKey;
}

// 使用
logger.info("用户信息: {}", UserToStringImpl.toString(user));
// 输出: 用户信息: User{username=admin}
// password和secretKey不会被记录到日志

场景2:调试信息

java
@AutoToString(includeSuper = true)
public class Order extends BaseEntity {
    private String orderNo;
    private BigDecimal amount;
}

// 使用
System.out.println(OrderToStringImpl.toString(order));
// 输出: Order{id=1001, createTime=2024-01-01T10:00, orderNo=ORD2024001, amount=999.99}

五、注意事项

5.1 使用限制

  1. SOURCE保留策略: 注解只在编译期存在,运行时无法通过反射获取
  2. 静态字段排除: 所有static字段自动排除,不参与toString生成
  3. 循环依赖: 如果类之间存在循环引用,可能导致StackOverflowError

5.2 性能考虑

  1. 编译期开销: 注解处理器会增加编译时间
  2. 运行时性能: 生成的代码使用StringBuilder,性能优于字符串拼接
  3. 内存占用: 生成的辅助类会增加类加载数量

5.3 扩展建议

  1. 支持方法引用: 可以扩展支持getter方法引用,处理计算属性
  2. 格式化选项: 可以添加格式化选项,支持JSON、XML等输出格式
  3. 缓存机制: 对于复杂对象,可以添加toString结果缓存

Released under the MIT License.