反射
Reflection 是指程序在运行期可以拿到一个对象的所有信息。反射是为了解决在运行期对某个实例一无所知的情况下如何调用其方法。
Class 类
class 是由 JVM 在执行过程中动态加载的。JVM 在第一次读取到一种 class 类型时将其加载进内存。每加载一种 class,JVM 就为其创建一个 Class 类型的实例并关联起来。注意:这里的 Class 类型是一个名叫 Class 的 class。它长这样:
public final class Class {private Class() {}
}
以 String 类为例,当 JVM 加载 String 类时,它首先读取 String.class
文件到内存,然后为 String 类创建一个 Class 实例并关联起来:Class cls = new Class(String);
。这个 Class 实例是 JVM 内部创建的,如果我们查看 JDK 源码,可以发现 Class 类的构造方法是 private,只有 JVM 能创建 Class 实例,我们自己的 Java 程序是无法创建 Class 实例的。所以 JVM 持有的每个 Class 实例都指向一个数据类型(class 或 interface)。
由于 JVM 为每个加载的 class 创建了对应的 Class 实例,并在实例中保存了该 class 的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此如果获取了某个 Class 实例,我们就可以通过这个 Class 实例获取到该实例对应的 class 的所有信息。这种通过 Class 实例获取 class 信息的方法称为反射 Reflection 。
获取一个 class 的 Class 实例:
// 方法一:直接通过一个class的静态变量class获取:
Class cls = String.class;// 方法二:如果我们有一个实例变量,可以通过该实例变量提供的 getClass() 方法获取:
String s = "Hello";
Class cls = s.getClass();// 方法三:如果知道一个class的完整类名,可以通过静态方法 Class.forName() 获取:
Class cls = Class.forName("java.lang.String");
因为 Class 实例在 JVM 中是唯一的,所以上述方法获取的 Class 实例是同一个实例。可以用 ==
比较两个 Class 实例。通常情况下我们应该用 instanceof 判断数据类型。只有在需要精确判断一个类型是不是某个 class 的时候,我们才使用 ==
判断 class 实例。
System.out.println("Class name: " + cls.getName());
System.out.println("Simple name: " + cls.getSimpleName());
if (cls.getPackage() != null) {System.out.println("Package name: " + cls.getPackage().getName());
}
System.out.println("is interface: " + cls.isInterface());
System.out.println("is enum: " + cls.isEnum());
System.out.println("is array: " + cls.isArray());
System.out.println("is primitive: " + cls.isPrimitive());
动态加载
JVM 在执行 Java 程序的时候,并不是一次性把所有用到的 class 全部加载到内存,而是第一次需要用到 class 时才加载。
// Main.java
public class Main {public static void main(String[] args) {if (args.length > 0) {create(args[0]);}}static void create(String name) {Person p = new Person(name);}
}
当执行 Main.java
时,JVM 首先会把 Main.class
加载到内存。然而并不会加载 Person.class
,除非程序执行到 create()
方法,JVM 发现需要加载 Person 类时,才会首次加载 Person.class
。如果没有执行 create()
方法,那么 Person.class
根本就不会被加载。这就是 JVM 动态加载 class 的特性。
动态加载 class 的特性对于 Java 程序非常重要。利用 JVM 动态加载 class 的特性,我们才能在运行期根据条件加载不同的实现类。例如Commons Logging 总是优先使用 Log4j,只有当 Log4j 不存在时,才使用 JDK 的 logging。利用 JVM 动态加载特性,大致的实现代码如下:
// Commons Logging优先使用Log4j:
LogFactory factory = null;
if (isClassPresent("org.apache.logging.log4j.Logger")) {factory = createLog4j();
} else {factory = createJdkLog();
}boolean isClassPresent(String name) {try {Class.forName(name);return true;} catch (Exception e) {return false;}
}
这就是为什么我们只需要把 Log4j 的 jar 包放到 classpath 中,Commons Logging 就会自动使用 Log4j 的原因。
访问字段
Class 类提供了以下几个方法来获取字段:
Field getField(name)
:根据字段名获取某个 public 的 field(包括父类)Field getDeclaredField(name)
:根据字段名获取当前类的某个 field(不包括父类)Field[] getFields()
:获取所有 public 的 field(包括父类)Field[] getDeclaredFields()
:获取当前类的所有 field(不包括父类)
// reflection
public class Main {public static void main(String[] args) throws Exception {Class stdClass = Student.class;// 获取public字段"score":System.out.println(stdClass.getField("score"));// 获取继承的public字段"name":System.out.println(stdClass.getField("name"));// 获取private字段"grade":System.out.println(stdClass.getDeclaredField("grade"));}
}class Student extends Person {public int score;private int grade;
}class Person {public String name;
}
上述代码分别获取 public 字段、继承的 public 字段以及 private 字段,打印出的 Field 对象类似:
public int Student.score
public java.lang.String Person.name
private int Student.grade
一个 Field 对象包含了一个字段的所有信息:
getName()
:返回字段名称,例如 "name";getType()
:返回字段类型,也是一个 Class 实例,例如 String.class;getModifiers()
:返回字段的修饰符,它是一个 int,不同的 bit 表示不同的含义。
获取字段定义信息
Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false
获取字段值
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
反射是一种非常规的用法。使用反射,首先代码非常繁琐,其次它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。
此外,setAccessible(true)
可能会失败。如果 JVM 运行期存在 SecurityManager,那么它会根据规则进行检查,有可能阻止 setAccessible(true)
。例如某个 SecurityManager 可能不允许对 java 和 javax 开头的 package 的类调用 setAccessible(true)
,这样可以保证 JVM 核心库的安全。
设置字段值
Person p = new Person("Xiao Ming");
System.out.println(p.getName()); // "Xiao Ming"
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
f.set(p, "Xiao Hong");
System.out.println(p.getName()); // "Xiao Hong"
调用方法
Class 类提供了以下几个方法来获取 Method:
Method getMethod(name, Class...)
:获取某个 public 的 Method(包括父类)Method getDeclaredMethod(name, Class...)
:获取当前类的某个 Method(不包括父类)Method[] getMethods()
:获取所有 public 的 Method(包括父类)Method[] getDeclaredMethods()
:获取当前类的所有 Method(不包括父类)
// reflection
public class Main {public static void main(String[] args) throws Exception {Class stdClass = Student.class;// 获取public方法getScore,参数为String:System.out.println(stdClass.getMethod("getScore", String.class));// 获取继承的public方法getName,无参数:System.out.println(stdClass.getMethod("getName"));// 获取private方法getGrade,参数为int:System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));}
}class Student extends Person {public int getScore(String type) {return 99;}private int getGrade(int year) {return 1;}
}class Person {public String getName() {return "Person";}
}
上述代码分别获取 public 方法、继承的 public 方法以及 private 方法,打印出的 Method 类似:
public int Student.getScore(java.lang.String)
public java.lang.String Person.getName()
private int Student.getGrade(int)
一个 Method 对象包含一个方法的所有信息:
getName()
:返回方法名称,例如:"getScore";getReturnType()
:返回方法返回值类型,也是一个 Class 实例,例如:String.class;getParameterTypes()
:返回方法的参数类型,是一个 Class 数组,例如:{String.class, int.class};getModifiers()
:返回方法的修饰符,它是一个 int,不同的 bit 表示不同的含义。
当我们获取到一个 Method 对象时,就可以对它进行调用。
// reflection
import java.lang.reflect.Method;public class Main {public static void main(String[] args) throws Exception {// String对象:String s = "Hello world";// 获取String substring(int)方法,参数为int:Method m = String.class.getMethod("substring", int.class);// 在s对象上调用该方法并获取结果:String r = (String) m.invoke(s, 6);// 打印调用结果:System.out.println(r); // "world"}
}
对 Method 实例调用 invoke 就相当于调用该方法,invoke 的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。
调用静态方法时,由于无需指定实例对象,所以 invoke 方法传入的第一个参数永远为 null。
// 获取Integer.parseInt(String)方法,参数为String:
Method m = Integer.class.getMethod("parseInt", String.class);
// 调用该静态方法并获取结果:
Integer n = (Integer) m.invoke(null, "12345");
// 打印调用结果:
System.out.println(n);
和 Field 类似,对于非 public 方法,我们虽然可以通过 Class.getDeclaredMethod()
获取该方法实例,但直接对其调用将得到一个 IllegalAccessException。为了调用非 public 方法,我们通过 Method.setAccessible(true)
允许其调用:
// reflection
import java.lang.reflect.Method;public class Main {public static void main(String[] args) throws Exception {Person p = new Person();Method m = p.getClass().getDeclaredMethod("setName", String.class);m.setAccessible(true);m.invoke(p, "Bob");System.out.println(p.name);}
}class Person {String name;private void setName(String name) {this.name = name;}
}
此外 setAccessible(true)
可能会失败。如果 JVM 运行期存在 SecurityManager,那么它会根据规则进行检查,有可能阻止 setAccessible(true)
。例如某个 SecurityManager 可能不允许对 java 和 javax 开头的 package 的类调用 setAccessible(true)
,这样可以保证 JVM 核心库的安全。
调用构造方法
我们通常使用 new 操作符创建新的实例:Person p = new Person();
。如果通过反射来创建新的实例,可以调用 Class 提供的 newInstance()方法:Person p = Person.class.newInstance();
。
调用 Class.newInstance()
的局限是,它只能调用该类的 public 无参数构造方法。如果构造方法带有参数,或者不是 public,就无法直接通过 Class.newInstance()
来调用。
为了调用任意的构造方法,Java 的反射 API 提供了 Constructor 对象,它包含一个构造方法的所有信息,可以创建一个实例。
import java.lang.reflect.Constructor;public class Main {public static void main(String[] args) throws Exception {// 获取构造方法Integer(int):Constructor cons1 = Integer.class.getConstructor(int.class);// 调用构造方法:Integer n1 = (Integer) cons1.newInstance(123);System.out.println(n1);// 获取构造方法Integer(String)Constructor cons2 = Integer.class.getConstructor(String.class);Integer n2 = (Integer) cons2.newInstance("456");System.out.println(n2);}
}
通过 Class 实例获取 Constructor 的方法如下:
getConstructor(Class...)
:获取某个 public 的 Constructor;getDeclaredConstructor(Class...)
:获取某个 Constructor;getConstructors()
:获取所有 public 的 Constructor;getDeclaredConstructors()
:获取所有 Constructor。
注意 Constructor 总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。调用非 public 的 Constructor 时,必须首先通过 setAccessible(true)
设置允许访问。setAccessible(true)
可能会失败。
获取继承关系
Class i = Integer.class;
Class n = i.getSuperclass();
System.out.println(n);
Class o = n.getSuperclass();
System.out.println(o);
System.out.println(o.getSuperclass());
当我们获取到某个 Class 对象时,实际上就获取到了一个类的类型。可以看到,Integer 的父类类型是 Number,Number 的父类是 Object,Object 的父类是 null。除 Object 外,其他任何非 interface 的 Class 都必定存在一个父类类型。
当我们判断一个实例是否是某个类型时,正常情况下,使用 instanceof 操作符:
Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true
如果是两个 Class 实例,要判断一个向上转型是否成立,可以调用 isAssignableFrom():
Integer.class.isAssignableFrom(Integer.class); // true,因为 Integer 可以赋值给 Integer
Number.class.isAssignableFrom(Integer.class); // true,因为 Integer 可以赋值给 Number
Object.class.isAssignableFrom(Integer.class); // true,因为 Integer 可以赋值给 Object
Integer.class.isAssignableFrom(Number.class); // false,因为 Number 不能赋值给 Integer
获取 interface
由于一个类可能实现一个或多个接口,通过 Class 我们就可以查询到实现的接口类型。
Class s = Integer.class;
Class[] is = s.getInterfaces();
for (Class i : is) {System.out.println(i);
}
运行上述代码可知,Integer 实现的接口有:java.lang.Comparable
、java.lang.constant.Constable
、java.lang.constant.ConstantDesc
。要特别注意:getInterfaces()
只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型。如果一个类没有实现任何 interface,那么 getInterfaces()
返回空数组。
动态代理
所有 interface 类型的变量总是通过某个实例向上转型并赋值给接口类型变量的。Java 标准库提供了一种动态代理的机制:可以在运行期动态创建某个 interface 的实例。JDK 提供的动态创建接口对象的方式,就叫动态代理。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;public class Main {public static void main(String[] args) {// 定义一个 InvocationHandler 实例,负责实现接口的方法调用InvocationHandler handler = new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(method);if (method.getName().equals("morning")) {System.out.println("Good morning, " + args[0]);}return null;}};// 通过 Proxy.newProxyInstance() 创建 interface 实例,将返回的 Object 强制转型为接口Hello hello = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(), // 传入ClassLoadernew Class[] { Hello.class }, // 传入要实现的接口handler); // 传入处理调用方法的InvocationHandlerhello.morning("Bob");}
}interface Hello {void morning(String name);
}
动态代理实际上是 JVM 在运行期动态创建 class 字节码并加载的过程。
注解
注解是放在 Java 源码的类、方法、字段、参数前的一种特殊“注释”。注释会被编译器直接忽略,注解则可以被编译器打包进入 class 文件,因此注解是一种用作标注的“元数据”。
@Resource("hello")
public class Hello {@Injectint n;@PostConstructpublic void hello(@Param String name) {System.out.println(name);}@Overridepublic String toString() {return "Hello";}
}
注解的作用
从 JVM 的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。
Java 的注解可以分为三类:
- 第一类是由编译器使用的注解,例如
@Override
作用让编译器检查该方法是否正确地实现了覆写;@SuppressWarnings
作用告诉编译器忽略此处代码产生的警告。这类注解不会被编译进入.class
文件,它们在编译后就被编译器扔掉了。 - 第二类是由工具处理
.class
文件使用的注解,比如有些工具会在加载 class 的时候,对 class 做动态修改,实现一些特殊的功能。这类注解会被编译进入.class
文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。 - 第三类是在程序运行期能够读取的注解,它们在加载后一直存在于 JVM 中,这也是最常用的注解。例如一个配置了
@PostConstruct
的方法会在调用构造方法后自动被调用(这是 Java 代码读取该注解实现的功能,JVM 并不会识别该注解)。
定义一个注解时还可以定义配置参数。配置参数可以包括所有基本类型、String、枚举类型、[基本类型|String|Class|枚举]的数组。配置参数必须是常量,此限制保证了注解在定义时就已经确定了每个参数的值。注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。
此外大部分注解会有一个名为 value 的配置参数,对此参数赋值,可以只写常量,相当于省略了 value 参数。如果只写注解,相当于全部使用默认值。
public class Hello {@Check(min=0, max=100, value=55)public int n;@Check(value=99)public int p;@Check(99) // @Check(value=99)public int x;@Checkpublic int y;
}
@Check
就是一个注解。@Check(min=0, max=100, value=55)
明确定义了三个参数,@Check(value=99)
只定义了一个 value 参数,它实际上和 @Check(99)
是完全一样的。@Check
表示所有参数都使用默认值。
元注解
有一些注解可以修饰其他注解,这些注解就称为元注解。Java 标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。
@Target
最常用的元注解是 @Target
。使用 @Target
可以定义 Annotation 能够被应用于源码的哪些位置:
- 类或接口:ElementType.TYPE
- 字段:ElementType.FIELD
- 方法:ElementType.METHOD
- 构造方法:ElementType.CONSTRUCTOR
- 方法参数:ElementType.PARAMETER
例如定义注解 @Report
可用在方法上,我们必须添加一个 @Target(ElementType.METHOD)
:
@Target(ElementType.METHOD)
public @interface Report {int type() default 0;String level() default "info";String value() default "";
}
定义注解 @Report
可用在方法或字段上,可以把 @Target
注解参数变为数组{ ElementType.METHOD, ElementType.FIELD }
:
@Target({ElementType.METHOD,ElementType.FIELD
})
public @interface Report {...
}
@Retention
元注解 @Retention
定义了 Annotation 的生命周期:
- 仅编译期:RetentionPolicy.SOURCE
- 仅 class 文件:RetentionPolicy.CLASS
- 运行期:RetentionPolicy.RUNTIME
如果 @Retention
不存在,则该 Annotation 默认为 CLASS。因为通常我们自定义的 Annotation 都是 RUNTIME,所以务必要加上@Retention(RetentionPolicy.RUNTIME)
这个元注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {int type() default 0;String level() default "info";String value() default "";
}
@Repeatable
使用 @Repeatable
这个元注解可以定义 Annotation 是否可重复。这个注解应用不是特别广泛。
@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {int type() default 0;String level() default "info";String value() default "";
}@Target(ElementType.TYPE)
public @interface Reports {Report[] value();
}
经过 @Repeatable
修饰后,在某个类型声明处,就可以添加多个 @Report
注解:
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
@Inherited
使用 @Inherited
定义子类是否可继承父类定义的 Annotation。@Inherited
仅针对 @Target(ElementType.TYPE)
类型的 annotation 有效,并且仅针对 class 的继承,对 interface 的继承无效:
@Inherited
@Target(ElementType.TYPE)
public @interface Report {int type() default 0;String level() default "info";String value() default "";
}
在使用的时候,如果一个类用到了 @Report
,则它的子类默认也定义了该注解
@Report(type=1)
public class Person {
}
定义注解
Java 语言使用 @interface
语法来定义注解。注解的参数类似无参数方法,可以用 default 设定一个默认值(强烈推荐)。最常用的参数应当命名为 value。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {int type() default 0;String level() default "info";String value() default "";
}
步骤:
- 第一步,用
@interface
定义注解 - 第二步,添加参数、默认值
- 第三步,用元注解配置注解:
其中必须设置 @Target
和 @Retention
,@Retention
一般设置为 RUNTIME,因为我们自定义的注解通常要求在运行期读取。一般情况下,不必写 @Inherited
和 @Repeatable
。
处理注解
Java 的注解本身对代码逻辑没有任何影响。如何使用注解完全由工具决定。
根据 @Retention
的配置:
- SOURCE 类型的注解在编译期就被丢掉了;
- CLASS 类型的注解仅保存在 class 文件中,它们不会被加载进 JVM;
- RUNTIME 类型的注解会被加载进 JVM,并且在运行期可以被程序读取。
SOURCE 类型的注解主要由编译器使用,我们一般只使用不编写。CLASS 类型的注解主要由底层工具库使用,涉及到 class 的加载,一般我们很少用到。只有 RUNTIME 类型的注解不但要使用,还经常需要编写。
Java 提供的使用反射 API 读取 Annotation 的方法包括判断某个注解是否存在于 Class、Field、Method 或 Constructor 的方法:Class.isAnnotationPresent(Class)
、Field.isAnnotationPresent(Class)
、Method.isAnnotationPresent(Class)
、Constructor.isAnnotationPresent(Class)
。使用反射 API 读取 Annotation的方法:Class.getAnnotation(Class)
、Field.getAnnotation(Class)
、Method.getAnnotation(Class)
、Constructor.getAnnotation(Class)
。
使用反射 API 读取 Annotation 有两种方法。方法一是先判断 Annotation 是否存在,如果存在,就直接读取:
Class cls = Person.class;
if (cls.isAnnotationPresent(Report.class)) {Report report = cls.getAnnotation(Report.class);int type = report.type();String level = report.level();
}
第二种方法是直接读取 Annotation,如果 Annotation 不存在,将返回 null:
Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {...
}
读取方法、字段和构造方法的 Annotation 和 Class 类似。但要读取方法参数的 Annotation 就比较麻烦一点,因为方法参数本身可以看成一个数组,而每个参数又可以定义多个注解,所以一次获取方法参数的所有注解就必须用一个二维数组来表示。
要读取方法参数的注解,我们先用反射获取 Method 实例,然后读取方法参数的所有注解:
// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {if (anno instanceof Range r) { // @Range注解r.max();}if (anno instanceof NotNull n) { // @NotNull注解//}
}
使用注解
注解如何使用,完全由程序自己决定。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {int min() default 0;int max() default 255;
}
在某个 JavaBean 中,我们可以使用该注解:
public class Person {@Range(min=1, max=20)public String name;@Range(max=10)public String city;
}
定义了注解对程序逻辑没有任何影响。我们必须自己编写代码来使用注解。我们编写一个 Person 实例的检查方法,它可以检查 Person 实例的 String 字段长度是否满足 @Range
的定义:
void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {// 遍历所有 Field:for (Field field : person.getClass().getFields()) {// 获取 Field 定义的 @Range:Range range = field.getAnnotation(Range.class);// 如果 @Range 存在:if (range != null) {// 获取 Field 的值:Object value = field.get(person);// 如果值是 String:if (value instanceof String s) {// 判断值是否满足 @Range 的 min/max:if (s.length() < range.min() || s.length() > range.max()) {throw new IllegalArgumentException("Invalid field: " + field.getName());}}}}
}
这样一来,我们通过 @Range
注解,配合 check()
方法,就可以完成 Person 实例的检查。注意检查逻辑完全是我们自己编写的,JVM 不会自动给注解添加任何额外的逻辑。