Java反射机制详解(看这篇就够了)

Java反射机制详解(看这篇就够了)-mikechen

Java反射机制是Java非常重要的动态特性,通过反射不仅可以获取到任何类的成员方法、成员变量、构造方法(Constructors)等信息,还可以动态创建Java类实例等,也是Java的各种框架底层实现的灵魂@mikechen

Java反射机制是什么?

Java反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

用一句话总结就是反射可以实现在运行时可以知道任意一个类的属性和方法。

 

为什么要用反射?

Java Reflection功能非常强大,并且非常有用,比如:

Java反射机制详解(看这篇就够了)-mikechen

  • 获取任意类的名称、package信息、所有属性、方法、注解、类型、类加载器等
  • 获取任意对象的属性,并且能改变对象的属性
  • 调用任意对象的方法
  • 判断任意一个对象所属的类
  • 实例化任意一个类的对象
  • 通过反射我们可以实现动态装配,降低代码的耦合度,动态代理等。

 

Java反射的应用场景

Java反射机制详解(看这篇就够了)-mikechen

面向开发,反射应用最广泛的是中间件和框架,比如:

  • JDBC的Class.forName(driverClass)加载驱动;
  • Spring MVC 通过反射调用 controller 的方法,动态代理处理请求;
  • Spring IOC 容器,在创建 Bean 实例时和依赖注入时的反射。
  • RMI 反序列化,反射调用远程方法;
  • RPC Dubbo动态代理利用;

 

Java反射机制原理

Java反射操作的是java.lang.Class对象,所以要理解Java反射机制,就需要搞懂Class对象。

比如,举一个简单的例子,创建一个对象:

MikeChen mikechen= new MikeChen();

在运行上面语句的时候,首先 JVM 启动,代码会编译成一个 .class 文件,然后被类加载器ClassLoader加载进 JVM 的内存中,并为之创建一个 java.lang.Class 对象。

我们的 MikeChen类会加载到方法区中,创建了的 MikeChen类的 Class 对象会到堆中。

理解 Java 的反射机制就是要理解 Class 类,在 Java 中,所有对象可分大致分为两种:Class 对象和实例对象。

每个类的运行时的类型信息,用 Class 对象表示,又或者称之为字节码对象,它包含了与类相关的信息,而实例对象就是通过 Class 对象来创建的。

获取 Class 对象的方式:

  1. 实例对象调用 Object 类的getClass()方法;
  2. 通过属性类名.class直接获取;
  3. 调用 Class 类的forName()方法;
  4. 使用类加载器 ClassLoader 的getSystemClassLoader().loadClass()方法。
package com.mikechen.reflection;

import com.mikechen.model.Person;

public class ReflectionTest {
    public static void main(String[] args) throws Exception {
        //1. 实例对象.getClass()
        Person person = new Person();
        Class clz1 = person.getClass();
        System.out.println(clz1);

        //2,类名.class属性
        System.out.println(Person.class);

        //3. Class.forName()
        Class clz2 = Class.forName("com.mikechen.reflection.Person");
        System.out.println(clz2);
    }
}

打印输出的结果:类型 包名.类名

以上三种方式都能获取 Person 类的字节码对象,但同时也存在区别:

  • 方法1需要创建一个实例对象才能获取类的信息;
  • 方法2则需要导入包否则无法通过编译;
  • 方法3只需传入类名的字符串,这个类名是类完整路径;
  • 方法4与其他方法不同的是,它不会执行类中的静态代码块。

其中,来看方法3,Class.forName 有两个重载方法:

public static Class<?> forName(String className)
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

参数解释:

  • className、name:类的完整路径名;
  • initialize:如果为 true,则会在返回 Class 对象之前,对该类型做连接和初始化操作,即类加载机制的后两个操作,在初始化阶段,JVM 会按照源代码语句的先后顺序去执行类变量的赋值和静态代码块。从第一个重载方法的代码可知,initialize 默认为 true 需要初始化;
  • loader:用选择的类加载器来请求这个类型,可以为 null。

因此我们得知,Class.forName默认是需要初始化。再来看看方法4,ClassLoader 的loadClass也有两个重载方法:

public Class<?> loadClass(String name)
protected Class<?> loadClass(String name, boolean resolve)

参数解释:

  • name:类的完整路径名;
  • resolve:表示是否连接该类型。这里的连接内容,并设置默认会验证 .class 文件,为类的静态成员变量分配内存并初始化为默认值,以及类型常量池引用替换,而并不会对该类型执行初始化操作。从第一个重载方法可知,resolve 默认为 false 不连接。

但容易令人造成疑惑的是,初始化时会执行哪些方法,执行的先后顺序是怎样的,我们可以来看个 demo。

package com.mikechen.model;

public class InitializationTest extends Test{

    {
        System.out.println("block of code");
    }

    static {
        System.out.println("static block code");
    }

    public InitializationTest(){
        super();
        System.out.println("constructor code");
    }
}

class Test{
    public Test(){
        System.out.println("Test");
    }
}

先看不同调用的方式的执行结果以及执行的先后顺序:

  • InitializationTest it = new InitializationTest();
    执行了static{}super(){}和构造方法;
  • Class.forName("com.hhh.model.InitializationTest");
    只执行了static{}
  • Class.forName("com.hhh.model.InitializationTest",false,InitializationTest.class.getClassLoader());
    无输出结果;
  • ClassLoader.getSystemClassLoader().loadClass("com.hhh.model.InitializationTest");
    无输出结果。

综上来说,首先调用的是static{} ,接着是super(),然后是{},最后是构造函数;而且static{}会在类的初始化的时候调用。

那么我们可以利用这种机制做一些坏坏的事情,当遇到以下情况的代码,如果方法中的 name 参数可控:

public void example(String name) throws Exception {
    Class.forName(name);
}

我们可以写一个恶意类,将恶意代码写在静态代码块中,通过某种方式(比如 URLClassLoader 加载 .class 文件、ClassLoader 加载字节码等)加载类,等类初始化的时候调用执行。

public class Evil{
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] cmd = {"touch", "/tmp/success"};
            Process p = runtime.exec(cmd);
            p.waitFor();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

Java反射使用详解

Java 类的成员包括以下三类:属性字段、构造函数、方法,反射的 API 也是与这几个成员相关:

Java反射机制详解(看这篇就够了)-mikechen
  • Field 类:提供有关类的属性信息,以及对它的动态访问权限。它是一个封装反射类的属性的类。
  • Constructor 类:提供有关类的构造方法的信息,以及对它的动态访问权限。它是一个封装反射类的构造方法的类。
  • Method 类:提供关于类的方法的信息,包括抽象方法。它是用来封装反射类方法的一个类。
  • Class 类:表示正在运行的 Java 应用程序中的类的实例。
  • Object 类:Object 是所有 Java 类的父类。所有对象都默认实现了 Object 类的方法。

接下来,我们通过一个典型的例子来学习反射。

先做准备工作,新建 com.test.reflection 包,在此包中新建一个 Student 类:

Java反射机制详解(看这篇就够了)-mikechen

可以看到,Student 类中有两个 字段、两个 构造方法、两个 函数,且都是一个私有,一个公有。由此可知,这个测试类基本涵盖了我们平时常用的所有类成员。

3.1.获取 Class 对象的三种方式

获取 Class 对象有三种方式:

Java反射机制详解(看这篇就够了)-mikechen
  • 第一种方法是通过类的全路径字符串获取 Class 对象,这也是我们平时最常用的反射获取 Class 对象的方法;
  • 第二种方法有限制条件:需要导入类的包;
  • 第三种方法已经有了 Student 对象,不再需要反射。

通过这三种方式获取到的 Class 对象是同一个,也就是说 Java 运行时,每一个类只会生成一个 Class 对象。

我们将其打印出来测试一下:

Java反射机制详解(看这篇就够了)-mikechen

运行程序,输出如下:

Java反射机制详解(看这篇就够了)-mikechen

OK,拿到 Class 对象之后,我们就可以为所欲为啦!

3.2.获取成员变量

获取字段有两个 API:getDeclaredFields 和 getFields。他们的区别是: getDeclaredFields 用于获取所有声明的字段,包括公有字段和私有字段,getFields 仅用来获取公有字段:

Java反射机制详解(看这篇就够了)-mikechen

运行程序,输出如下:

Java反射机制详解(看这篇就够了)-mikechen

3.3.获取构造方法

获取构造方法同样包含了两个 API:用于获取所有构造方法的 getDeclaredConstructors和用于获取公有构造方法的 getConstructors:

Java反射机制详解(看这篇就够了)-mikechen

运行程序,输出如下:

Java反射机制详解(看这篇就够了)-mikechen

3.4.获取非构造方法

同样地,获取非构造方法的两个 API 是:获取所有声明的非构造函数的 getDeclaredMethods 和仅获取公有非构造函数的 getMethods:

Java反射机制详解(看这篇就够了)-mikechen

运行程序,输出如下:

Java反射机制详解(看这篇就够了)-mikechen

从输出中我们看到,getMethods方法不仅获取到了我们声明的公有方法 setStudentAge,还获取到了很多 Object 类中的公有方法。

这是因为我们前文已说到:Object 是所有 Java 类的父类,所有对象都默认实现了 Object 类的方法, 而 getDeclaredMethods是无法获取到父类中的方法的。

陈睿mikechen

10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。

关注「mikechen」公众号,获取更多技术干货!

后台回复面试即可获取《史上最全阿里Java面试题总结》,后台回复架构,即可获取《阿里架构师进阶专题全部合集

评论交流
    说说你的看法