24、反射
尽管在平时的业务开发中,我们很少会用到反射、注解、动态代理这些比较高级的 Java 语法,但是在框架开发中它们却非常常用,可以说是支撑框架开发的核心技术
比如我们常用的 Spring 框架,其中的 IOC 就是基于反射实现的,AOP 就是基于动态代理实现的,配置就是基于注解实现的
尽管在业务开发中,我们不常用到它们,但是要想阅读开源框架的源码,掌握这些技术是必不可少的
接下来,我们就来讲讲反射、注解、动态代理,本节我们重点讲下反射
1、反射的作用
反射的作用主要有 3 个:创建对象、执行方法、获取类信息
1.1、创建对象
在编写代码的时候,我们通过 new 语句用来创建对象,代码中有多少条 new 语句,JVM 就会创建多少个对象,但并不是所有对象的创建,都是在编写代码时事先知道的
如果在代码的运行过程中,我们需要根据配置、输入、执行结果等,动态创建一些额外的对象,这个时候我们无法使用 new 语句了
我们需要有一种新的方法,在程序运行期间,动态的告知 JVM 去创建某个类的对象,这种方法就是反射
不管是 new 还是反射,对象的创建都是在运行时进行的,不过申请创建对象的时机却是不同的
- 通过 new 来创建对象:其创建对象的需求是在 "代码编写时" 确定的
- 通过反射来创建对象:其创建对象的需求是在 "运行时" 确定的
- 因此,我们把通过 new 语句的方式来创建对象叫做 "静态申请对象创建",我们把通过反射的方式来创建对象叫做 "动态申请对象创建"
1.2、执行方法
除了在程序运行期间动态申请对象创建之外,程序还可以动态申请执行方法
跟创建对象类似,尽管执行方法总是发生在运行时,但是申请执行方法的时机却可以不同
- 一般来讲,程序会执行哪些方法,在代码编写时就确定了
- 但如果在运行时,额外申请新的要执行的方法,这个时候,就只能依靠反射来实现了
稍后讲到的动态代理,实际上就是依赖反射可以动态执行方法来实现的
不管是反射创建对象,还是执行方法,实际上跟普通的对象创建和方法执行,本质上没有太大区别,只不过是告知 JVM 的时机和方式不同而已
1.3、获取类信息
除了创建对象、执行方法之外,反射还能够获取对象的类信息,包括类中的构造函数、方法、成员变量等信息
稍后要讲到的注解,实际上就是依赖反射的这个作用
2、反射的用法
实现上述反射的这 3 个作用需要 4 个类:Class、Constructor、Method、Field,这也是反射所涉及的核心类,接下来我们依次来介绍一下这 4 个类
2.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[] getFields();
public Field getField(String name);
// 获取成员的变量, 包含私有成员变量, 不包含父类成员变量
public Field[] getDeclaredFields();
public Field getDeclaredField(String name);
---------------------------------------------------------------------------
// 获取类的方法, 只包含公有方法, 包含父类方法
public Method[] getMethods();
public Method getMethod(String name, Class<?>... parameterTypes);
// 获取类的方法, 包括私有方法, 不包含父类方法
public Method[] getDeclaredMethods();
public Method getDeclaredMethod(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.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() 方法,稍后会有代码示例
2.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);
2.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;
}
3、反射攻击
上面罗列了 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 p = (Person) constructor.newInstance();
Field field = clazz.getDeclaredField("age");
field.setAccessible(true);
field.set(p, 10);
Method method = clazz.getDeclaredMethod("print");
method.setAccessible(true);
method.invoke(p);
}
}
在《设计模式之美》中,我们有讲到单例模式,单例模式只允许单例类实例化一个对象
单例模式有很多实现方式,其中一种实现方法如下所示,我们通过将构造函数设置为私有的,来禁止外部代码创建新的对象
// 单例
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());
}
}
4、反射的应用
在《设计模式之美》中,我们讲到,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;
}
}
5、反射的原理
前面我们提到,使用反射来创建对象,跟使用 new 创建对象,大体的流程是一样,只不过向 JVM 申请创建对象的方式不同而已
但是我们还经常听说,使用反射来创建对象,要比使用 new 创建对象要慢很多,那这到底又是为什么呢?
接下来,我们先做个实验来验证一下情况是否属实,测试代码如下所示
public class Demo {
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 创建对象的耗时为 2 ms,通过反射创建对象的耗时为 17 ms,差不多 10 倍的差距
尽管耗时有 10 倍的差距,但从耗时的绝对值上来看,通过反射创建 1000 万个对象,耗时才只有 17 ms,对于大部分应用程序来说都是可以接受的
绝大部分情况下,通过反射创建对象都不会是应用程序的性能瓶颈,我们不需要为反射带来的一丢丢性能损耗而担忧
前面讲到,使用反射还可以动态的执行方法,那么相比于直接执行方法,使用反射执行方法会不会也很慢呢?
为了测试使用反射执行方法的性能,我们对上面的测试代码稍作修改,如下所示
public class Demo {
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);
}
}
执行上述代码,得到的结果为:普通方法调用的耗时为 3 ms,而使用反射执行方法的耗时为 568 ms,有几百倍的差距,这个差距就比较大了
尽管性能差距如此大,但我们也不必为使用反射导致方法执行性能下降而担忧,这是为什么呢?
原因有二
- 其一是:使用反射执行方法,并不会让方法内部逻辑的执行速度变慢,只是增加了一些额外耗时而已,这部分额外的耗时是固定的,跟方法内部逻辑的复杂程度无关
- 其二是:1000 万次方法调用才耗时 568 ms,平均执行一次方法的增加的额外耗时为 0.0000568 ms,非常小
对于大部分方法来说,特别是一些包含 IO 操作的方法(比如访问数据库),方法本身内部逻辑执行的耗时远远大于使用反射而额外增加的耗时
因此在大部分情况下,我们也并不需要担心使用反射执行方法导致的一丢丢性能下降
那么,相比普通的对象创建和执行,使用反射创建对象和执行方法,增加的额外耗时产生在哪里呢?
安全性检查
对于普通的对象创建和执行,大量的安全性检查
比如:传入某个方法的数据类型必须与参数类型匹配、在某个对象上调用某个方法必须确保这个对象有这个方法,这些都是在编译时完成的,不占用运行时间
但是对于反射,因为其是在运行时才确定创建什么对象、执行什么方法的,所以安全性检查无法在编译时执行,只能在运行时真正创建创建、执行方法时再完成,那么这就会增加额外的运行时间
类、方法查找
当我们使用反射创建对象或执行方法时,我们需要通过类名、方法名去查找对应的类或方法,而类名、方法名都是字符串,字符串匹配相对来说比较慢速
而正常情况下,代码经过编译之后,得到的字节码中,每个类和方法都会分配一个对应的编号,保存在常量池中,代码中所有出现类或方法的地方,都会被替换为编号
相比于通过类名、方法名字符串来查找类和方法,通过编号来查找对应的类或方法,显然要快得多
我们再通过一个简单的例子进一步解释一下,代码如下所示
public class Demo {
public static void f() {
}
public static void main(String[] args) {
Demo d = new Demo();
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
}
6、课后思考题
单例模式有多种实现方式,其他实现方式是否也存在可能被反射攻击的问题呢?
是的
只要是利用 private 构造函数来限制类创建对象的单例实现,我们都可以利用反射调用 private 构造函数创建新的对象
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17471397.html

浙公网安备 33010602011771号