已经讲烂的Java反射但相信总有你漏掉的知识点

480 阅读14分钟

文章首发于博客:布袋青年,原文链接直达:Java反射详解


反射作为 Java 的三大特性之一其重要性不言而喻,它允许在运行时分析、检查和修改类、接口、字段和方法等程序构件的行为。

Java 反射的主要作用包括:

  • 在运行时获取对象的类型信息,包括类名、父类、接口、字段、方法等信息,从而可以实现动态实例化对象、动态调用方法等功能。
  • 可以在运行时访问和操作对象的私有属性和方法,从而实现一些 Java 语言本身无法实现的功能。
  • 可以实现动态代理,可以在程序运行时动态生成代理类,从而实现对目标对象的代理操作。
  • 可以实现工具类、框架和库等高级应用,如 Spring 框架中的 Bean 管理、 AOP 等功能,还可以实现 ORM 框架的数据映射等功能。

Java 反射的意义在于提高了程序的灵活性和可扩展性,使得程序在运行时可以动态地适应不同的需求和环境,从而增强了程序的适应性和可维护性。

一、反射介绍

1. 类对象

在开发中最常见的场景就是通过已由的属性信息从而初始化实例对象,而反射则恰恰相反,通俗一点的讲就是通过类从而获取类中的所具备的属性。

在进行类反射之前首先需要获取对应的类对象信息,如字符串对应的 String.class

针对类对象信息的获取 Java 中提供了三类方式:

  • xxx.class: 最常见的方式,但缺点在于无法作用于私有类等访问受限类。
  • Class.forName(): 通过类加载实现,默认会在加载时同时初始化对象,需要耗费一定资源。
  • loadClass(): 通过类加载实现,与 forName() 类似,但默认不初始化对象,当使用到才会初始化。

如下述示例中创建了一个私有测试类 Foo,并提供了四类类对象获取方式,但是由于 Foo 类是私有的,而通过 XXX.class 方式获取类对象是基于类导入实现因此此处是非法的。因此在实际应用中通常是基于类的完整限定名来获取类对象,但注意 cls2 中获取的方式会默认初始化对象,若对象内属性字段过多或程序需要频繁获取类对象时则会造成内存浪费,因为对象在初始化时需要为属性字段等信息设置默认值。

因此,在实际应用中更多推荐 cls3cls4 的声明方式,即默认不初始化对象,只有当程序需要用到该对象时才会为对象初始化。

// 测试私有类
private class Foo {
}

public class FooTest {
    public static void main(String[] args) {
        Class<?> cls1 = Foo.class;      // 非法,Foo 为私有类将无法导入类引用
        Class<?> cls2 = Class.forName("com.example.Foo");       // 默认初始化对象

        // 获取当前上下文类加载
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        // 更推荐下述两种方式
        Class<?> cls3 = Class.forName("com.example.Foo", false, classLoader);
        Class<?> cls4 = classLoader.loadClass("com.example.Foo")
    }
}

2. 类信息

在获取类对象之后即可通过其获取类的基本信息如包名、类型、作用域等等。

如下述示例即通过获取的类对象从而查询 String 类的基本信息。

public void basicInfo() {
    Class<?> cls = String.class;
    // 完整类名:java.lang.String
    System.out.println("Class name: " + cls.getName());
    // 类名:String
    System.out.println("Simple name: " + cls.getSimpleName());
    if (cls.getPackage() != null) {
        // 包名:java.lang
        System.out.println("Package name: " + cls.getPackage().getName());
    }
    // 是否为枚举类
    System.out.println(cls.getName() + " is enum? " + cls.isEnum());
    // 是否为接口类
    System.out.println(cls.getName() + " is interface? " + cls.isInterface());
    // 类是否私有
    System.out.println(cls.getName() + " is primitive? " + cls.isPrimitive());
}

3. 动态代理

除了获取类等基本信息外通过反射也可获取其字段 (Field)、方法 (Method)、方法参数 (Parameter) 与构造器 (Constructor) 等属性信息,这里仅以获取字段举例,其它类型使用方式同理。

当上述四类属性声明为非 public 而又需要在外部实现访问时,可通过 setAccessible(true) 方法开放访问权限,这样即便定义为 private 仍可通过访问在类外部中使用。

  • 信息获取

    每一类属性都提供 getXXXs()getDeclaredXXXs()getXXX()getDeclaredXXX() 四种获取方式,名称中包含 Declared 表明读取的为当前类中所定义信息,否则默认读取所有(包含继承的父类),结尾为 s 表示读取所有集合,否则为指定名称进行过滤查询。

    这里以类属性字段 Field 为例,其方法描述信息如下。

    方法 作用
    getFields() 获取所有类对象的属性 field(包括父类)。
    getDeclaredFields() 获取当前类的所有属性 field(不包括父类)。
    getField(name) 根据字段名获取指定属性 field(包括父类)。
    getDeclaredField(name) 根据字段名获取当前类的指定属性 field(不包括父类)。

    上述表格中对应方法的基本使用示例如下:

    public void fieldDemo() throws NoSuchFieldException {
        Class<Student> clazz = Student.class;
        // Get Supper class
        System.out.println("getSuperclass(): " + clazz.getSuperclass());
    
        // 获取自身与其父类所有字段, 只能获取 public 声明字段
        Field[] fields1 = clazz.getFields();
        System.out.println("getFields(): " + Arrays.toString(fields1));
    
        // 获取自身所有字段
        Field[] fields2 = clazz.getDeclaredFields();
        System.out.println("getDeclaredFields(): " + Arrays.toString(fields2));
    
        // 获取 Student 的指定字段信息
        Field name = clazz.getField("name");
        System.out.println("getField(name): " + name);
        System.out.println("getField(name).getName: " + name.getName());
        System.out.println("getField(name).getType: " + name.getType());
    
        // 获取字段详细信息
        int m = name.getModifiers();
        System.out.println("getField(name).getModifiers: " + m);
        System.out.println("Is Final: " + Modifier.isFinal(m));
        System.out.println("Is Public: " + Modifier.isPublic(m));
        System.out.println("Is Static: " + Modifier.isStatic(m));
    }
    
  • 属性赋值

    同理反射也可通过 set() 方法给实例对象赋值,若字段为私有属性时需要额外设置 setAccessible(true)

    public void demo() throws Exception {
        Student std = new Student(123, 85);
        // Get field instance
        Field score = Student.class.getDeclaredField("score");
        // Set accessible.
        score.setAccessible(true);
        // Update Filed value
        score.set(std, 90);
        // Get field value
        int s1= (int) score.get(std);
        System.out.println(s1);
        System.out.println(std);
    }
    

4. 方法调用

对象的方式反射与字段类似,同样提供了四类获取方式,这里不再重复介绍,这里重点介绍一下在通过反射获取方法对象之后如何进行调用。

在反射的 Method 对象中为方法调用提供了 invoke() 方法,即通过其实现方法的调用。

查看源码现即可看到 invoke() 定义了两个入参,具体含义如下:

  • obj: 即方法调用对象,在反射中通常通过 newInstance() 创建。
  • args: 对应目标方法的方法入参,可以为多个或零个。
public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        // 省去具体实现
    }

如下述示例中即通过反射调用 Foo 类中的 sayHello() 方法。

注意使用反射无论在获取字段还是方法当目标作用域为私有无法访问时需要通过 setAccessible(true) 设置允许访问,否则将会抛出 IllegalAccessException 异常。

public class Foo {
    private String sayHello() {
        return "Hello World!";
    }
}

public class FooTest {
    public static void main(String[] args) {
        // 获取类对象
        Class<?> cls = Class.forName("com.example.Foo");
        // 获取类中的方法
        Method method = cls.getMethod("sayHello");
        // 作用域为私有需要设置允许访问
        method.setAccessible(true);

        // 创建对象,等价于 Foo foo = new Foo()
        Foo foo = cls.newInstance();
        // 调用方法,等价于 foo.sayHello()
        Object invoke = method.invoke(foo);
        // 输出:Hello World!
        System.out.println(invoke);
    }
}

二、基本应用

1. 对象代理

在应用程序中若需要使用对象需要实现通过 new 关键字初始化,再编译为可执行的二进制 class 文件,但通过反射即可实现对象动态代理,即在运行时生成代理对象,实现对原对象的增强。

动态代理可以用于实现 AOP (面向切面编程),事务处理等,如下示例中即通过 newInstance() 方式生成代理对象,而非使用 new 关键字。

public void instanceDemo() throws Exception {
    Class<?> clazz = Teacher.class;
    Teacher teacher1 = (Teacher) clazz.newInstance();
    System.out.println("teacher1: " + teacher1);

    Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
    constructor.setAccessible(true);
    Teacher teacher2 = (Teacher) constructor.newInstance("ibudai");
    System.out.println("teacher2: " + teacher2);
}

需要注意虽然 clazz.newInstance()constructor.newInstance() 两种方式都能生成代理对象,但二者有着极大的差异。

  • clazz.newInstance():只能生成无参的代理对象,且无法对私有构造函数触发,新版 JDK 中已标记弃用。
  • constructor.newInstance():可以指定代理对象构造参数,且通过 setAccessible() 可作用于私有构造器。

2. 堆栈信息

在上面的示例中介绍了在定义时通过反射获取类信息,下面介绍一下如何在运行中获取方法的堆栈信息。

通过 Thread.currentThread().getStackTrace() 即可获取当前方法调用的堆栈信息 StackTraceElement ,返回的类型为数组对象。 StackTraceElement 类包含了如下基本信息:

属性 描述
ClassName 方法所有类的完成限定名,如 xyz.ibudai.bean.User。
MethodName 方法的名称。
FileName 方法所在类的文件名称,如 User.java。

返回的数组中第二个对象(下标为 1)为当前方法的信息,第三个对象(下标为 2)为当前方法调用者的信息,具体示例如下:

public void call() {
    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
    // 当前方法堆栈信息
    StackTraceElement current = stackTrace[1];
    // 调用者方法堆栈信息
    StackTraceElement caller = stackTrace[2];
    System.out.println("Current info: " + String.format("Class:[%s], Method:[%s], File:[%s]",
            current.getClassName(), current.getMethodName(), current.getFileName()));
    System.out.println("Caller info: " + String.format("Class:[%s], Method:[%s], File:[%s]",
            caller.getClassName(), caller.getMethodName(), caller.getFileName()));
}

三、高级应用

1. 依赖引用

在默认的 JDK 中可利用反射从而实现类的动态获取或创建等操作,而 org.reflections 类库在此基础上提供更丰富的操作。

在开始之前现在工程中导入下述 Maven 依赖。

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.10.2</version>
</dependency>

2. 初始化

org.reflections 类库的核心处理基于 Reflections 对象,通过其指定反射作用对象。

在传统的反射中通过 Class<?> 针对的是单个类的反射操作,而 Reflections 则更多的应用场景为针对包 package 而言,如下实例中创建了 Reflections 作用于 xyz.ibudai.service 包路径。

public void demo() {
    String packageName = "xyz.ibudai.service";
    Reflections reflections = new Reflections(packageName);
}

Reflections 对象的常用操作接口方法参考下表,同样的 Reflections 针对构造器(Constructor)、字段(Field)与方法(Method)提供了一系列反射方法这里不作详细阐述。

方法 作用
getSubTypesOf() 获取 Reflections 作用域下指定类的所有实现类或继承类。
getTypesAnnotatedWith() 获取 Reflections 作用域下使用指定的注解的所有类。

3. 示例演示

在开始之前先创建如下测试接口类 HelloService 及其相应的两个测试实现类用于后续操作。

package xyz.ibudai.service;

public interface HelloService {
    void sayHello();
}

public class StudentHelloImpl implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("Student say hello.");
    }
}

public class TeacherHelloImpl implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("Teacher say hello.");
    }
}

完成上述测试类创建之后即可实现相应的操作,下述中提供了两个实例作用如下:

  • demo1(): 查询 xyz.ibudai.service 包路径下所有包含 @Producer 注解的类。
  • demo2(): 查询 xyz.ibudai.service 包路径下所有 HelloService 类的实现类或继承类。

完整的 Reflections 示例代码如下:

public class ReflectionTest {
    @Test
    public void demo1() {
        String packageName = "xyz.ibudai.service";
        Reflections reflections = new Reflections(packageName);
        // getTypesAnnotatedWith(): Get class with specify annotation
        Set<Class<?>> classSet = reflections.getTypesAnnotatedWith(Producer.class);
        for (Class<?> cls : classSet) {
            System.out.println(cls.getName());
        }
    }

    @Test
    public void demo2() {
        String packageName = "xyz.ibudai.service";
        Reflections reflections = new Reflections(packageName);
        Class<HelloService> parent = HelloService.class;
        // getSubTypesOf(): Get class extends and implementation source.
        Set<Class<? extends HelloService>> classSet = reflections.getSubTypesOf(parent);
        for (Class<? extends HelloService> cls : classSet) {
            System.out.println("Name: " + cls.getName());
        }
    }
}

四、Unsafe

反射一个较为常见的用法即用于获取 Unsafe 实例,因为其并不提供直接的构造方法用于初始化。Unsafe 类虽然类名为不安全,但并非表示这个类不安全,而是 Unsafe 常涉及到内存等敏感操作,因此称其非安全类。

1. 实例获取

Unsafe 无法直接通过 new 实现对象创建,只能通过反射的方式获取。

/**
 * 通过反射获取 Unsafe 实例
 */
public void init() throws NoSuchFieldException, IllegalAccessException {
    Field field = Unsafe.class.getDeclaredField("theUnsafe");
    field.setAccessible(true);
    Unsafe unsafe = (Unsafe) field.get(null);
}

2. 基本操作

在获取 unsafe 实例之后即可利用其实现一系列操作,如常见的的 CAS 等等。

public void info() throws NoSuchFieldException {
    MyEntity entity = new MyEntity("Alex");
    long offset = unsafe.objectFieldOffset(MyEntity.class.getDeclaredField("name"));
    // Get object value
    String str = (String) unsafe.getObject(entity, offset);
    System.out.println(str);

    // Put object value
    unsafe.putObject(entity, offset, "Beth");
    System.out.println(entity.getName());

    // CAS: 保证操作原子性
    unsafe.compareAndSwapObject(entity, offset, "Beth", "Jack");
    System.out.println(entity.getName());
}

public static class MyEntity {

    public String name;

    public MyEntity(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

3. 内存操作

Unsafe 之所以被称为非安全的一大重要原因即它提供了直接操作内存的能力,那就让我们看一下它究竟是怎么个操作法。

Unsafe 针对内存操作的常用方法如下:

  • allocateMemory(): 分配一块连续的直接内存块,不受堆内存与 GC 的管控,返回内存块的起始地址。
  • freeMemory(address): 根据传入的内存地址释放内存资源,在分配内存后一定要进行释放。
  • putByte(address, data): 在指定内存处写入一字节数据。
  • getByte(address, data): 在指定内存处读取一字节数据。

如下述示例即通过 unsafe 对象手动声明了一块直接内存,并存储一个 entity 对象,其中 convertObjbyteToObject 即利用 ByteArrayInputStreamByteArrayOutputStreamJava 对象进行二进制的转化,因为内存中存储的数据必须为字节数据。

public void memoryDemo() {
    MyEntity entity = new MyEntity("Alex");
    byte[] data = ObjSerialize.convertObj(entity);
    // Allocate native memory, return the memory start address
    long address = unsafe.allocateMemory(data.length);

    try{
        // Put real data to memory
        for (int i = 0; i < data.length; i++) {
            // putByte(val1, val2):
            // ==> val1: the memory address
            // ==> val2: the data
            unsafe.putByte(address + i, data[i]);
        }

        byte[] origin = new byte[data.length];
        for (int i = 0; i < origin.length; i++) {
            // Read data from native memory
            origin[i] = unsafe.getByte(address + i);
        }
        MyEntity result = ObjSerialize.byteToObject(origin, MyEntity.class);
        System.out.println("Result: " + result);
    } catch (Exception ignored) {
    } finally {
        // Release the memory, important!!!
        unsafe.freeMemory(address);
    }
}

4. 内存对齐

Unsafe 中通过 allocateMemory() 申请的内存属于直接内存,是未经任何初始化的内存空间。

在计算机系统中,有些硬件操作要求数据以特定的字节对齐方式存储,这可以提高数据读取和写入的效率。因此对于 allocateMemory() 申请的内存空间,通常为了更好的性能会利用空数据进行对齐填充操作,以确保数据存储在内存中时满足特定的对齐要求,进而提高了程序的执行效率。

如下示例即对与 unsafe 申请的内存空间进行对齐填充操作。

private static Unsafe unsafe;

private static long address;

private static byte[] data;

private void preFillData(byte[] data) {
    int position = 0;
    int dataLen = data.length;
    for (; dataLen - position >= 8; position++) {
        unsafe.putLong(address + position, 0L);
    }

    for (; dataLen - position >= 4; position++) {
        unsafe.putInt(address + position, 0);
    }

    for (; dataLen - position >= 0; position++) {
        unsafe.putByte(address + position, (byte) 0);
    }
}

五、Spring反射

1. bean获取

Spring 工程中获取已注入的 Bean 对象可通过 @Autowired 获取应用上下文对象 applicationContext,再通过其 getBean() 方式即可获取相应的 Bean 对象。

具体的示例如下:

public class BeanService {
    /**
     * 通过装配获取上下文对象
     */
    @Autowired
    private ApplicationContext applicationContext;
        
    /**
     * 通过名称获取 bean 对象
     */
    public Object getBean(String bean) {
        return applicationContext.getBean(bean);
    }
}

2. 反射代理

在获取 Bean 对象之后,即可通过反射实现方法的执行调用。

相应的示例代码如下,其中 methodNamebean 对象中所要执行的方法, params 为方法的参数。

public static Object springInvokeMethod(String serviceName, String methodName, Object[] params) {
    Object service = getBean(serviceName);
    Class<?>[] paramClass = null;
    if (params != null) {
        int length = params.length;
        paramClass = new Class[length];
        for (int i = 0; i < length; i++) {
            paramClass[i] = params[i].getClass();
        }
    }
    // Find target method
    Method method = ReflectionUtils.findMethod(service.getClass(), methodName, paramClass);
    // Executed method
    assert method != null;
    return ReflectionUtils.invokeMethod(method, service, params);
}

六、注解介绍

1. 定义方式

Java 中通过 @interface 创建注解,并通过 @Target@Retention 定义注解的作用对象与声明周期对象。

  • 注解类型

    通过 @Target 注解定义注解的作用对象,其取值范围如下:

    参数 描述
    ElementType.TYPE 表明注解作用于类。
    ElementType.CONSTRUCTOR 表明注解作用于构造方法。
    ElementType.FIELD 表明注解作用于字段。
    ElementType.METHOD 表明注解作用于方法。
    ElementType.PARAMETER 表明注解作用于方法参数。
  • 作用范围

    通过 @Retention 注解定义注解的声明周期,其取值范围如下:

    参数 描述
    RetentionPolicy.CLASS 默认的取值,表明作用于类文件。
    RetentionPolicy.SOURCE 表明注解作用编译期间。
    RetentionPolicy.RUNTIME 表明注解作用程序运行期间。

    如下定义了一个方法注解,作用对象为方法,并设置作用于程序运行期间。

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MethodAnn {
    
        String value() default "";
    
        String comment() default "";
    }
    

2. 使用示例

这里我提前自定义了三类不同的注解,具体注解声明之前之前的文章已经介绍过了这里便不再详细说明,同时新建测试类 TestService 对三类注解进行简单应用供后续使用。

@ClassAnn(value = "test-class", comment = "test-class-comment")
public class TestService {

    @FieldAnn("test-field1")
    public String field1;

    @FieldAnn("test-field2")
    public String field2;

    @MethodAnn("test-method1")
    public void method1() {

    }

    @MethodAnn("test-method2")
    public void method2(@ParameterAnn("abc") String msg1,
                        @ParameterAnn("def") String msg2) {
        System.out.println(msg1 + msg2);
    }
}

3. 属性获取

在完成上述注解的定义与使用之后,即可通过反射获取不同注解中配置的属性值。

  • 类注解属性

    一个类中相同的类注解只能包含一个,因此类注解获取相对简单,直接通过反射获取即可。

    通过 clazz.getDeclaredAnnotations() 获取是类上定义的所有注解,而 clazz.getAnnotation(annClazz) 获取的则是指定注解。

    public void classDemo() {
        Class<TestService> clazz = TestService.class;
        Class<ClassAnn> annClazz = ClassAnn.class;
        // Class is existed "ClassAnn"?
        System.out.println("Exist annotation: " + clazz.isAnnotationPresent(annClazz));
    
        // Get declare annotation
        Annotation[] annotations = clazz.getDeclaredAnnotations();
        System.out.println("Annotations: " + Arrays.toString(annotations));
    
        // Get class annotation value
        ClassAnn classAnn = clazz.getAnnotation(annClazz);
        System.out.println("The annotation value: " + classAnn.value());
        System.out.println("The annotation comment: " + classAnn.comment());
    }
    
  • 字段注解属性

    一个类中可能包含多个属性字段,其基本操作方法参考下表。

    方法 作用
    isAnnotationPresent(class) 判断字段是否存在注解。
    getAnnotatedType() 获取注解作用对象信息,包含字段类型与字段名等信息。
    getAnnotations() 获取字段上的所有注解,包含其父类中标识。
    getAnnotation(class) 通过注解类信息获取指定注解,包含其父类中标识。
    getDeclaredAnnotations() 获取字段上的所有注解,不包含其父类中标识。
    getDeclaredAnnotation(class) 通过注解类信息获取指定注解,不包含其父类中标识。

    如下示例中通过反射获取所有字段列表,再由 getDeclaredAnnotation() 获取字段注解并其配置值。

    public void fieldDemo() {
        Class<TestService> clazz = TestService.class;
        Field[] fields = clazz.getDeclaredFields();
        for (Field f : fields) {
            System.out.printf("=========== %s ============\n", f.getName());
            boolean isPresent = f.isAnnotationPresent(fieldAnnClass);
            System.out.println("Exist annotation ? " + isPresent);
            if (isPresent) {
                FieldAnn fieldAnn = f.getAnnotation(FieldAnn.class);
                System.out.println("Value: " + fieldAnn.value());
                System.out.println("Comment: " + fieldAnn.comment());
            }
        }
    }
    
  • 方法注解属性

    方法注解的获取思路与字段注解获取类似,这里就不再描述直接提供代码示例。

    public void methodDemo() {
        Class<TestService> clazz = TestService.class;
        Method[] methods = clazz.getDeclaredMethods();
        for (Method m : methods) {
            System.out.printf("=========== %s ============\n", m.getName());
            boolean isPresent = m.isAnnotationPresent(methodAnnClass);
            System.out.println("Exist annotation ? " + isPresent);
            if (isPresent) {
                MethodAnn methodAnn = m.getAnnotation(methodAnnClass);
                System.out.println("Value: " + methodAnn.value());
                System.out.println("Comment: " + methodAnn.comment());
            }
        }
    }
    
  • 参数注解属性

    方法中的参数注解获取对象较为复杂,通过反射返回的结果是一个二维数组 A[i][j],其中的 A[i][] 表示方法的第 i 个参数注解, A[][j] 存储的是对应注解的属性值。

    参数注解的获取示例如下:

    public class TestService {
        public void method2(@ParameterAnn("abc") String msg1,
                            @ParameterAnn("def") String msg2) {
            System.out.println(msg1 + msg2);
        }
    }
    
    public void parameterDemo() throws NoSuchMethodException {
        Class<TestService> clazz = TestService.class;
        // 根据方法名和参数类型获取指定方法
        Method method = clazz.getMethod("method2", String.class, String.class);
        // Annotation[i][j] i: 方法的第 i 个参数注解, j: 该参数注解的第 j 个值
        Annotation[][] annos = method.getParameterAnnotations();
        Arrays.stream(annos).forEach(record -> {
            ParameterAnn parameterAnn = null;
            for (Annotation ann : record) {
                if (ann instanceof ParameterAnn) {
                    parameterAnn = (ParameterAnn) ann;
                }
            }
            System.out.print("\n" + parameterAnn);
            System.out.print("Annotation value: " + parameterAnn.value());
            System.out.print("Annotation comment: " + parameterAnn.comment());
        });
    }
    

    最终打印输出的结果如下:

    @xyz.ibudai.ann.ParameterAnn(value="abc", comment=""), Annotation value: abc, Annotation comment: 
    @xyz.ibudai.ann.ParameterAnn(value="def", comment=""), Annotation value: def, Annotation comment: