《深入理解Java虚拟机》 类 & Class对象/反射

———— JDK 1.7
words: 1.7k    views:    time: 6min

在类加载成功之后,便以Class对象的形式保存在内存中。可以将Class对象理解成实例对象的元数据,通过Class对象中描述的类型信息,便可以创建对应的实例对象,从而进行方法调用,以及字段修改。那么,便可以通过反射根据运行时获取的类型信息来创建实例,而且可以跳过访问权限的限制访问字段和方法

1. Class

Class对象被用来描述某种类型,但是类型有很多种,那么如何区分描述的是哪种类型?其实觉得这里可以将Class对象理解成一个类型属性的容器,其中包括了构造器Constructor,字段Field,方法Method,注解等信息,而至于是哪种类型,则可以通过泛型来表示

  • 关于Class对象的获取
  1. 如果在编译期间类型已知,则可以直接通过类型字面量获取,比如Class<Object> clazz = Object.class;,不过使用类型字面量的方式并不会触发Class对象的初始化,它被延迟到首次对静态域,或者静态方法的引用(构造器也是静态调用)

  2. 如果已经存在类型对应的实例,则可以直接通过实例调用Object提供的getClass()方法,因为在使用类型信息创建对象时,会在对象头中(直接指针的方式)保留指向类型信息的引用

  3. 如果上面信息都没有,那么可以通过类的全限定名由类加载器进行加载,Class提供了接口Class.forName(),由其帮忙调用加载器进行加载,当然也可以自己主动指定加载器。但是,这里的加载就会立即进行初始化了(如果没有加载过)。另外,对于加载器而言,它无法知道加载的是那种具体的类型信息,只知道是某种确定的类型,因此只能用Class<?>来接收

2. 反射

反射是Java作为动态语言的手段或者体现,为Java赋予了更多的灵活性,它是很多框架(比如spring、mybatis),以及动态代理实现的基础。在Class对象中,我们可以拿到类型的种种信息,比如构造器,字段和方法等,而这些信息也各自定义了对应的类型来表示,其中定义了各自可以支持的操作和使用方式,当然这些信息类型以及Class对象自己所属的类型都由虚拟机自身所定义。

下面简单整理一些常见的反射使用场景

2.1. Constructor

最常见的就是通过反射创建实例,如果是默认构造器,则可以直接通过Calss对象提供的newInstance()创建,否则需要获取对应的构造器,而构造器通过Constructor来封装,Class对象提供了getConstructors()以及getDeclaredConstructor()接口来获取类型实例的构造器列表,区别在于获取的是public还是所有的,有了构造器之后便可以调用其newInstance(..)来创建对应的实例

2.2. Method

一般情况下,是先有对象,然后再调用其方法,但Method提供了invoke(...)接口,它本身表示一个方法实例,但它可以帮忙通过指定的实例来调用。同样的,Class对象也提供了getMethods()以及getDeclaredMethods()接口来获取方法列表

2.3. Field

对于字段也是类似的,获取到类型中定义的字段Field之后,便可以为对应的实例进行赋值,但是要求先判断字段本身的所属类型,另外如果字段本身并不可见,则先要通过setAccessible(true)来设置其可访问

2.4. 注解

对于类Class、方法Method、以及字段Field上的注解,各自都提供了getAnnotation(XX.class)接口来获取,注解可以简单理解成一种附属在字段或方法上的只读标记,通常这些注解信息可以表示某种处理方式获取其它含义。

2.5. 泛型

其实,相比于类型Class,还有更高层的抽象Type,它包括原始类型、参数化类型、数组类型、类型变量以及基本类型等

java.lang.reflect.Type
1
2
3
4
5
6
public interface Type {

default String getTypeName() {
return toString();
}
}

对于参数化类型,可以通过反射获取其类型参数,比如:

1
2
3
public class Person<T> {

}
1
2
Class clazz = Person.class;
TypeVariable type = clazz.getTypeParameters()[0]; // T

在继承参数化类型时,可以选择保留或者使用具体类型,比如

1
2
3
public class Man<T> extends Person<T> {

}

或者

1
2
3
public class Man extends Person<String> {

}

此时可能希望通过父类获取其实际使用的参数类型

1
2
3
4
5
Type type = clazz.getGenericSuperclass();
if(type instanceof ParameterizedType){
ParameterizedType p = (ParameterizedType)type;
Type t = p.getActualTypeArguments()[0]; // T 或者 java.lang.String
}

类似的,对于泛型接口的实现也一样,比如HashMap中判断目标实例是否为Comparable并且符号自限定类型的判断

java.util.HashMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
if ((c = x.getClass()) == String.class){ // 如果是String直接返回
return c;
}

if ((ts = c.getGenericInterfaces()) != null) { // 获取并遍历所有实现的接口
for (int i = 0; i < ts.length; ++i) {
if (((t = ts[i]) instanceof ParameterizedType) // 是参数化类型接口
&& ((p = (ParameterizedType)t).getRawType() == Comparable.class) // 接口是Comparable
&& (as = p.getActualTypeArguments()) != null // 类型参数不为空
&& as.length == 1 // 类型参数只有一个
&& as[0] == c) // 类型参数为自己
return c;
}
}
}
return null;
}

2.6. CallerSensitive

在反射Api中,即java.lang.reflect下面,经常会看到注解@CallerSensitive,其目的是找到真正的调用者,但由于嵌套调用的关系,它需要忽略掉api自身的调用,因此就给自己定义的方法添加@CallerSensitive进行标记,这样在检查调用链时发现@CallerSensitive就直接跳过,直到发现真正的调用者。

以前的做法是检查固定深度,这样有一个问题,如果本来调用者没有访问权限,他可以修改调用链深度来骗过检查。比如:调用者->反射1->反射2 这样的调用链上,反射2检查权限时如果检查深度固定为1,那么看到的将是反射1的类,这就被欺骗了,而反射相关的类通常有很高的权限,容易导致安全漏洞。


参考:

  1. 《深入理解java虚拟机》