← 返回

编译期方案:用 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 倍。


两条路线对比

特性SerializedLambdaAPT
代表框架MyBatis PlusQueryDSL, MyBatis Flex
写法User::getAgeQUser.user.age
时机运行时编译期
首次解析性能 (JDK 26)~25 ns~1.6 ns
Lambda 表达式支持
IDE 自动补全✅(需要 apt 插件)
字段变更后不需要重新编译需要重新编译
运行时动态构建

下一篇:ASM 字节码分析和 Javac Plugin — 两种真正能解析 Lambda 表达式的方案,但代价是深入 JVM 底层。