编译期方案:用 SerializedLambda 和注解处理器提取属性名
📅 2026-06-10 | 🏷️ MyBatis Plus, QueryDSL, SerializedLambda, APT | 📖 阅读约 25 分钟
前言
上一篇说了 Java 的 Lambda 编译后丢失了 AST 信息,不能类似 C# 一样直接解析。但框架开发者们没有放弃,而是找到了其他办法。
这一篇讲两种编译期方案:它们不解析 Lambda 本身,而是利用 Java 的其他机制来提取属性信息。
- 方案 1: SerializedLambda — MyBatis Plus 用的方式,从方法引用中提取 getter 方法名
- 方案 2: Annotation Processor — QueryDSL 用的方式,编译期扫描实体字段,生成元模型类
方案 1: SerializedLambda(MyBatis Plus 路线)
发现过程
这个方案的关键发现是:当一个 Lambda 表达式实现了 Serializable 接口时,Java 编译器会额外生成一个 writeReplace() 方法。
这个方法返回一个 SerializedLambda 对象,里面包含了:
implClass— 实现方法所在的类名implMethodName— 实现方法的方法名implMethodSignature— 方法签名capturedArgCount— 捕获的外部变量数量
对于 User::getAge 这种方法引用,implMethodName 就是 "getAge",这就是我们要的属性信息。
定义 Serializable 的 Getter 接口
/**
* 类型安全的 getter 函数式接口。
*
* 关键设计:继承 Serializable。
*
* 不继承 Serializable 的话,编译器只会生成一个普通的 Lambda 实现类,
* 没有 writeReplace() 方法,运行时就拿不到 SerializedLambda。
*
* 继承 Serializable 之后,编译器会在实现类中额外生成:
* private Object writeReplace() {
* return new SerializedLambda(...);
* }
*
* 这个 SerializedLambda 包含了 Lambda 的所有元信息。
*/
@FunctionalInterface
public interface Getter<T, R> extends Serializable {
R get(T obj);
}从 Getter 提取属性名
/**
* 从 Getter 方法引用中提取属性名。
*
* 流程:
* 1. 通过反射调用 writeReplace(),获取 SerializedLambda
* 2. 从 SerializedLambda 中读取 implMethodName(getter 方法名)
* 3. 去掉 get/is 前缀,得到属性名
*
* 例如:
* User::getAge → SerializedLambda(implMethodName="getAge") → "age"
* User::isDeleted → SerializedLambda(implMethodName="isDeleted") → "deleted"
*/
public static <T, R> String propertyOf(Getter<T, R> getter) {
try {
// Step 1: 反射调用 writeReplace()
// getter 的运行时类型是编译器生成的匿名类,比如:
// LambdaTest$$Lambda$1/0x0000000123456789
// 这个类有 writeReplace() 方法,返回 SerializedLambda
Method writeReplace = getter.getClass()
.getDeclaredMethod("writeReplace");
writeReplace.setAccessible(true); // 私有方法,需要 setAccessible
// Step 2: 调用并强转
Object replacement = writeReplace.invoke(getter);
if (!(replacement instanceof SerializedLambda)) {
throw new IllegalArgumentException(
"writeReplace 返回的不是 SerializedLambda: "
+ replacement.getClass().getName());
}
SerializedLambda lambda = (SerializedLambda) replacement;
// Step 3: 读取方法名
String methodName = lambda.getImplMethodName();
// User::getAge → methodName = "getAge"
// User::getName → methodName = "getName"
// User::isDeleted → methodName = "isDeleted"
// Step 4: 去掉 get/is 前缀,得到属性名
return resolvePropertyName(methodName);
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException(
"无法从 " + getter.getClass().getName() + " 提取 SerializedLambda。"
+ "请确保 Getter 接口继承了 Serializable。", e);
}
}
/**
* getter 方法名 → 属性名。
*
* 规则(和 MyBatis Plus / Spring BeanUtils 一致):
* - "getAge" → 去掉 "get" 前缀,首字母小写 → "age"
* - "isDeleted" → 去掉 "is" 前缀 → "deleted"
* - "getUserName" → 去掉 "get" → "UserName" → 首字母小写 → "userName"
* - "getID" → 如果前两个字母都大写(全大写缩写),保留原样 → "ID"
*/
private static String resolvePropertyName(String methodName) {
if (methodName.startsWith("get") && methodName.length() > 3) {
String rest = methodName.substring(3);
// 如果属性名只有一个字母,直接小写
if (rest.length() == 1) {
return rest.toLowerCase();
}
// 如果前两个字母都大写(如 getID),保留原样
if (Character.isUpperCase(rest.charAt(0))
&& Character.isUpperCase(rest.charAt(1))) {
return rest;
}
// 常规情况:首字母小写 + 其余保留
return Character.toLowerCase(rest.charAt(0)) + rest.substring(1);
}
if (methodName.startsWith("is") && methodName.length() > 2) {
String rest = methodName.substring(2);
return Character.toLowerCase(rest.charAt(0)) + rest.substring(1);
}
return methodName;
}为什么普通 Lambda 不行
这里有一个坑:只有实现了 Serializable 的 Lambda 才有 writeReplace。
// ❌ 不行 — Predicate 没有继承 Serializable
Predicate<User> p = u -> u.getAge() > 18;
// 编译器生成的匿名类没有 writeReplace()
// 调用 getDeclaredMethod("writeReplace") 会抛 NoSuchMethodException
// ✅ 可以 — Getter 继承了 Serializable
Getter<User, Integer> getter = User::getAge;
// 编译器生成的匿名类有 writeReplace()
// 返回 SerializedLambda(implMethodName="getAge")限制
这个方案有一个核心性的限制:只能解析方法引用,不能解析 Lambda 表达式。
// ✅ 方法引用 — implMethodName = "getAge"
Getter<User, Integer> getter = User::getAge;
propertyOf(getter); // → "age"
// ❌ Lambda 表达式 — implMethodName = "lambda$main$0"
// 编译器把 Lambda 体编译成一个私有方法,方法名是自动生成的
// 而且这个方法里面可能有很多逻辑,不是简单的 getter 调用
SerializablePredicate<User> p = u -> u.getAge() > 18;
// 你拿到 "lambda$main$0" 这个名字有什么用呢?没法推断出 "age"方案 2: Annotation Processor(QueryDSL 路线)
核心思路
既然运行时拿不到 Lambda 的结构,那就在编译期就把实体类的字段信息提取出来。
QueryDSL 的做法是:用注解处理器扫描被 @Entity 或其他注解标注的类,自动生成一个"Q 类"(Query model class),里面包含了每个字段的元信息。
// 1. 定义实体类
@ExpressionEntity
public class User {
private String name;
private Integer age;
private Boolean deleted;
}
// 2. 编译期,注解处理器自动生成 QUser
// 这个文件你不需要手写,编译器自动生成
public final class QUser {
// 单例 — 全局唯一
public static final QUser user = new QUser();
// 每个字段对应一个 FieldPath 对象
// FieldPath 里存储了属性名和类型
public final IntegerField age = new IntegerField("age");
public final StringField name = new StringField("name");
public final BooleanField deleted = new BooleanField("deleted");
private QUser() {} // 私有构造器,强制用单例
}
// 3. 使用 — 直接读取字段引用
Expression expr = QUser.user.age.gt(18);
// QUser.user.age → IntegerField("age")
// .gt(18) → BinaryExpression(Property("age"), GT, Constant(18))注解处理器实现
注解处理器是 Java 编译器提供的一种扩展机制。你实现 AbstractProcessor 接口,注册到编译器,编译器在编译过程中会调用你的处理器。
/**
* 表达式实体注解处理器。
*
* 编译期扫描 @ExpressionEntity 标注的类,自动生成 Q 类。
*
* 工作流程:
* javac 编译 User.java(含 @ExpressionEntity)
* ↓
* process() 方法被调用
* ↓
* 遍历 User 的所有字段
* ↓
* 用 JavaPoet 生成 QUser.java 源码
* ↓
* javac 把 QUser.java 也编译了
* ↓
* 用户代码可以直接使用 QUser.user.age
*/
@SupportedAnnotationTypes("com.example.apt.ExpressionEntity")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class ExpressionEntityProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
// 遍历所有被 @ExpressionEntity 标注的类
for (Element element : roundEnv
.getElementsAnnotatedWith(ExpressionEntity.class)) {
// 确认是类(不是接口、枚举等)
if (element.getKind() != ElementKind.CLASS) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
"@ExpressionEntity 只能标注在类上",
element);
continue;
}
TypeElement typeElement = (TypeElement) element;
try {
generateQClass(typeElement);
} catch (IOException e) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
"生成 Q 类失败: " + e.getMessage(),
element);
}
}
return true; // 返回 true 表示"消费"了这些注解
}
/**
* 为实体类生成 Q 类源码。
*/
private void generateQClass(TypeElement entityClass) throws IOException {
String packageName = processingEnv.getElementUtils()
.getPackageOf(entityClass)
.getQualifiedName().toString();
String entityName = entityClass.getSimpleName().toString();
String qClassName = "Q" + entityName;
// 收集所有字段(包括父类的)
List<VariableElement> fields = getAllFields(entityClass);
// 用 JavaPoet 构建类
TypeSpec.Builder builder = TypeSpec.classBuilder(qClassName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
// 添加单例: public static final QUser user = new QUser();
ClassName selfClass = ClassName.get(packageName, qClassName);
builder.addField(FieldSpec.builder(
selfClass, "user",
Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL
).initializer("new $T()", selfClass).build());
// 为每个字段生成 FieldPath 实例
for (VariableElement field : fields) {
String fieldName = field.getSimpleName().toString();
String fieldType = mapToFieldPathType(field);
// 例如: public final IntegerField age = new IntegerField("age");
ClassName fieldClassName = ClassName.get(
"com.example.apt.field", fieldType);
builder.addField(FieldSpec.builder(
fieldClassName, fieldName,
Modifier.PUBLIC, Modifier.FINAL
).initializer("new $T($S)", fieldClassName, fieldName)
.build());
}
// 写入源文件
JavaFile javaFile = JavaFile.builder(
packageName, builder.build()).build();
javaFile.writeTo(processingEnv.getFiler());
}
/**
* 收集实体类的所有字段(递归包括父类)。
*/
private List<VariableElement> getAllFields(TypeElement typeElement) {
List<VariableElement> fields = new ArrayList<>();
// 先递归收集父类字段
TypeMirror superclass = typeElement.getSuperclass();
if (superclass.getKind() != TypeKind.NONE) {
Element superElement = processingEnv.getTypeUtils()
.asElement(superclass);
if (superElement instanceof TypeElement superType) {
fields.addAll(getAllFields(superType));
}
}
// 收集当前类的字段(排除 static 和 transient)
for (VariableElement field : ElementFilter.fieldsIn(
typeElement.getEnclosedElements())) {
Set<Modifier> modifiers = field.getModifiers();
if (!modifiers.contains(Modifier.STATIC)
&& !modifiers.contains(Modifier.TRANSIENT)) {
fields.add(field);
}
}
return fields;
}
/**
* Java 类型 → FieldPath 子类名。
* String → StringField, Integer → IntegerField, ...
*/
private String mapToFieldPathType(VariableElement field) {
String typeName = field.asType().toString();
return switch (typeName) {
case "java.lang.String", "String" -> "StringField";
case "java.lang.Integer", "int" -> "IntegerField";
case "java.lang.Long", "long" -> "LongField";
case "java.lang.Boolean", "boolean" -> "BooleanField";
default -> "StringField"; // 未知类型降级为 StringField
};
}
}FieldPath 类型体系
Q 类中的每个字段都是一个 FieldPath 对象。它存储了属性名和类型,并提供了链式比较方法:
/**
* 字段路径基类。
*
* 每个 FieldPath 持有一个属性名,提供比较方法,返回 Expression。
* 这就是 QueryDSL / MyBatis Flex 的核心 API 设计。
*/
public abstract class FieldPath<T> {
private final String propertyName;
protected FieldPath(String propertyName) {
this.propertyName = propertyName;
}
/**
* 等于比较。
* QUser.user.name.eq("Tom")
* → BinaryExpression(Property("name"), EQ, Constant("Tom"))
*/
public Expression eq(T value) {
return new BinaryExpression(
new PropertyExpression(propertyName, getType()), // 左边:属性
Operator.EQ, // 运算符
new ConstantExpression(value, getType()) // 右边:常量
);
}
/**
* 大于比较。
* QUser.user.age.gt(18)
* → BinaryExpression(Property("age"), GT, Constant(18))
*/
public Expression gt(T value) {
return new BinaryExpression(
new PropertyExpression(propertyName, getType()),
Operator.GT,
new ConstantExpression(value, getType())
);
}
/**
* IN 查询。
* QUser.user.age.in(18, 19, 20)
* → InExpression(Property("age"), [18, 19, 20])
*/
@SafeVarargs
public final Expression in(T... values) {
List<Expression> exprs = new ArrayList<>(values.length);
for (T v : values) {
exprs.add(new ConstantExpression(v, getType()));
}
return new InExpression(
new PropertyExpression(propertyName, getType()),
exprs
);
}
/**
* LIKE 查询(仅 String 类型)。
* QUser.user.name.like("%Tom%")
* → LikeExpression(Property("name"), "%Tom%")
*/
public Expression like(String pattern) {
return new LikeExpression(
new PropertyExpression(propertyName, getType()),
pattern
);
}
protected abstract Class<T> getType();
}
// 具体类型 — getType() 返回对应的 Java 类型
public class IntegerField extends FieldPath<Integer> {
public IntegerField(String name) { super(name); }
@Override protected Class<Integer> getType() { return Integer.class; }
}
public class StringField extends FieldPath<String> {
public StringField(String name) { super(name); }
@Override protected Class<String> getType() { return String.class; }
}性能
APT 是五条路线中速度高的,因为它在编译期就完成了所有工作,运行时只是读取字段引用:
// 运行时的开销 = 读取一个 static final 字段
// QUser.user.age → 直接读取预生成的 IntegerField 对象
// 没有反射,没有字节码解析,没有正则匹配JMH 基准测试结果:~1.6 ns/op,比 Method Reference 快 15 倍。
两条路线对比
| 特性 | SerializedLambda | APT |
|---|---|---|
| 代表框架 | MyBatis Plus | QueryDSL, MyBatis Flex |
| 写法 | User::getAge | QUser.user.age |
| 时机 | 运行时 | 编译期 |
| 首次解析性能 (JDK 26) | ~25 ns | ~1.6 ns |
| Lambda 表达式支持 | ❌ | ❌ |
| IDE 自动补全 | ✅ | ✅(需要 apt 插件) |
| 字段变更后 | 不需要重新编译 | 需要重新编译 |
| 运行时动态构建 | ❌ | ❌ |
下一篇:ASM 字节码分析和 Javac Plugin — 两种真正能解析 Lambda 表达式的方案,但代价是深入 JVM 底层。