Teyru

Lombok 兼容层

编译器内置的 Lombok 兼容层:注解在语义分析阶段展开成普通的 Teyru 成员,不需要 annotation processor。

Teyru 的编译器内置 Lombok 兼容层:注解(annotation)会被解析成带参数的语法树, 再于语义分析阶段展开成普通的 Teyru 成员,之后与手写代码走完全相同的 类型检查与代码生成路径。不需要 annotation processor,不需要 javac,也不会有 AST 注入。

import lombok.*

@Data
@AllArgsConstructor
@Builder
class Person {
  private String name
  private int age
}

Person p = Person.builder().name("ada").age(36).build()
System.out.println(p.getName())        // ada
System.out.println(p)                  // Person(name=ada, age=36)

使用方式:import lombok.X(或直接写 @lombok.X)。导入只是为了可读性, 编译器以注解的简单名称比对,所以 @Data、@lombok.Data、 @lombok.experimental.UtilityClass 都认得。


1. 支持状态总表

注解状态说明
@Getter✅ 完整含 AccessLevel(Lombok 的参数名是 value:位置形式与 value = AccessLevel.X 都读)、@Accessors 影响命名;lazy = true 在第一次读取时计算一次并缓存(原生类型也支持),双重检查,与 Lombok 一样线程安全
@Setter✅ 完整含 AccessLevel(Lombok 的参数名是 value:位置形式与 value = AccessLevel.X 都读)、@Accessors(chain)、@NonNull 字段的检查;名称已被占用时不生成(与 Lombok 相同)
@ToString✅ 完整of/exclude/callSuper/includeFieldNames/onlyExplicitlyIncluded(配合字段上的 @ToString.Include/@ToString.Exclude);callSuper 的 super 那一段写字段前面,与 Lombok 相同
@EqualsAndHashCode✅ 完整of/exclude/callSuper/onlyExplicitlyIncluded(@EqualsAndHashCode.Include/@EqualsAndHashCode.Exclude);hashCode 的常量(59/43/79/97)、字段顺序(原生类型,再 boxed 原生类型,再其余)与 canEqual 都与 Lombok 相同
@NoArgsConstructor✅ 完整staticName 会产生静态工厂;access(Lombok 的参数名就叫 access:位置形式与 access = AccessLevel.X 都读)
@RequiredArgsConstructor✅ 完整final(无初始值)与 @NonNull 字段
@AllArgsConstructor✅ 完整略过已有初始值的 final 字段
@Data⚠️ 部分getter + setter + @RequiredArgsConstructor + @ToString + @EqualsAndHashCode;隐式构造函数收 @NonNull 字段并在里面插入检查
@Value⚠️ 部分private final 字段、getter、全参构造函数、staticConstructor;继承会被 TY-TYP-0007 拦下
@Builder⚠️ 部分类、构造函数与方法;builderMethodName/buildMethodName/builderClassName/toBuilder/@Builder.Default/@Builder.ObtainVia/setterPrefix(首字母会大写:with 加 name 是 withName)
@NonNull⚠️ 部分字段与参数都检查:字段被收进生成的构造函数时插入检查,@Setter 生成的 setter 也检查,手写方法与构造函数的参数(只标在参数上即可)同样检查。直接赋值字段不检查(Lombok 也一样);@Builder 的检查位置与 Lombok 不同(见 §3)
@With⚠️ 部分字段上的 @With 生成 withX(T),以全参构造函数复制;写在类上不会为所有字段生成(Lombok 会)
@Accessors✅ 完整chain/fluent/prefix;fluent = true 会连带把 setter 变成可链式,写出来的 chain 仍然自己决定,静态字段的 setter 一律 void(都与 Lombok 相同)
@FieldDefaults✅ 完整level/makeFinal
@UtilityClass⚠️ 部分构造函数 private、成员 static;继承会被 TY-TYP-0007 拦下
@StandardException⚠️ 部分生成 4 个标准异常构造函数;E(Throwable) 用 cause.getMessage() 当消息。差异:全参构造函数是 super(message, cause),Lombok 是 super(message) 加 initCause(cause)
@Cleanup✅ 完整展开为 try-with-resources,任何退出路径都会 close
@SneakyThrows✅ 完整展开为 try/catch(Throwable) 后重新抛出
@Synchronized✅ 完整方法体包进 synchronized;静态方法用生成的 __lock$<类名> 字段
@Log 家族✅ 完整@Log/@Slf4j/@Log4j/@Log4j2/@CommonsLog/@JBossLog/@Flogger/@XSlf4j 都生成 private static final Logger log(见 §5)
@ExtensionMethod✅ 完整找不到方法时改写为 Ext.method(receiver, ...)
@FieldNameConstants⚠️ 部分生成嵌套的 Fields 类;prefix 是加在常量的值上,不是加在名称上,与 Lombok(1.18.4 以前)相反
@Delegate✅ 完整为字段类型的公开方法生成委托方法
@Helper✅ 完整方法内的局部类:生成实例,声明之后同名的非限定调用都走它(实例必须有无参构造函数)
@Tolerate✅ 完整被标的成员对生成器“不存在”:@Setter private Instant date 加上 @Tolerate public void setDate(String) 会同时有两个重载
@Locked✅ 完整用具名锁字段包住方法体
@NonFinal⚠️ 部分收得下注解,但没有任何作用:Lombok 用它让 @FieldDefaults(makeFinal = true)/@Value 放过一个字段,这里不读;标在类上时,继承检查在更早的阶段就已经跑过了
@PackagePrivate⚠️ 部分只有写在类上才有效,把该类字段与方法的访问修饰符拿掉;写在字段或方法上(Lombok 的用法:让 @FieldDefaults(level = …)/@Value 放过一个字段)没有作用
@Var✅ 完整已废弃的 Lombok 别名,无需生成任何东西
@SuperBuilder✅ 完整构造函数链上的所有字段都在同一个 builder;见 §4
@Singular✅ 完整逐项添加、批量添加、清除、build() 取得副本;@Singular("name") 可改名;见 §4
@Jacksonized❌ 不适用没有 Jackson,注解会被接受但不生成任何东西
@Builder.ObtainVia✅ 完整field/method/isStatic,由 toBuilder 读取——与 Lombok 相同,build() 读的是 builder 自己的字段
@onMethod_/@onParam_/@onConstructor_✅ 完整注解会被复制到生成的 getter/setter 参数/构造函数上(见 §3.5)
@CustomLog✅ 完整读取 lombok.config 的 lombok.log.custom.declaration(见 §3.5)

“完整”的定义:tests/programs/t16–t19、t54 有对应的测试,go test ./... 会验证输出; t55_lombok_every.teyru 在一支程序里把上表每一个 ✅ 的注解各用一次(只有 @CustomLog 与 @onX 家族不在里面,见下一句),输出逐行比对; t91_lombok_log.teyru 覆盖 @Log、@CustomLog(含 lombok.config)与 @onX 家族; t144_lombok_parity.teyru 覆盖 @NonNull 的各条路径、@Tolerate、 onlyExplicitlyIncluded、setterPrefix、方法上的 @Builder、@Builder.ObtainVia、 @Helper、@Getter(lazy = true) 与 @StandardException。 标 ⚠️ 的是“能接受注解、但行为与 Lombok 有差距”的行。

这张表是逐条编写程序、对照 Lombok 的文档和源码之后才标注的:@Builder.ObtainVia 以前在 build() 里被读取(Lombok 只在 toBuilder 读它),@Helper 与 @Tolerate 以前只是接收注解,@Getter(lazy = true) 的初始值会被计算两次且原生类型编译不过, @Data 的构造函数不收 @NonNull 字段,@Setter 生成的 setter 不插入检查, @Value/@UtilityClass 的 final 拦不住继承,@StandardException 的 E(Throwable) 不带消息——这些都已照 Lombok 的行为修掉,并且各有测试。


2. 生成的成员是什么样子

以 @Data class Person { private String name; private int age } 为例, 展开后等同于:

class Person {
  private String name
  private int age

  public Person() {
  }

  public String getName() {
    return this.name
  }
  public int getAge() {
    return this.age
  }
  public void setName(String value) {
    this.name = value
  }
  public void setAge(int value) {
    this.age = value
  }
  public String toString() {
    return "Person(name=" + this.name + ", age=" + this.age + ")"
  }
  public boolean equals(Object o) {
    if (this == o) {
      return true
    }
    if (o == null || !(o instanceof Person)) {
      return false
    }
    Person other = (Person) o
    if (!other.canEqual(this)) {
      return false
    }
    /* the members are read in Lombok's order: the int, then the String */
    if (this.age != other.age) {
      return false
    }
    Object $this$name = this.name
    Object $other$name = other.name
    if ($this$name == null ? $other$name != null : !$this$name.equals($other$name)) {
      return false
    }
    return true
  }
  protected boolean canEqual(Object other) {
    return other instanceof Person
  }
  public int hashCode() {
    int result = 1
    result = 59 * result + this.age
    Object $hash$name = this.name
    result = 59 * result + ($hash$name == null ? 43 : $hash$name.hashCode())
    return result
  }
}

差异说明:

  • @Getter/@Setter 必须与 @Data、@Value 或类级注解搭配才会覆盖所有字段; 写在单个字段上只影响该字段。
  • @Data 生成的构造函数是 @RequiredArgsConstructor(final 且无初始值的字段,加上 标了 @NonNull 的字段,与 Lombok 相同)。如果类没有这类字段,就是无参构造函数; 要全参构造函数请同时加上 @AllArgsConstructor。
  • @Builder 不会生成 getter,与 Lombok 相同。
  • @Getter(lazy = true) 把字段的初始值搬进 getter:构造函数不再计算它,第一次读取时 计算一次之后缓存(与 Lombok 相同)。持有的值是 boxed 的,所以原生类型也可以。生成的 getter 是双重检查的(与 Lombok 一样会同步),而且算出来是 null 也会被缓存—— 八条线程同时读一个睡着的 initializer 只会计算一次(t267_lombok_lazy_threads),返回 null 的 initializer 也只计算一次(t268_lombok_lazy_null);这两条以前是差异,PR #173 之后不是。
  • @EqualsAndHashCode 的 hashCode 用 Lombok 自己的常量(59、43、布尔的 79/97、long 与 double 的位移─异或)与 Lombok 的字段顺序,而且生成 canEqual(t270_lombok_equals,它的 .java.ref 是 Lombok 自己展开的代码、期望值 是 JDK 跑出来的)。equals 与 hashCode 一起生成或一起不生成:写了其中一个,另一个 就不会被生成(t271_lombok_equals_handwritten)。这些以前都是差异,PR #173 之后不是。
  • @ToString(callSuper = true) 生成的字符串是 Child(super=Base(b=1), c=2), 与 Lombok 相同(t269_lombok_tostring_callsuper 验了 super 那一段的四种形状); 以前这里写的是 Teyru 自创的 Child(c=2; super=Base(b=1)),PR #173 收敛掉了。 计算一次之后缓存(与 Lombok 相同)。持有的值是 boxed 的,所以原生类型也可以。getter 是双重检查的,与 Lombok 相同;Lombok 的持有字段是一个 AtomicReference,把“还没算” 与“算出来是 null”分开,并同步在那个 reference 上,这里的持有字段就是值本身(所以 null 不能拿来当“还没算”用),monitor 因此是实例本身,而“算过了”是一个 volatile 的标志——先写值、后写标志,读到标志就读得到值。
  • @StandardException 的 E(Throwable) 是 super(cause == null ? null : cause.getMessage(), cause),所以 new E(new RuntimeException("c")).getMessage() 是 c(与 Lombok 相同)。全参 构造函数走 super(message, cause);Lombok 是 super(message) 之后 initCause(cause),差别只在“先明确表示没有 cause、之后还能 initCause”这个细节。

3. 与 Lombok 的差异(重要)

  1. 没有 annotation processor。 展开发生在编译器内部,javac 完全不参与。
  2. @NonNull 检查的位置。 字段标了 @NonNull、又被收进生成的构造函数时会插入 检查,@Setter 生成的 setter、手写方法与构造函数的参数(只标在参数上即可)也都 会插入检查;直接赋值字段不检查——Lombok 的说明也只承诺“给这个字段生成的方法 赋值”会插检查,这点两边一致。差别在 @Builder:Lombok 在 builder 的 setter 上 就检查,builder().name(null) 当场抛出;这里的 builder setter 不检查,检查落在 build() 调用的构造函数里。
  3. @Singular 传的是可变副本,不是 Collections.unmodifiableList 包装(见 §4)。
  4. @SuperBuilder 生成一个扁平的 builder,不是 builder 继承链(见 §4)。
  5. @onX 注解只会被复制,不会被执行。 注解字面上会挂到生成出来的成员上, 但 Teyru 没有 java.lang.annotation 的运行时,所以 @Deprecated 之类的标记 不会有任何效果;需要反射读取注解的框架在这里不适用。
  6. 只读取 lombok.config 的一个键。 lombok.log.custom.declaration (供 @CustomLog 使用)会被读取;其余键与 config.stopBubbling 都不读取,搜索一律 走到文件系统根目录。
  7. @Value 的字段一定是 private final;若字段已经有初始值,构造函数不会再收它。 final 是在检查继承之后才由注解标上去的,所以 class Ext extends V 由一个 补做的检查拦下(TY-TYP-0007,消息与 Lombok 的 cannot inherit from final V 同义)。@UtilityClass 的 final 走同一条路。
  8. @Builder 的 setterPrefix 会把名字的首字母大写:setterPrefix = "with" 加字段 name 生成 withName(与 Lombok 相同);没有 prefix 时名字就是字段名本身。 挂在方法上的 @Builder 会把目标方法的参数当成字段,build() 调用该方法 (static 的用 <类>.<方法>(...),实例方法用一个新实例);@Builder.ObtainVia 由 toBuilder 读取,method/isStatic 两种形式都支持。
  9. @Helper 只认方法内的局部类。 这里会生成一个实例,声明之后同名的非限定 调用都走它;写在成员类上没有作用,只是把类标成 static(Lombok 会直接报错: @Helper is legal only on method-local classes)。 @Tolerate 则是让生成器“看不到”被标的成员:@Setter private Instant date 加上 @Tolerate public void setDate(String) 之后两个重载都在,与 Lombok 相同。

3.5 @onX 家族与 @CustomLog

@onMethod_/@onParam_/@onConstructor_

这些选项把一个注解复制到另一个注解生成出来的成员上:

class Annotated {
  @Getter(onMethod_ = @Deprecated) String name
  @Getter @Setter(onParam_ = @Deprecated) int age
}

@AllArgsConstructor(onConstructor_ = @Deprecated)
class Made {
  String a
  int b
}

onMethod_ 挂到 getter 上,onParam_ 挂到 setter 的参数上,onConstructor_ 挂到生成的构造函数上。两种写法都读:Lombok 的参数形式(含 javac7 时代的 @__(...) 包装)与紧挨在旁边的 @onMethod_Deprecated 裸写法。

只有单个注解能读回来:数组形式 onMethod_ = {@A, @B} 会被静默忽略,因为解析器 不会保留数组参数里的注解。

注解只是被复制,不会被执行——Teyru 没有 java.lang.annotation 的运行时, 所以标记本身没有作用,是给后续编译器阶段读取的。

@CustomLog

Lombok 只用 lombok.config 配置这一个注解。Teyru 读取 lombok.log.custom.declaration,格式与 Lombok 相同:

lombok.log.custom.declaration = MyLog MyLog.of(NAME)

第一个词是 logger 类型,后面是创建它的模板;NAME 会被替换成挂注解的类名, TYPE 在 Lombok 里是类对象。Teyru 不支持 TYPE:模板里的参数会传给一个 静态工厂,而 Teyru 的 X.class(见 docs/language.md)能表达的只有名称, 硬传会生成一个对不上工厂参数的东西,因此报告 TY-INT-0006 并要求改用 NAME。

搜索规则与 Lombok 相同:从源文件所在目录往上找最近的 lombok.config,每个键 以最近的一份为准。差别是 config.stopBubbling 不被读取,搜索一律走到根目录。

如果声明的类型找不到,报告 TY-INT-0006;找不到 log 符号时则是一般的 TY-TYP-0048。


4. @Singular 与 @SuperBuilder

@Singular

@Builder
class Order {
  @Singular private List<String> items
  @Singular private Map<String, Integer> counts
  @Singular("tag") private List<String> tags
}

生成(以 items 为例):

成员行为
addItems(E value)第一次调用时创建 ArrayList,之后逐项添加
addItemsAll(List<E> values)批量添加
clearItems()清空(下次添加会重新创建)
build()传入副本,且永远不是 null

Map 字段的添加方法用字段名本身:counts(K key, V value)、countsAll(Map<K,V>)、 clearCounts()。@Singular("tag") 会把添加方法改名为 tag(E)。

命名与 Lombok 不同:Lombok 对 List 字段 items 生成的是单数化的 item(E) 与 items(Collection),对 Map 字段 counts 生成的是 count(K,V) 与 counts(Map)。 Teyru 一律是 add<字段名>/add<字段名>All,Map 的双参数版本直接叫字段名。

与 Lombok 的差别:Lombok 生成 java.util.Collections.unmodifiableList 包装, Teyru 没有那个 API,所以传的是一份可变副本——对象与 builder 不共用同一个集合, 但拿到的人仍可修改它。

@SuperBuilder

@SuperBuilder
class Animal {
  private String name
  private int legs
}

@SuperBuilder
class Dog extends Animal {
  private String breed
}

Dog d = Dog.builder().name("rex").legs(4).breed("lab").build()

Lombok 用“builder 继承 builder,并以自引用的类型参数 B extends Builder<B> 返回 自身”来让链式调用跨层延续。Teyru 改成单一扁平的 builder:子类的 builder 覆盖 整条继承链的字段,build() 一次传给子类构造函数,构造函数再把父类那一份往上传。 链式写法完全一样,而且不需要泛型。

代价:Animal.builder() 与 Dog.builder() 是两个独立的类,Dog 的 builder 不是 Animal 的 builder 的子类。把 builder 当参数在继承链之间传递的代码 在 Lombok 可以编译,在这里不行——这种写法很少见。

5. 日志注解

Teyru 没有 SLF4J、Log4j 这些外部包,标准库提供一个简单的 Logger:

class Logger {
  public Logger(String name)
  public void trace(String msg)
  public void debug(String msg)
  public void info(String msg)
  public void warn(String msg)
  public void error(String msg)
}

@Slf4j 等注解生成的字段是 private static final Logger log = new Logger("<类名>"), 输出格式为 LEVEL <类名> - <信息>,写到标准输出。要接真正的日志系统,请自行把 log 字段换成对应的实现。


6. 展开顺序

  1. 类级的结构性注解(@Value、@FieldDefaults、@UtilityClass、@Data)先调整修饰符。
  2. 成员级注解(@Getter、@Setter、@NonNull、@With、@Delegate …)逐字段处理。
  3. 类级的生成器(@ToString、@EqualsAndHashCode、构造函数、@Builder)最后执行。
  4. 生成的成员在 vtable 分配之前加入,因此它们和手写成员一样参与重写与多态。

如果同一个签名已经存在(手写或生成的),构造函数会被跳过,方法则是 TY-TYP-0011 重复定义的编译错误;标了 @Tolerate 的成员例外——生成器把它当成 不存在,于是照样生成自己的那一份(Lombok 的行为:两个重载,或者真正的重复定义 错误)。@Setter/@Getter 生成的访问器另外会先看名字有没有被占用,占用了就不 生成,这也是 Lombok 的规则。

本页目录