反射和注解

反射

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

描述类中内容的类

一个类往往包含字段,函数。其对应的为Class,Field,Method

Class即代表了一个的抽象(请注意不是实例,而是类)

Method代表了一个方法的抽象

Field代表了一个字段的抽象

任何类及其内部的字段,方法均可以使用以上三个类进行描述

即一个Class与多个Field,Method相关联

实例中用到的类

class Person{
    private int age;
    public double money;
    public Person() {
    }
    public Person(int age, double money) {
        this.age = age;
        this.money = money;
    }

    public int getAge() {
        return age;
    }

    public double getMoney() {
        return money;
    }
    private static final void sayHello(){
        System.out.println("hello");
    }

    @Override
    public String toString() {
        return new StringJoiner(", ", Person.class.getSimpleName() + "[", "]")
                .add("age=" + age)
                .add("money=" + money)
                .toString();
    }
}

反射基础类

Class

public static void getAClass() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //可以通过类名.class 或者一个实例调用getClass()的方法获取一个反射
    //或者通过全限定名来获取
        Class<Person> aClass = Person.class;
        Class<Person> aClass1 = (Class<Person>) new Person().getClass();
        Class<?> aClass2 = Class.forName("com.Person");
        //获取包名
        System.out.println(aClass.getPackageName());
        //获取完整类名 即包名+类名
        System.out.println(aClass.getName());
        //获取类加载器
        System.out.println(aClass.getClassLoader());
        //获取其实现的全部接口
        System.out.println(Arrays.toString(aClass.getInterfaces()));
        //获取其声明的全部字段
        System.out.println(Arrays.toString(aClass.getDeclaredFields()));
        //获取其声明的全部方法
        System.out.println(Arrays.toString(aClass.getDeclaredMethods()));
        //获取一个构造器然后实例化一个Person实例
        System.out.println(( aClass.getConstructor(int.class, double.class).newInstance(10, 100)));
    }

其结果输出

com
com.Person
jdk.internal.loader.ClassLoaders$AppClassLoader@2f0e140b
[]
[private int com.Person.age, public double com.Person.money]
[public java.lang.String com.Person.toString(), public int com.Person.getAge(), public double com.Person.getMoney(), private void com.Person.sayHello()]
Person[age=10, money=100.0]

请注意其中的形如getDeclared*方法,其能够获取到所有声明的方法

而对应get*的方法则只能够获取到非public方法

请看下面这个代码的执行

 public static void diffDeclared(){
        Class<Person> personClass = Person.class;
        System.out.println(Arrays.toString(personClass.getDeclaredMethods()));
        System.out.println(Arrays.toString(personClass.getMethods()));
    }

其输出

[public java.lang.String com.Person.toString(), public int com.Person.getAge(), public double com.Person.getMoney(), private void com.Person.sayHello()]

[public java.lang.String com.Person.toString(), public int com.Person.getAge(), public double com.Person.getMoney(), public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException, public final void java.lang.Object.wait() throws java.lang.InterruptedException, public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException, public boolean java.lang.Object.equals(java.lang.Object), public native int java.lang.Object.hashCode(), public final native java.lang.Class java.lang.Object.getClass(), public final native void java.lang.Object.notify(), public final native void java.lang.Object.notifyAll(), public default void com.temp.t()]

即简单来说如果需要获取到声明的private属性或者方法就需要getDeclared*方法

Method

Method即为方法的抽象

还记得我们之前提到的如何分辨两个函数吗?其中就会涉及到方法调用者的区别

所以说一个Method的实例必须通过一个Class实例来获取

public static void getAMethod() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Person person = new Person(10, 100.00);
        Class<? extends Person> personClass = person.getClass();
        //根据方法签名获取一个Method实例
        Method setAge = personClass.getDeclaredMethod("setAge", int.class);
        //获取返回值类型
        System.out.println("返回值类型为"+setAge.getReturnType());
        //获取参数信息
        Arrays.stream(setAge.getParameters())
                //在默认编译选项下 getName的返回值为arg${index}
                .map(p -> "参数类型为:"+p.getType()+",参数名称为"+p.getName())
                .forEach(System.out::println);
        //反射调用这个函数,第一个参数为调用这个方法的实例(此处也符合面向对象的多态)
        //后续变长参数为实际调用方法参数
        setAge.invoke(person, 23);
        System.out.println("修改后的为"+person.getAge());

        Method sayHello = personClass.getDeclaredMethod("sayHello");
        //如果反射调用的这个方法为private则需要设定允许访问,否则报错
        sayHello.setAccessible(true);
        sayHello.invoke(person);
        //获取方法描述
        int modifiers = sayHello.getModifiers();
        System.out.println(Modifier.toString(modifiers));
    }

其输出为

返回值类型为class com.Person
参数类型为:int,参数名称为arg0
修改后的为23
hello
private static final

如何获取参数名*

添加编译参数-parameters

idea操作:

Settings > Build, Execution, Deployment > Compiler > Java Compiler > Additional command line parameters

此时输出为:

返回值类型为class com.Person
参数类型为:int,参数名称为age
修改后的为23
hello
private static final

而maven是这样配置的

<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <compilerVersion>${maven.compiler.target}</compilerVersion>
                    <source>${java.version}</source>
                    <target>${java.version}</target>

                    <compilerArgs>
                        <arg> -paramter </arg>
          
                    </compilerArgs>
                </configuration>
            </plugin>

Field

Field是对一个字段的抽象

通过这个我们可以直接修改字段的值,或者获取信息等

public static void getField() throws NoSuchFieldException, IllegalAccessException {
        Person person = new Person(10, 100);
        Class<? extends Person> personClass = person.getClass();
        Field age = personClass.getDeclaredField("age");
        //还是私有的需要设置访问权限
        age.setAccessible(true);
        //通过Field的get方法获取调用者内部的值
        System.out.println("此时年龄为:"+age.get(person));
        //通过Field的set方法修改调用者内部的值
        age.set(person, 1000);
        System.out.println("此时年龄为:"+age.get(person));
        //该字段的类型
        System.out.println(age.getType());
        int modifiers = age.getModifiers();
        System.out.println(Modifier.toString(modifiers));
    }

其输出为

此时年龄为:10
此时年龄为:1000
int
private

实战

bean参数的拷贝

两个不同类存在相同名称的和类型的字段,要把对应值互相拷贝

public static void beanCopy(Object src,Object target) throws IllegalAccessException {
        //只模拟字段类型相同的情况
        Class<?> srcClass = src.getClass();
        Class<?> targetClass = target.getClass();
        for (Field field : srcClass.getDeclaredFields()) {
            field.setAccessible(true);
            for (Field targetClassDeclaredField : targetClass.getDeclaredFields()) {
                if (targetClassDeclaredField.getName().equals(field.getName())){
                    targetClassDeclaredField.set(target, field.get(src));
                    break;
                }
            }
        }
    }

注解

何为注解 ?就是一个携带元数据的标识

最常见的就是@Override注解,其提示编译器检查这个是否为重写的方法

注解的组成

观察这个自定义的注解

首先两个jdk提供的注解负责规定这个注解的使用范畴

java.lang.annotation.Target可以提供多个枚举类负责规定这个自定义注解可以标注在哪里

现在这个自定义注解可以标识在类上(更多类型请参照java.lang.annotation.ElementType

java.lang.annotation.Retention可以提供多个枚举负责提供这个自定义注解的生命周期

现在这个自定义注解存在于运行时(其余生命周期请参照java.lang.annotation.RetentionPolicy

//标识
@Target(ElementType.TYPE)
//注解的声明周期
@Retention(RetentionPolicy.RUNTIME)
public @interface User {
    String username();
    boolean isBanned() default false;
    String password();
    Status status();
    
}

我们先来看如何使用这个注解再来解释上面那些看起来和方法一样东西的作用

@User(username = "us",password = "***",status = Status.ONLINE)
public class LoginUser {
    private String username;
    private boolean isBanned;
    private String password;
    private Status status;
    @Override
    public String toString() {
        return new StringJoiner(", ", LoginUser.class.getSimpleName() + "[", "]")
                .add("username='" + username + "'")
                .add("isBanned=" + isBanned)
                .add("password='" + password + "'")
                .add("status=" + status)
                .toString();
    }
}
public enum Status {
    ONLINE,
    OUTLINE;
}

你在使用这个类或者这个类的实例的时候,其并不会影响你的使用

它仅仅是起到一个标注作用而已,类似于你在一个箱子上贴一个贴纸,一个携带信息的贴纸

然后再让我们回到携带的信息部分

观察这个注解中的类似于方法的部分

其“返回值”的类型很有限,只可以允许八种基本类型及其包装类,String,枚举类,Class

其允许一个默认值,使用default指定缺省默认值

不存在默认值的项在标注时必须指定其值

如何使用标注的这些元数据

运行时

public static void getMetaData(){
        Class<LoginUser> loginUserClass = LoginUser.class;
        //只有标识@Retention(RetentionPolicy.RUNTIME)的自定义注解才可以取到
        //如果其并没有标识这个注解其返回值为null
        User user = loginUserClass.getAnnotation(User.class);
        //就类似于方法一样使用
        System.out.println(user.isBanned());
        System.out.println(user.password());
        System.out.println(user.username());
        System.out.println(user.status());

    }

输出

false
***
us
ONLINE

然后我们利用这个注解生成一个实体类,这里需要一定的反射知识

 public static void getBeanFromMetaData() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Class<LoginUser> loginUserClass = LoginUser.class;
        //只有标识@Retention(RetentionPolicy.RUNTIME)的自定义注解才可以取到
        //如果其并没有标识这个注解其返回值为null
        User user = loginUserClass.getAnnotation(User.class);
        //获取一个实例通过空参构造器
        LoginUser loginUser = loginUserClass.getConstructor().newInstance();


        Field username = loginUserClass.getDeclaredField("username");
        username.setAccessible(true);
        username.set(loginUser, user.username());

        Field password = loginUserClass.getDeclaredField("password");
        password.setAccessible(true);
        password.set(loginUser, user.password());


        Field isBanned = loginUserClass.getDeclaredField("isBanned");
        isBanned.setAccessible(true);
        isBanned.set(loginUser, user.isBanned());

        Field status = loginUserClass.getDeclaredField("status");
        status.setAccessible(true);
        status.set(loginUser, user.status());

        System.out.println(loginUser);

    }

编译期*

相信你也应该用过lombok这种库,他的实现就是编译期解析对应的注解然后,反射javac的库进行语义修改,这种做法会妨碍你的jdk升级,jdk17刚出的时候就因为模块化的强封装机制把它拦下来了。当然我是不推荐使用这个库的,但是不妨碍我们从中学习一点技术。

很多语言都存在 这种元编程技术,比如说rust的过程宏可以支持额外生成新的代码,修改原有的代码树,进而提高代码的可读性和性能,很可惜java只允许你根据它解析出来的元数据让你生成新的文件。这项技术叫APT,annotation process tool

但是就目前来讲足够用了,很久之前的安卓开发中就经常使用这种技术,Dagger2, ButterKnife这些库都是,现在java后端开发中为了减少反射的使用,将更多的任务安排到编译期降低运行时的损耗也在逐步采用这种模式,比如说quarkus和spring native

首先做点准备

<dependency>
        <groupId>com.google.auto.service</groupId>
        <artifactId>auto-service</artifactId>
        <version>${auto-service.version}</version>
        <scope>provided</scope>
</dependency>

 <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>${maven-compiler-plugin.version}</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>

现在我们的目的是帮助一个实体类生成builder方法

public class Person {
    private int age;
    private String name;
    // getters and setters …
}
//以生成这样的方法
{
   Person person = new PersonBuilder()
  .setAge(25)
  .setName("John")
  .build();
}

既然是注解处理器,那么我们得先指定一个要解析的注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface BuilderProperty {
}

然后放到需要的方法上面

public class Person {
    private int age;
    private String name;
    @BuilderProperty
    public void setAge(int age) {
        this.age = age;
    }
    @BuilderProperty
    public void setName(String name) {
        this.name = name;
    }
}

然后就需要一个处理器了

@SupportedAnnotationTypes(
  "com.dreamlike.annotation.processor.BuilderProperty")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class) //这个来自于我们引入的那个依赖
public class BuilderProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, 
      RoundEnvironment roundEnv) {
        return false;
    }
}

你不仅可以指定具体的注解类名称,还可以指定通配符,例如*“com.dreamlike.annotation.*”来处理 com.dreamlike.annotation 包及其所有子包中的注解,甚至可以指定“*”*来处理所有注解。

你的注解作为第一个 Set<? extens TypeElement> annotations参数传递,有关当前处理轮次的信息作为 RoundEnviroment roundEnv 参数传递。

如果你的注解处理器已处理所有传递的注解,并且不希望将它们传递给列表中的其他注解处理器,则返回值应该是true

然后发挥你的创造力输出就行了(可以看看javapoet这个依赖)

//获取输出源
JavaFileObject builderFile = processingEnv.getFiler()
  .createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
    // writing generated file to out …
}

然后把它打包就行了

之后需要使用这个只需要配置一下的compile插件就行了

<build>
    <plugins>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
                <generatedSourcesDirectory>${project.build.directory}
                  /generated-sources/</generatedSourcesDirectory>
                <annotationProcessors>
                    <annotationProcessor>
                        com.dreamlike.annotation.processor.BuilderProcessor
                    </annotationProcessor>
                </annotationProcessors>
            </configuration>
        </plugin>

    </plugins>
</build>

参考这个项目tutorials/annotations at master · eugenp/tutorials (github.com)

写最后的话

反射不过是我们用于元编程的一种手段,虽然可以帮助我们提高程序的灵活性,但是不要乱用

尤其注意,不要乱反射jdk内部的参数,为了升级的兼容性,不要对标准库的实现做任何的假设,不要假设实现随着升级不发生改变

能够编译期解决的事情就不要拖延到运行时(点名批评spring),能通过良好oop抽象解决的就不要滥用反射

反射,类加载器,源代码生成,字节码生成都是我们实际扩充灵活性的利器,可能我们写业务用不到,但是我们总需要了解我们使用的工具是怎么实现的,以帮助我们提高所谓的”造轮子“的能力

因为java是一种比较安全的语言,其实反射也存在一些限制,这一块从jdk9之后会变得越来越明显,下一篇我会讲讲模块化以及它为反射带来的限制

附录

java反射

Java的反射调用性能很低吗

基于jdk动态代理的Aop实现

死磕 java魔法类之Unsafe解析(Unsafe也可以做类似于Field的修改)

JUC整理笔记四之梳理VarHandle(更安全的Unsafe修改变量的封装,其中涉及到的内存顺序我会有空再写一篇)

方法句柄MethodHandle使用

注意实际上在jdk18之后反射由methodhandler重写实现过

JEP draft: 用方法句柄重写反射 - 掘金 (juejin.cn)

Last updated