反射
反射
为什么通过反射创建对象要比使用new创建对象慢?
尽管在平时的业务开发中,我们很少会用到反射、注解、动态代理这些比较高级的Java语法,但是,在框架开发中,它们却非常常用,可以说是支撑框架开发的核心技术。比如,我们常用的Spring框架,其中的IOC就是基于反射实现的,AOP就是基于动态代理实现的,配置就是基于注解实现的。尽管在业务开发中,我们不常用到它们,但是,要想阅读开源框架的源码,掌握这些技术是必不可少的。接下来,我们就来讲讲反射、注解、动态代理。本节我们重点讲下反射。
一、反射的作用
反射的作用主要有3个:创建对象、执行方法、获取类信息。
1)创建对象
在编写代码的时候,我们通过new语句用来创建对象。代码中有多少条new语句,JVM就会创建多少个对象。但是,并不是所有的对象的创建,都是在编写代码时事先知道的。如果在代码的运行过程中,我们需要根据配置、输入、执行结果等,动态创建一些额外的对象,这个时候我们无法使用new语句了。我们需要有一种新的方法,在程序运行期间,动态地告知JVM去创建某个类的对象。这种方法就是反射。
不管是new还是反射,对象的创建都是在运行时进行的,不过申请创建对象的时机却是不同的。通过new来创建对象,其创建对象的需求是在代码编写时确定的,而通过反射来创建对象,其创建对象的需求是在运行时是确定的。因此,我们把通过new语句的方式来创建对象叫做静态申请对象创建,我们把通过反射的方式来创建对象叫做动态申请对象创建。
2)执行方法
除了在程序运行期间动态申请对象创建之外,程序还可以动态申请执行方法。跟创建对象类似,尽管执行方法总是发生在运行时,但是申请执行方法的时机却可以不同。一般来讲,程序会执行哪些方法,在代码编写时就确定了。但是,如果在运行时,额外申请新的要执行的方法,这个时候,就只能依靠反射来实现了。稍后讲到的动态代理,实际上,就是依赖反射可以动态执行方法来实现的。
不管是反射创建对象,还是执行方法,实际上,跟普通的对象创建和方法执行,本质上没有太大区别。只不过是告知JVM的时机和方式不同而已。
3)获取类信息
除了创建对象、执行方法之外,反射还能够获取对象的类信息,包括类中的构造函数、方法、成员变量等信息。稍后要讲到的注解,实际上,就是依赖反射的这个作用。
二、反射的用法
实现上述反射的这3个作用需要4个类:Class、Method、Constructor、Field,这也是反射所涉及的核心类,接下来,我们依次来介绍一下这4个类。
1)Class
Class跟关键字class容易混淆,Class实际上跟Person、String等一样,也是一个类,只是其比较特殊,存储的是类的信息。Class类提供了大量的方法,可以获取类的信息,比如获取类中的方法,获取构造函数,获取成员变量等。我们将重要的常用到方法罗列如下,当然,你也可以查看java.lang.Class源码来了解更多细节。
// 获取类信息
public static Class<?> forName(String className);
// 获取类名
public String getName();
public String getSimpleName();
// 获取父类信息
public native Class<? super T> getSuperclass();
// 获取package信息
public Package getPackage()
// 获取接口信息
public Class<?>[] getInterfaces();
// 获取成员的变量,包含私有成员变量,不包含父类成员变量
public Field[] getDeclaredFields();
public Field getDeclaredField(String name);
// 获取成员变量,只包含公有成员变量,包含父类成员变量
public Field[] getFields();
public Field getField(String name);
// 获取类的方法,包括私有方法,不包含父类方法
public Method[] getDeclaredMethods();
public Method getDeclaredMethod(String name, Class<?>... parameterTypes);
// 获取类的方法,只包含公有方法,包含父类方法
public Method[] getMethods();
public Method getMethod(String name, Class<?>... parameterTypes);
// 获取构造函数,只包含公共构造函数
public Constructor<?>[] getConstructors();
public Constructor<T> getConstructor(Class<?>... parameterTypes);
// 获取构造函数,包含私有构造函数
public Constructor[] getDeclaredConstructors();
public Constructor getDeclaredConstructor(Class... parameterTypes);
// 获取类上的注解
public Annotation[] getAnnotations();
除了获取类信息的方法之外,Class类还提供了方法来创建对象,如下所示。
// 创建类对象
public T newInstance();
一般来讲,我们有以下3种方式来创建Class类对象。
// 方法一:使用forName()+类名全称
Class<?> clazz = Class.forName("com.wz.demo.Student");
// 方法二
Class<?> clazz = Student.class;
Class<Student> clazz = Student.class;
// 方法三
Class<?> clazz = student.getClass();
从上述代码,我们可以发现,实际上,Class类是一个泛型类。如果我们无法提前知道获取的类的信息是哪个类的,那么我们就可以使用?通配符来具体化泛型类。如果我们可以明确获取的是哪个类的信息,那么我们可以直接使用具体类型具体化泛型类。不过,方法一、方法三并不能像下面这样具体化Class类,因为forName()函数和getClass()函数在函数定义中的返回值本来就是Class<?>,Class<?>类型的返回值不能赋值给Class<Student>。
//不正确的使用方法
Class<Student> clazz = Class.forName("com.wz.demo.Student");
Class<Student> clazz = student.getClass();
2)Constructor
Constructor用来存储构造函数的信息。如下所示。
// 构造函数所包含的信息
// 在Constructor中,以下信息都有相应的方法来获取
public final class Constructor<T> extends Executable {
private Class<T> clazz;
private int slot;
private Class<?>[] parameterTypes;
private Class<?>[] exceptionTypes;
private int modifiers;
// Generics and annotations support
private transient String signature;
// generic info repository; lazily initialized
private transient ConstructorRepository genericInfo;
private byte[] annotations;
private byte[] parameterAnnotations;
}
Constructor类也提供了一些方法来获取以上信息,这里我们就不一一列举了,你可以查看java.lang.reflect.Constructor类源码去自行了解。这里介绍一下Constructor类中常用的newInstance()方法,如下所示。
public T newInstance(Object ... initargs);
通过newInstance()方法,我们可以调用构造函数来创建对象。你应该也已经注意到,Class类中也包含newInstance()方法。区别在于,Class类上的newInstance()方法只能通过无参构造函数来创建对象,如果想要使用有参构造函数创建对象,我们需要先获取对应的Constructor类对象,然后再调用其上的newInstance()方法。稍后会有代码示例。
3)Method
Method存储的是方法的信息。如下所示。
public final class Method extends Executable {
private Class<?> clazz;
private int slot;
// This is guaranteed to be interned by the VM in the 1.4
// reflection implementation
private String name;
private Class<?> returnType;
private Class<?>[] parameterTypes;
private Class<?>[] exceptionTypes;
private int modifiers;
// Generics and annotations support
private transient String signature;
// generic info repository; lazily initialized
private transient MethodRepository genericInfo;
private byte[] annotations;
private byte[] parameterAnnotations;
private byte[] annotationDefault;
private volatile MethodAccessor methodAccessor;
}
同时,Method类也提供了大量方法来获取以上信息,这里我们也不一一罗列了。感兴趣的话,你可以查看java.lang.reflect.Method类的源码。这里我们介绍一下常用的invoke()方法,如下所示,调用此方法可以执行对应方法。稍后会有代码示例。
public Object invoke(Object obj, Object... args);
4)Field
Filed用来存储成员变量的信息,如下所示。同样,Field类也提供了大量方法来获取以下信息,这里我们也不一一罗列了。感兴趣的话,你可以查看java.lang.reflect.Field类的源码。
public final class Field extends AccessibleObject implements Member {
private Class<?> clazz;
private int slot;
// This is guaranteed to be interned by the VM in the 1.4
// reflection implementation
private String name;
private Class<?> type;
private int modifiers;
// Generics and annotations support
private transient String signature;
// generic info repository; lazily initialized
private transient FieldRepository genericInfo;
private byte[] annotations;
// Cached field accessor created without override
private FieldAccessor fieldAccessor;
// Cached field accessor created with override
private FieldAccessor overrideFieldAccessor;
}
三、反射攻击
上面罗列了Class、Constructor、Method、Field中的常用方法,实际上,在Constructor、Method、Field类中,包含一个公共的方法,能够改变构造函数、方法、成员变量的访问权限,
public void setAccessible(boolean flag);
利用这个方法,我们可以将私有的构造函数、方法、成员变量设置为可访问的,这样就可以超越权限限制,在代码中访问私有的构造函数、方法和成员变量。示例代码如下所示。
public class Demo {
public static class Person {
private int age;
private Person() {}
private void print() {
System.out.println(this.age);
}
}
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("com.wz.demo.Demo$Person");
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Person pobj = (Person) constructor.newInstance();
Field field = clazz.getDeclaredField("age");
field.setAccessible(true);
field.set(pobj, 10);
Method method = clazz.getDeclaredMethod("print");
method.setAccessible(true);
method.invoke(pobj);
}
}
在《设计模式之美》中,我们有讲到单例模式。单例模式只允许单例类实例化一个对象。单例模式有很多实现方式,其中一种实现方法如下所示,我们通过将构造函数设置为私有的,来禁止外部代码创建新的对象。
public class IdGenerator { //单例
private AtomicLong id = new AtomicLong(0);
private static final IdGenerator instance = new IdGenerator();
private IdGenerator() {}
public static IdGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
不过,通过反射,我们仍然可以绕开代码中的访问权限控制,调用私有的构造函数,实例化新的对象,如下所示,这种打破单例类只能实例化一个对象的限制的情况,就叫做反射攻击。
public class Demo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("com.wz.demo.IdGenerator");
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
IdGenerator idGenerator = (IdGenerator) constructor.newInstance();
System.out.println(idGenerator.getId());
}
}
四、反射的应用
在《设计模式之美》中,我们讲到,Spring可以作为一种IOC容器(也叫做DI容器,依赖注入容器),实际上,IOC容器就是一个大的工厂类,负责在程序启动时,根据配置,事先创建好对象。当应用程序需要使用某个对象时,直接从容器中获取即可。
在普通的工厂模式中,工厂类要创建哪个对象是事先确定好的,并且是写死在工厂类代码中的。作为一个通用的框架来说,框架代码跟应用代码应该是高度解耦的,IOC容器事先并不知道应用会创建哪些对象,不可能把某个应用要创建的对象写死在框架代码中。应用程序通过配置文件,定义好需要创建的对象。IOC容器读取配置文件,并将每个要创建的对象信息,解析为一定的内存结构:BeanDefinition,然后根据BeanDefinition中的信息,通过反射创建对象。
对于IOC容器的完整实现,我们在《设计模式之美》中有详细介绍。这里,我们重点展示跟反射有关的部分,也就是根据BeanDefinition创建对象。代码如下所示。在下列代码中,我们使用Class.forName()来创建对象,对于无参构造,我们使用Class对象上的newInstance()来创建对象,对于有参构造,我们先获取对应的Constructor对象,然后调用Constructor对象上的newInstance()来创建对象。
public class BeansFactory {
private ConcurrentHashMap<String, Object> singletonObjects
= new ConcurrentHashMap<>();
private ConcurrentHashMap<String, BeanDefinition> beanDefinitions
= new ConcurrentHashMap<>();
public void addBeanDefinitions(List<BeanDefinition> beanDefinitionList) {
for (BeanDefinition beanDefinition : beanDefinitionList) {
this.beanDefinitions.putIfAbsent(beanDefinition.getId(), beanDefinition);
}
for (BeanDefinition beanDefinition : beanDefinitionList) {
if (beanDefinition.isLazyInit() == false && beanDefinition.isSingleton()) {
createBean(beanDefinition);
}
}
}
public Object getBean(String beanId) {
BeanDefinition beanDefinition = beanDefinitions.get(beanId);
if (beanDefinition == null) {
throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId);
}
return createBean(beanDefinition);
}
@VisibleForTesting
protected Object createBean(BeanDefinition beanDefinition) {
if (beanDefinition.isSingleton()
&& singletonObjects.contains(beanDefinition.getId())) {
return singletonObjects.get(beanDefinition.getId());
}
Object bean = null;
try {
Class beanClass = Class.forName(beanDefinition.getClassName());
List<BeanDefinition.ConstructorArg> args = beanDefinition.getConstructorArgs();
if (args.isEmpty()) {
bean = beanClass.newInstance();
} else {
Class[] argClasses = new Class[args.size()];
Object[] argObjects = new Object[args.size()];
for (int i = 0; i < args.size(); ++i) {
BeanDefinition.ConstructorArg arg = args.get(i);
if (!arg.getIsRef()) {
argClasses[i] = arg.getType();
argObjects[i] = arg.getArg();
} else {
BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg());
if (refBeanDefinition == null) {
throw new NoSuchBeanDefinitionException(arg.getArg());
}
argClasses[i] = Class.forName(refBeanDefinition.getClassName());
argObjects[i] = createBean(refBeanDefinition);
}
}
bean = beanClass.getConstructor(argClasses).newInstance(argObjects);
}
} catch (ClassNotFoundException | IllegalAccessException
| InstantiationException | NoSuchMethodException
| InvocationTargetException e) {
throw new BeanCreationFailureException(
"Create Bean failed: " + beanDefinition.getId(), e);
}
if (bean != null && beanDefinition.isSingleton()) {
singletonObjects.putIfAbsent(beanDefinition.getId(), bean);
return singletonObjects.get(beanDefinition.getId());
}
return bean;
}
}
五、反射的原理
前面我们提到,使用反射来创建对象,跟使用new创建对象,大体的流程是一样,只不过向JVM申请创建对象的方式不同而已。但是,我们还经常听说,使用反射来创建对象,要比使用new创建对象,要慢很多。那这到底又是为什么呢?接下来,我们先做个实验来验证一下情况是否属实。测试代码如下所示。
public class Demo24_1 {
public static class C {
public void f() {}
}
public static void main(String[] args) throws Exception {
// 使用new创建对象
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; ++i) {
C c = new C();
}
System.out.println(System.currentTimeMillis()-start);
// 使用反射创建对象
start = System.currentTimeMillis();
for (int i = 0; i < 10000000; ++i) {
Class<?> clazz = C.class;
Object obj = clazz.newInstance();
}
System.out.println(System.currentTimeMillis()-start);
}
}
执行上述代码,得到结果为:通过new创建对象的耗时为3ms,通过反射创建对象的耗时为31ms,差不多10倍的差距。尽管耗时有10倍的差距,但从耗时的绝对值上来看,通过反射创建1000万个对象,耗时才只有31ms,对于大部分应用程序来说都是可以接受的,绝大部分情况下,通过反射创建对象都不会是应用程序的性能瓶颈,我们不需要为反射带来的一丢丢性能损耗而担忧。
前面讲到,使用反射还可以动态的执行方法,那么,相比于直接执行方法,使用反射执行方法会不会也很慢呢?为了测试使用反射执行方法的性能,我们对上面的测试代码稍作修改,如下所示。
public class Demo24_1 {
public static class C {
public void f() {}
}
public static void main(String[] args) throws Exception {
// 普通方法调用
long start = System.currentTimeMillis();
C c = new C();
for (int i = 0; i < 10000000; ++i) {
c.f();
}
System.out.println(System.currentTimeMillis()-start);
// 使用反射执行方法
start = System.currentTimeMillis();
Class<?> clazz = C.class;
Object obj = clazz.newInstance();
for (int i = 0; i < 10000000; ++i) {
Method method = clazz.getMethod("f");
method.invoke(obj);
}
System.out.println(System.currentTimeMillis()-start);
}
}
执行上述代码,得到的结果为:普通方法调用的耗时为3ms,而使用反射执行方法的耗时为1259ms,有几百倍的差距。这个差距就比较大了。尽管性能差距如此大,但我们也不必为使用反射导致方法执行性能下降而担忧。这是为什么呢?
原因有二。其一是:使用反射执行方法,并不会让方法内部逻辑的执行速度变慢,只是增加了一些额外耗时而已,这部分额外的耗时是固定的,跟方法内部逻辑的复杂程度无关。其二是:1000万次方法调用才耗时1259ms,平均执行一次方法的增加的额外耗时为0.0001259ms,非常小,对于大部分方法来说,特别是一些包含IO操作的方法(比如访问数据库),方法本身内部逻辑执行的耗时远远大于使用反射而额外增加的耗时,因此,在大部分情况下,我也并不需要担心使用反射执行方法导致的一丢丢性能下降。
那么,相比普通的对象创建和执行,使用反射创建对象和执行方法,增加的额外耗时产生在哪里呢?
1)安全性检查
对于普通的对象创建和执行,大量的安全性检查,比如传入某个方法的数据类型必须与参数类型匹配、在某个对象上调用某个方法必须确保这个对象有这个方法,这些都是在编译时完成的,不占用运行时间,但是,对于反射,因为其是在运行时才确定创建什么对象、执行什么方法的,所以,安全性检查无法在编译时执行,只能在运行时真正创建创建、执行方法时再完成,那么这就会增加额外的运行时间。
2)类、方法查找
当我们使用反射创建对象或执行方法时,我们需要通过类名、方法名去查找对应的类或方法,而类名、方法名都是字符串,字符串匹配相对来说比较慢速。而正常情况下,代码经过编译之后,得到的字节码中,每个类和方法都会分配一个对应的编号,保存在常量池中,代码中所有出现类或方法的地方,都会被替换为编号。相比于通过类名、方法名字符串来查找类和方法,通过编号来查找对应的类或方法,显然要快得多。
我们再通过一个简单的例子进一步解释一下,代码如下所示。
public class Demo {
public static void main(String[] args) {
Demo d = new Demo();
f();
}
public static void f() {}
}
上述代码经过编译之后,得到的字节码如下所示,其中,常量池(Constant pool)中保存了各个类、方法的编号。类创建通过“new #编码”来实现,方法执行通过“invokespecial #编号”来实现。
public class Demo
minor version: 0
major version: 53
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // Demo
super_class: #5 // java/lang/Object
interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #5.#15 // java/lang/Object."<init>":()V
#2 = Class #16 // Demo
#3 = Methodref #2.#15 // Demo."<init>":()V
#4 = Methodref #2.#17 // Demo.f:()V
#5 = Class #18 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 f
#13 = Utf8 SourceFile
#14 = Utf8 Demo.java
#15 = NameAndType #6:#7 // "<init>":()V
#16 = Utf8 Demo
#17 = NameAndType #12:#7 // f:()V
#18 = Utf8 java/lang/Object
{
public Demo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Demo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: invokestatic #4 // Method f:()V
11: return
LineNumberTable:
line 3: 0
line 4: 8
line 5: 11
public static void f();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 8: 0
}
六、课后思考题
单例模式有多种实现方式,其他实现方式是否也存在可能被反射攻击的问题呢?