Java 基础体系 · 第 10/100 篇。示例统一以 Java 25 LTS 为语言和 JVM 基线;框架示例使用与其兼容的现代稳定版本。

Java 反射与注解:Class、MethodHandle、元注解、处理器和边界

反射与注解经常同时出现在框架代码中,但它们解决的是不同问题:

  • Class 描述运行时可见的类、接口、数组和基本类型;
  • 反射 API 根据 Class 查找字段、方法、构造器,并在运行时访问它们;
  • MethodHandle 提供另一套更接近 JVM 调用模型的动态调用机制;
  • 注解是附着在声明或类型使用位置上的结构化元数据;
  • 元注解决定注解本身可以放在哪里、保存到什么时候、是否可继承;
  • 注解处理器在编译期读取注解并生成诊断信息或新源代码;
  • “边界”则包括类型擦除、访问控制、模块封装、类加载器、编译期与运行期的边界。

这些概念可以组合成 IoC、ORM、序列化、路由、校验和 AOP 等框架能力,但组合之前必须先区分它们各自处于哪个阶段、拥有哪种权限,以及失败时由谁报告错误。


一、先区分四个时间点

Java 程序中的类型信息至少经历四个阶段:

  1. 源代码阶段:编译器能看到泛型参数、注解源码位置和类型结构。
  2. 编译阶段:注解处理器通过 javax.lang.model 读取源码模型。
  3. 类文件阶段:编译器把部分信息写入 class 文件,例如运行时注解、泛型签名、方法参数名等。
  4. 运行阶段:类加载器定义类,Class、反射和 MethodHandle 查询实际运行时结构。

一个信息是否存在,取决于它是否被保存,以及当前 API 是否能读取它。例如:

List<String> names = new ArrayList<>();

运行时通常只能知道变量使用了 List,不能从对象实例可靠地得知它是 List<String>。这是类型擦除造成的限制。

而下面的字段声明:

class Config {
    List<String> names;
}

编译器可以把 List<String> 写入字段的泛型签名,因此反射可以通过 Field.getGenericType() 读取一个 ParameterizedType。这不是运行时对象保留了 String,而是 class 文件额外保存了声明签名。

因此,下面三件事不能混为一谈:

信息 典型读取方式 是否一定存在
运行时类及其成员 Class、反射、MethodHandle 类定义中存在
泛型声明签名 TypegetGenericType() 编译器写入且未被工具移除时
注解 getAnnotation() 或注解处理器 取决于 RetentionPolicy 和处理阶段

二、Class<T>:运行时类型的入口

2.1 Class<T> 表示什么

Class<T> 是一个运行时对象,表示某个类型。它可以表示:

  • 普通类;
  • 接口;
  • 枚举;
  • 注解接口;
  • 数组类型;
  • 基本类型;
  • void

例如:

Class<String> stringClass = String.class;
Class<int[]> arrayClass = int[].class;
Class<Integer> primitiveWrapper = int.class; // 实际类型是 Class<Integer>
Class<Void> voidClass = void.class;

int.class 的类型在 Java 源代码层面可视为 Class<Integer>,但它表示的是基本类型 int,不是包装类型 Integer

System.out.println(int.class == Integer.class); // false
System.out.println(int.class.getName());        // int
System.out.println(Integer.class.getName());    // java.lang.Integer

基本类型没有对象实例,也没有可通过普通反射访问的成员方法。对 int.class.getDeclaredMethods() 查询结果为空,不表示 int 继承了某些方法,而是因为 int 不是类实例。

2.2 获取 Class 的几种方式

类字面量

Class<String> c1 = String.class;

这是编译期确定的类型引用。加载类和初始化类是不同概念。根据 Java 语言规范,使用类字面量获取 Class 通常不会因此初始化被引用的类。

对象实例

Object value = "hello";
Class<?> c2 = value.getClass();

getClass() 返回对象的实际运行时类,而不是变量声明类型:

CharSequence value = new StringBuilder("hello");
System.out.println(value.getClass()); // class java.lang.StringBuilder

这里 CharSequence 是静态类型,StringBuilder 是运行时类型。

按名称加载

Class<?> c3 = Class.forName("java.lang.String");

Class.forName(String) 会使用调用者关联的类加载器查找类,并初始化该类。需要避免初始化时,可以使用重载版本:

Class<?> c4 = Class.forName(
        "com.example.Plugin",
        false,
        Thread.currentThread().getContextClassLoader()
);

ClassLoader.loadClass(String) 通常只负责加载和链接,不主动初始化目标类:

Class<?> c5 = Thread.currentThread()
        .getContextClassLoader()
        .loadClass("com.example.Plugin");

“加载”“链接”“初始化”是 JVM 生命周期中的不同阶段。框架如果只是扫描类型,不希望执行静态初始化代码,就不能无条件使用会触发初始化的路径。

2.3 Class 的类型参数不是运行时类型检查

下面的代码使用了无界通配符:

Class<?> type = String.class;

Class<?> 表示“某个未知类型的 Class”。它可以安全地保存任意类型的类对象,但不能直接把任意对象传给 type.cast 之外的泛型操作。

Object value = type.cast("abc"); // 运行时检查 value 是否是 String

Class.cast 的作用类似于:

String value = (String) object;

但它使用 Class 对象中的运行时类型进行检查:

static <T> T cast(Class<T> type, Object value) {
    return type.cast(value);
}

调用:

String s = cast(String.class, "hello"); // 编译器推导 T 为 String

如果对象不匹配,会抛出 ClassCastException

Class<T> 的泛型参数主要帮助编译器检查 API 使用方式,并不能让 JVM 在运行时保留被擦除的泛型参数:

Class<List> raw = List.class;
// 不存在 List<String>.class

Java 没有 List<String>.class 这种类字面量,因为 List<String> 不是一个独立的运行时类。


三、从 Class 查找成员:声明边界与继承边界

反射 API 中最容易误用的一组区别是 getMethodgetDeclaredMethod

class Parent {
    public void inherited() {}
}

class Child extends Parent {
    private void own() {}
    public void ownPublic() {}
}

查找行为如下:

Child.class.getMethod("inherited");       // 能找到继承的 public 方法
Child.class.getDeclaredMethod("own");     // 能找到 Child 自身声明的 private 方法
Child.class.getMethod("own");             // NoSuchMethodException

基本规则:

  • getMethod 只查找 public 方法,并沿继承体系查找;
  • getDeclaredMethod 只查找当前 Class 自身声明的方法,不自动查找父类;
  • getFieldsgetDeclaredFieldsgetConstructorsgetDeclaredConstructors 具有类似区别;
  • getDeclaredMethods 返回当前类声明的方法,包括不同访问级别,但不保证数组顺序;
  • 编译器或编译后工具可能生成桥接方法、合成方法,不能假设返回结果只有源代码中写出的方法。

例如泛型重写可能生成桥接方法:

class Parent<T> {
    T get() {
        return null;
    }
}

class Child extends Parent<String> {
    @Override
    String get() {
        return "child";
    }
}

由于类型擦除,JVM 方法描述符需要兼容父类的 Object get(),编译器可能生成一个桥接方法,把 Object get() 转发到 String get()。反射扫描时可能看到 Method.isBridge()Method.isSynthetic()true 的方法。

因此框架按方法名扫描时,不能简单地认为:

Arrays.stream(type.getDeclaredMethods())
      .filter(m -> m.getName().equals("get"))

所得结果就等于源代码中的一个 get。应根据框架语义决定是否过滤桥接方法和合成方法,同时处理重载。

3.1 反射调用的完整路径

下面是一个可以运行的例子:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectionDemo {
    static class Calculator {
        public int add(int left, int right) {
            return left + right;
        }
    }

    public static void main(String[] args)
            throws NoSuchMethodException, InvocationTargetException,
                   InstantiationException, IllegalAccessException {

        Class<Calculator> type = Calculator.class;

        Calculator calculator = type.getDeclaredConstructor().newInstance();

        Method method = type.getMethod("add", int.class, int.class);

        Object result = method.invoke(calculator, 2, 3);

        System.out.println(result); // 5
    }
}

执行过程是:

  1. Calculator.class 得到运行时类型描述;
  2. getDeclaredConstructor() 查找无参构造器;
  3. newInstance() 创建对象;
  4. getMethod() 查找 public int add(int, int)
  5. invoke() 检查目标对象、参数数量和参数可转换性;
  6. 返回值统一以 Object 形式暴露,因此基本类型 int 会发生装箱;
  7. 目标方法抛出的异常通常通过 InvocationTargetException 包装。

如果目标方法本身抛出异常,诊断代码通常需要解包:

try {
    method.invoke(calculator, 2, 3);
} catch (InvocationTargetException e) {
    Throwable targetFailure = e.getCause();
    targetFailure.printStackTrace();
}

InvocationTargetException 表示“反射调用过程已经找到并调用了目标方法,但目标方法内部失败”。它不同于:

  • NoSuchMethodException:查找阶段没有找到匹配方法;
  • IllegalAccessException:访问权限不允许;
  • IllegalArgumentException:目标对象或参数不匹配;
  • ExceptionInInitializerError:类初始化失败等初始化问题。

四、访问控制:setAccessible 不是万能的越权开关

对于非公开成员,传统反射可能出现:

Method method = type.getDeclaredMethod("privateMethod");
method.setAccessible(true);

setAccessible(true) 的含义是请求抑制 Java 语言访问检查,但它受到模块系统和运行时封装约束。它不等于“无条件访问任意类的私有成员”。

Java 模块系统把访问分成两类重要边界:

  • **导出(exports)**主要控制其他模块对公开类型的编译期和运行期访问;
  • **开放(opens)**允许深度反射访问包中的非公开成员。

一个包可以导出给普通代码,但没有开放给深度反射;也可以不导出,却通过 opens 允许反射框架进行深度访问。

当模块不允许深度反射时,常见失败是 InaccessibleObjectException。解决方案不是在业务代码中无限重试,而是明确模块边界,例如:

--add-opens my.module/com.example.internal=framework.module

这类启动参数会削弱封装,适合迁移、诊断或明确授权的场景,不应被当成普遍的库设计方式。

MethodHandles.privateLookupIn 也要求调用者拥有相应模块权限;它不是绕过模块封装的替代方案。


五、MethodHandle:带有精确方法类型的动态调用

5.1 它与反射方法调用有什么不同

java.lang.reflect.Method 是对方法元数据和反射调用的抽象。java.lang.invoke.MethodHandle 是一个可组合、带有明确 MethodType 的调用点。

一个 MethodHandle 有固定的方法类型:

返回类型 (参数类型1, 参数类型2, ...)

例如:

(int, int)int

表示接收两个 int,返回一个 int

方法句柄通过 MethodHandles.Lookup 获取。Lookup 代表调用者的访问权限,而不是一个全局超级权限对象。

5.2 查找并调用实例方法

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class MethodHandleDemo {
    static class Calculator {
        public int add(int left, int right) {
            return left + right;
        }
    }

    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();

        MethodHandle add = lookup.findVirtual(
                Calculator.class,
                "add",
                MethodType.methodType(int.class, int.class, int.class)
        );

        Calculator calculator = new Calculator();

        int result = (int) add.invokeExact(calculator, 2, 3);

        System.out.println(result); // 5
        System.out.println(add.type());
        // (Calculator,int,int)int
    }
}

findVirtualMethodType 不包含接收者对象,但得到的句柄类型会把接收者作为第一个参数:

(Calculator, int, int)int

调用时必须把 calculator 作为第一个实际参数。

5.3 invokeExact 的关键规则

invokeExact 是签名多态方法。调用点的静态方法类型必须与句柄类型完全一致,包括:

  • 参数数量;
  • 参数类型;
  • 返回类型;
  • 基本类型与引用类型的区别。

下面的代码可能失败:

Object result = add.invokeExact(calculator, 2, 3);

原因不是结果不能装箱,而是调用点被编译成:

(Calculator,int,int)Object

而句柄类型是:

(Calculator,int,int)int

两者不完全相同。

正确写法是:

int result = (int) add.invokeExact(calculator, 2, 3);

如果希望使用更宽松的转换,可以使用:

Object result = add.invoke(calculator, 2, 3);

invoke 允许符合方法句柄转换规则的适配,例如装箱、拆箱和引用类型转换,但它仍然不是任意动态转换。需要明确控制签名时,可以先使用:

MethodHandle generic = add.asType(
        MethodType.methodType(Object.class, Object.class, Object.class, Object.class)
);

不过这要求适配过程在运行时合法。调用句柄前,可以用:

System.out.println(add.type());

验证实际签名,而不是凭源代码直觉猜测。

5.4 静态方法、特殊调用与反射适配

静态方法使用:

MethodHandle handle = lookup.findStatic(
        Utility.class,
        "parse",
        MethodType.methodType(int.class, String.class)
);

父类方法或特定实现调用可能涉及:

lookup.findSpecial(
        Parent.class,
        "method",
        MethodType.methodType(void.class),
        Child.class
);

findSpecial 的调用者和访问上下文要求更严格,不能用它随意调用任意私有或父类实现。

已有反射对象时,可以适配为方法句柄:

Method method = Calculator.class.getMethod(
        "add", int.class, int.class
);

MethodHandle handle = MethodHandles.lookup().unreflect(method);

unreflect 会根据 Method 的访问权限和当前 Lookup 检查是否允许访问。它不会自动消除模块系统的限制。

5.5 绑定接收者和组合

方法句柄可以绑定参数:

MethodHandle bound = add.bindTo(new Calculator());

int result = (int) bound.invokeExact(2, 3);

绑定后类型从:

(Calculator,int,int)int

变为:

(int,int)int

还可以通过 insertArgumentsdropArgumentsfilterArgumentsfilterReturnValueguardWithTest 等方法进行组合。这类组合是方法句柄区别于普通反射调用的重要能力:方法句柄可以先构造调用路径,再反复执行该路径。

需要区分规范保证和性能经验:

  • MethodHandle 的类型检查、适配规则和访问规则由 API 规范定义;
  • 某些 JVM 可能对稳定的方法句柄调用链进行优化;
  • 不能把“可能被 JIT 优化”写成固定性能承诺;
  • 动态生成大量不同签名或不同组合的句柄,仍可能增加编译、缓存和内存压力。

六、注解:附着在声明或类型使用位置的元数据

注解类型本质上是一种特殊接口:

public @interface Endpoint {
    String path();
    String method() default "GET";
}

使用时:

@Endpoint(path = "/users")
public void listUsers() {
}

注解元素必须是受限类型,包括:

  • 基本类型;
  • String
  • Class 或其数组;
  • 枚举;
  • 注解;
  • 上述类型的数组。

不能把任意对象、集合或动态计算结果放进注解:

// 非法
// String[] values() default new String[0]; // 默认值语法也不允许这样写
// List<String> values();

数组默认值的正确写法是:

String[] tags() default {};

注解元素如果没有默认值,使用注解时必须提供:

public @interface Required {
    String value();
}
@Required("production")
class Service {
}

注解实例由编译器或运行时反射代理提供,不应使用 new Required()。其 equalshashCodetoString 由注解契约定义,数组元素按内容比较,而不是简单按数组对象引用比较。


七、元注解:决定注解的语义边界

元注解是“用于声明注解类型的注解”。最核心的元注解包括 @Target@Retention@Documented@Inherited@Repeatable

7.1 @Target:允许放在哪里

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Endpoint {
    String path();
}

ElementType 包括类、接口、方法、字段、构造器、参数、局部变量、模块、包、类型参数和类型使用等位置。

声明位置与类型使用位置不同:

class Box<@Marker T> {
    @Marker
    String field;

    List<@Marker String> values;
}

若要标记 List<String> 中的 String,必须使用 ElementType.TYPE_USE;仅声明 FIELDTYPE_PARAMETER 不足以覆盖这个位置。

类型使用注解可以通过反射中的 AnnotatedType 系列 API 读取,例如:

Field field = Example.class.getDeclaredField("values");
AnnotatedType type = field.getAnnotatedType();

这与 Field.getAnnotation(...) 读取字段声明上的注解不是同一条路径。

7.2 @Retention:保存到什么时候

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Endpoint {
    String path();
}

三种保留策略:

  • SOURCE:只在源代码中存在,编译后通常不可见;
  • CLASS:写入 class 文件,但运行时反射不一定可见;
  • RUNTIME:写入 class 文件,并允许运行时反射读取。

如果一个注解没有声明 @Retention,默认策略是 CLASS。因此下面的注解不能依赖运行时反射读取:

public @interface CompileOnly {
}

注解处理器工作在编译阶段,能够看到适合当前处理轮次的源模型;它不要求注解使用 RUNTIMERUNTIME 是运行时反射需求,不是编译器处理需求。

7.3 @Documented:是否进入 API 文档

@Documented
public @interface PublicApi {
}

@Documented 只影响标准 Javadoc 是否把该注解作为被注解元素的一部分展示。它不改变运行时可见性,也不改变继承规则。

7.4 @Inherited:只对类注解有效

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
}
@Component
class Base {
}

class Child extends Base {
}

对类使用 getAnnotation(Component.class) 时,子类可能继承父类的类注解。

@Inherited 有严格边界:

  • 只作用于类;
  • 不作用于接口实现;
  • 不作用于方法;
  • 不作用于字段;
  • 不作用于构造器;
  • 不会让覆盖方法自动继承父方法注解。

因此下面的假设是错误的:

interface Audited {
    @Audit
    void save();
}

class Service implements Audited {
    // Service.save() 不会因为实现接口而自动获得 @Audit
}

框架如果需要“方法注解继承”,必须自行沿方法解析顺序查找,例如先查实现类方法,再查父类方法和接口方法,并处理重载、桥接方法和协变返回值。

7.5 @Repeatable:重复注解的容器机制

import java.lang.annotation.Repeatable;

@Repeatable(Endpoints.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Endpoint {
    String path();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Endpoints {
    Endpoint[] value();
}

使用:

@Endpoint(path = "/users")
@Endpoint(path = "/members")
void list() {
}

编译器会按重复注解规则保存容器信息。读取时:

Endpoint[] endpoints = method.getAnnotationsByType(Endpoint.class);

通常应使用 getAnnotationsByType,而不是只调用 getAnnotation(Endpoint.class)。后者不会把多个重复注解直接当成一个数组返回。


八、运行时读取注解:代理、继承与默认值

定义一个运行时注解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Route {
    String path();
    String method() default "GET";
}

读取代码:

import java.lang.reflect.Method;

class UserController {
    @Route(path = "/users")
    public String users() {
        return "ok";
    }
}

public class AnnotationReadDemo {
    public static void main(String[] args) throws Exception {
        Method method = UserController.class.getMethod("users");

        Route route = method.getAnnotation(Route.class);

        System.out.println(route.path());   // /users
        System.out.println(route.method()); // GET
    }
}

运行时注解对象通常是由 JDK 生成的代理视图。读取一个带默认值的元素时,返回的是注解声明中的默认值;没有默认值的元素在编译阶段就必须提供。

要注意查找方法的区别:

method.getDeclaredAnnotation(Route.class);
method.getAnnotation(Route.class);
method.getDeclaredAnnotations();
method.getAnnotations();

它们在继承和查找范围上存在差异。对于方法注解,不应根据 @Inherited 推断父类方法注解会自动出现在子类方法上,因为 @Inherited 本身不适用于方法。


九、注解处理器:在编译期消费注解

9.1 注解处理器不是运行时反射

注解处理器实现:

javax.annotation.processing.Processor

通常通过继承:

javax.annotation.processing.AbstractProcessor

在编译阶段被 javac 调用。它读取的是 javax.lang.model 提供的符号模型,而不是已经加载并执行的业务类。

处理器可以:

  • 检查注解使用是否满足约束;
  • 发出编译错误或警告;
  • 生成新的源文件、类文件或资源文件;
  • 根据类型模型生成注册表、适配器和元数据。

处理器不能把运行中的业务对象当作输入,也不应通过 Class.forName 代替源码模型查询。此时目标类可能尚未生成,或者加载类会引入编译期副作用。

9.2 一个最小可运行处理器

注解:

package example.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface GenerateMarker {
}

处理器:

package example.processor;

import example.anno.GenerateMarker;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

public class GenerateMarkerProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment environment) {
        super.init(environment);
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Set.of(GenerateMarker.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(
            Set<? extends TypeElement> annotations,
            RoundEnvironment roundEnvironment) {

        for (Element element :
                roundEnvironment.getElementsAnnotatedWith(GenerateMarker.class)) {

            if (!(element instanceof TypeElement type)) {
                processingEnv.getMessager().printMessage(
                        Diagnostic.Kind.ERROR,
                        "@GenerateMarker 只能用于类型",
                        element
                );
                continue;
            }

            processingEnv.getMessager().printMessage(
                    Diagnostic.Kind.NOTE,
                    "发现类型: " + type.getQualifiedName(),
                    element
            );
        }

        return true;
    }
}

处理器需要通过 META-INF/services/javax.annotation.processing.Processor 注册,文件内容为:

example.processor.GenerateMarkerProcessor

被处理的源代码:

package example.app;

import example.anno.GenerateMarker;

@GenerateMarker
public class Demo {
}

使用 javac 时,处理器通常放在处理器路径上:

javac \
  -processorpath build/processor-classes \
  -cp build/processor-classes \
  -d build/app-classes \
  src/example/app/Demo.java

预期会看到类似编译器提示:

Note: 发现类型: example.app.Demo

这里的 Element 是源码声明或符号模型中的元素,TypeElement 表示类或接口声明。它不是 java.lang.Class,也没有要求目标类已经被 JVM 加载。

9.3 处理轮次和生成文件

注解处理可能有多轮:

  1. 第一轮读取用户源代码;
  2. 处理器生成新的源文件;
  3. 编译器编译新源文件;
  4. 后续轮次再次让处理器观察新产生的类型;
  5. 当没有新的源文件产生时进入最后一轮,RoundEnvironment.processingOver()true

处理器不能在同一轮无限生成同名文件,否则会出现 FilerException 或重复类型错误。

生成源文件应使用 Filer

JavaFileObject file = processingEnv.getFiler()
        .createSourceFile("example.generated.GeneratedRegistry");

try (Writer writer = file.openWriter()) {
    writer.write("""
            package example.generated;

            public final class GeneratedRegistry {
                private GeneratedRegistry() {}
            }
            """);
}

处理器应把错误定位到具体元素:

processingEnv.getMessager().printMessage(
        Diagnostic.Kind.ERROR,
        "缺少 path 属性",
        element
);

这样编译器通常可以把错误显示在对应源代码位置,而不是只输出一个无法定位的文本。

9.4 处理器中的泛型边界

注解处理器读取泛型时,不能使用运行时反射的 Type 体系,而要使用:

  • TypeMirror:类型镜像;
  • DeclaredType:参数化声明类型;
  • TypeVariable:类型变量;
  • WildcardType:通配符;
  • ArrayType:数组类型;
  • Types:类型关系判断;
  • Elements:名称、包和声明查询。

例如判断某类型是否是 List 的子类型,不应直接比较字符串名称,也不能简单依赖 TypeMirror.toString()。应使用:

Types types = processingEnv.getTypeUtils();
boolean assignable = types.isAssignable(candidate, listType);

这使处理器能够尊重继承、泛型参数和编译器的类型规则。


十、注解处理器与运行时反射如何配合

两者可以形成“编译期生成、运行时执行”的链路:

flowchart LR
    A[源代码与注解] --> B[javac]
    B --> C[注解处理器]
    C --> D[生成注册表/适配器]
    B --> E[class 文件]
    D --> E
    E --> F[类加载器]
    F --> G[Class 与运行时注解]
    G --> H[反射或 MethodHandle 调用]

关键路径是:

  1. 源代码中的 @Route 被处理器读取;
  2. 处理器生成 GeneratedRoutes
  3. javac 把用户类和生成类编译为 class 文件;
  4. 应用启动时加载生成的注册表;
  5. 注册表可以使用直接调用、反射或 MethodHandle 调用目标方法。

这类设计可以把“发现错误”的时间提前到编译期。例如,处理器可以检查路由路径重复;运行时反射则适合插件、脚本、动态配置等编译时无法确定的对象。

但生成代码并不会自动解决所有边界:

  • 生成代码仍然受模块访问规则约束;
  • 运行时类加载器必须能找到生成类;
  • 生成的成员调用必须匹配实际方法签名;
  • 增量编译必须正确声明生成文件与输入文件的关系;
  • 处理器生成的注册表可能与运行时部署的类版本不一致。

十一、泛型、类型擦除和反射的边界

11.1 为什么 List<String> 不能通过对象得到

考虑:

List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();

擦除后,两者运行时都主要表现为 ArrayList。因此:

strings.getClass() == integers.getClass(); // true

如果 JVM 根据对象实例区分 ArrayList<String>ArrayList<Integer>,同一个类文件就需要为每种参数化生成不同运行时类型,这不是 Java 泛型的实现模型。

泛型参数仍可从声明中读取:

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

class Repository {
    List<String> names;
}

Field field = Repository.class.getDeclaredField("names");
Type type = field.getGenericType();

if (type instanceof ParameterizedType parameterized) {
    Type raw = parameterized.getRawType();
    Type argument = parameterized.getActualTypeArguments()[0];

    System.out.println(raw);      // interface java.util.List
    System.out.println(argument); // class java.lang.String
}

这里读取的是 Repository.names 的声明签名,而不是某个 List 对象实例的实际元素类型。

11.2 通配符边界不是运行时检查

List<? extends Number> numbers;
List<? super Integer> integers;

这些边界主要服务于编译器类型检查:

  • ? extends Number:可以安全读取为 Number,但不能安全写入任意 Number
  • ? super Integer:可以写入 Integer,读取时只能安全视为 Object

反射读取泛型签名时可能看到 WildcardType,但运行时对象不会自动执行完整的泛型约束。下面的强制转换:

@SuppressWarnings("unchecked")
List<String> list = (List<String>) rawList;

通常只检查对象是否是 List,不会逐个检查已有元素是不是 String。真正访问元素时才可能抛出 ClassCastException

11.3 类型变量的上界

class Box<T extends Number> {
    T value;
}

擦除后,T 的擦除上界是 Number。反射读取字段时可能得到:

TypeVariable<?> variable = ...;
variable.getBounds(); // [Number]

如果没有显式上界:

class Box<T> {
    T value;
}

类型变量的默认上界是 Object

这解释了一个重要边界:Class<T> 能表达一个具体运行时类,但不能表达任意参数化类型。若需要保存 List<String> 这种类型信息,通常要使用 Type、自定义 TypeToken,或框架自己的类型描述对象。


十二、反射和 MethodHandle 的错误路径

动态调用的错误可以按阶段定位。

12.1 查找阶段

lookup.findVirtual(
    Calculator.class,
    "add",
    MethodType.methodType(int.class, int.class, int.class)
);

如果名称、静态性、参数或返回类型不匹配,可能得到:

  • NoSuchMethodException
  • IllegalAccessException
  • NoSuchFieldException 等对应查找异常。

这是“没有得到句柄”的失败。

12.2 适配阶段

handle.asType(expectedType);

如果两种方法类型之间不存在允许的转换,会抛出 WrongMethodTypeException。这是“句柄存在,但不能适配到要求的签名”。

12.3 调用阶段

handle.invokeExact(...);

调用点的静态签名与句柄类型不一致时,通常抛出 WrongMethodTypeException;参数对象不符合引用类型或绑定对象错误时,可能抛出 ClassCastException 或其他参数相关异常。

12.4 目标方法阶段

句柄已经正确调用,但目标方法自身抛出的异常会直接沿调用路径传播,而不像 Method.invoke 那样统一包装成 InvocationTargetException。这对异常处理和日志记录很重要。


十三、类加载器边界:名字相同不代表类型相同

JVM 中一个类的身份通常由:

类的二进制名称 + 定义它的类加载器

共同决定。

因此,如果两个不同类加载器分别加载同名类:

com.example.Plugin

它们可能是两个不同的运行时类型。即使类文件字节完全相同,也可能出现:

ClassCastException: com.example.Plugin cannot be cast to com.example.Plugin

这不是字符串名称矛盾,而是类加载器身份不同。

框架使用线程上下文类加载器加载插件时,应明确:

  • 插件接口由哪个类加载器定义;
  • 插件实现由哪个类加载器加载;
  • SPI 配置由哪个加载器搜索;
  • 反射得到的 Class<?> 是否与接口的 Class<?> 属于兼容加载器体系。

Class.forName 使用的加载器和 Thread.currentThread().getContextClassLoader() 可能不同。容器、应用服务器和插件系统中,这个差异经常决定资源和类型是否可见。


十四、模块边界与服务加载

Java 模块系统还影响注解处理器和运行时插件。

编译期处理器需要被编译器发现,通常通过处理器路径和服务注册文件完成;模块化处理器也可以通过模块声明提供 Processor 服务。

运行时插件若使用服务加载机制,需要:

  • 服务接口所在模块导出接口包;
  • 服务实现模块声明 provides ... with ...
  • 使用方声明 uses ...
  • 模块路径或类路径能够找到实现。

反射扫描“所有类”并不是 Java 模块系统提供的通用能力。类加载器只知道已经可见或被请求加载的类;要发现某个包下的全部类,通常需要构建工具、文件系统、模块层、JAR 索引或显式注册信息配合。把“扫描 classpath”误认为 JVM 原生能力,会导致部署到模块路径、容器或原生镜像环境时失败。


十五、反射、方法句柄与框架代理的关系

以事务或 AOP 代理为例,调用链可能是:

客户端
  -> 代理对象
  -> 前置逻辑
  -> 目标方法
  -> 后置逻辑/事务提交

代理可以使用反射调用目标方法,也可以预先把目标方法适配成 MethodHandle。两者的边界不同:

  • 反射适合按名称、注解和 Method 元数据进行发现;
  • MethodHandle 适合在发现后缓存一个有明确签名的调用路径;
  • 注解处理器适合把固定的发现结果在编译期生成;
  • 直接调用或生成字节码适合静态结构非常明确的场景。

代理必须正确处理以下情况:

  1. 目标方法是接口方法还是实现类方法;
  2. 方法是否是桥接方法;
  3. 返回类型是否协变;
  4. 异常是否被代理层包装;
  5. 目标对象是否来自另一个类加载器;
  6. 目标方法所属包是否允许当前模块访问;
  7. 注解是在接口方法、父类方法还是实现方法上声明。

例如,接口方法上的事务注解不会因为实现类方法被调用就自动通过 Method.getAnnotation 出现。框架需要定义自己的方法解析策略,而不能把注解继承规则交给 @Inherited


十六、常见误解与对应诊断

误解一:getDeclaredMethods() 能看到所有可调用方法

它只返回当前类声明的方法,不自动包含父类和接口方法。要分析完整可调用 API,需要沿继承体系和接口体系遍历,并处理重载、默认方法、桥接方法和合成方法。

误解二:setAccessible(true) 可以访问所有私有成员

模块封装、运行时权限和成员实际存在性仍然有效。遇到 InaccessibleObjectException,先检查模块是否开放目标包,而不是只检查 Java 访问修饰符。

误解三:@Inherited 能让方法注解向下传递

它只影响类注解,并且不覆盖接口实现、方法覆盖和字段。方法级继承必须由框架自行定义。

误解四:注解处理器需要 RetentionPolicy.RUNTIME

处理器在编译期读取注解。若只需要生成代码或诊断,可以使用 SOURCE;只有运行时反射需要 RUNTIME

误解五:invokeExact 会自动帮忙装箱和转换

它要求调用点签名与句柄类型完全一致。出现 WrongMethodTypeException 时,首先打印:

System.out.println(handle.type());

然后检查调用表达式的静态返回类型和参数类型。

误解六:反射能得到对象真实的泛型参数

对象实例通常只保留原始运行时类。getGenericType() 读取的是字段、方法或父类声明签名,不是集合当前内容的元素类型。

误解七:扫描注解等于扫描所有类

运行时只能读取已加载或可加载的类;包扫描是框架或部署工具构建的发现机制,不是 Class API 自动提供的全局索引。


十七、如何选择机制

可以按“信息何时确定”来选择:

需求 更合适的机制
编译期校验注解参数 注解处理器
根据类名加载插件 ClassLoaderClass.forName
根据注解发现方法 反射
发现后反复执行固定签名调用 缓存 MethodHandle
编译期生成无反射注册表 注解处理器 + 生成代码
读取参数化声明类型 TypeParameterizedTypeTypeVariable
访问模块内非公开成员 明确的模块 opens、合适的 Lookup
运行时高灵活性配置 反射或方法句柄,但必须处理签名和失败路径

这里没有“反射一定慢、方法句柄一定快”的规范结论。实际行为取决于调用频率、句柄是否稳定、适配层数量、JIT 优化、类加载器和缓存策略。更可靠的工程判断是:

  1. 先确定发现发生在编译期还是运行期;
  2. 再确定调用签名是否固定;
  3. 再确定模块和类加载器是否允许访问;
  4. 最后通过基准测试验证具体 JVM、具体版本和具体负载。

十八、边界检查清单

一个依赖反射或注解的组件,至少应明确回答这些问题:

  • 目标信息存在于源码、class 文件还是运行时对象中?
  • 注解使用了什么 RetentionPolicy
  • @Target 是否覆盖了实际使用位置,尤其是 TYPE_USE
  • 是否错误地依赖了 @Inherited
  • 反射查询是否需要声明成员,还是需要继承成员?
  • 是否会遇到桥接方法、合成方法和重载?
  • 目标包是否被模块导出或开放?
  • 使用的 Class 是否来自正确的类加载器?
  • 泛型信息来自声明签名,还是被误认为来自对象实例?
  • MethodHandle 的方法类型与调用点静态签名是否一致?
  • 目标方法异常是否需要解包,还是会直接传播?
  • 注解处理器是否正确处理多轮编译和重复生成?
  • 生成代码、目标类和运行时模块是否来自同一版本?

反射和注解真正的难点不在于记住几个 API 名称,而在于理解信息和权限的边界:Class 只能描述运行时类型,反射只能访问当前可见结构,MethodHandle 必须满足精确调用类型,元注解决定注解的作用范围,处理器只处在编译期,而泛型参数和方法注解也不会凭空跨越类型擦除、继承、模块和类加载器边界。


系列导航与关联阅读

官方资料

本文依据 Java、Spring 与相关项目官方文档重新梳理;正文、示例与生产清单由 WR BLOG 编写。