24、反射

内容来自王争 Java 编程之美
反射 API

尽管在平时的业务开发中,我们很少会用到反射、注解、动态代理这些比较高级的 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 构造函数创建新的对象
posted @ 2023-06-10 15:49  lidongdongdong~  阅读(48)  评论(0)    收藏  举报