Java反射概述

Java语言允许通过程序化的方式间接对Class进行操作。

Class文件由类装载器装载后,在JVM中形成一份描述Class结构的元信息对象,通过该元对象可以获知Class的结构信息,如构造函数、属性和方法等。

Java允许用户借由这个与Class相关的元信息对象间接调用Class对象的功能, 这就为使用程序化方式操作Class对象开辟了途径。

使用反射不同于常规的Java编程,其中它与 元数据–描述其它数据的数据协作。Java语言反射接入的特殊类型的原数据是JVM中类和对象的描述。

Java反射机制可以让我们在编译期(Compile Time)之外的运行期(Runtime)获得任何一个类的字节码。包括接口、变量、方法等信息。还可以让我们在运行期实例化对象,通过调用get/set方法获取变量的值。


示例

Code

我们将用下面这个例子来了解Java反射机制。

package com.xgj.master.ioc.reflect;

public class Car {

    private String brand ;
    private String color;
    private int speed;

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public int getSpeed() {
        return speed;
    }

    public void setSpeed(int speed) {
        this.speed = speed;
    }

    /**
     * 

     * @Title:Car

     * @Description:默认构造函数
     */
    public Car(){

    }

    /**
     * 

     * @Title:Car

     * @Description:带参构造函数

     * @param brand
     * @param color
     * @param speed
     */
    public Car(String brand ,String color ,int speed){
        this.brand = brand;
        this.color = color;
        this.speed = speed;
    }

    public void introduceCar(){
        System.out.println("Car : " + this.brand + " , " + this.color + " , " + this.speed);
    }

}

通常情况下,我们实例化类,调用类中的方法如下:

    Car car = new Car("BMW","Black",180);
    car.introduceCar();

输出: Car : BMW , Black , 180

这是使用传统的方式来直接调用目标类的方法。

如果使用Java的反射机制 该如何控制目标类呢?

来看代码

package com.xgj.master.ioc.reflect;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class ReflectTest {

    public static Car initCarByDefaultConstrut() throws Exception {

        // (1)通过类装载器获取Car类对象
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        Class claz = loader.loadClass("com.xgj.master.ioc.reflect.Car");

        // (2)获取类的默认构造函数,并通过它实例化Car
        Constructor constructor = claz.getDeclaredConstructor(null);
        Car car = (Car) constructor.newInstance();

        // (3)通过反射方法设置属性
        Method method = claz.getMethod("setBrand", String.class);
        method.invoke(car, "BMW");

        Method method2 = claz.getMethod("setColor", String.class);
        method2.invoke(car, "black");

        Method method3 = claz.getMethod("setSpeed", int.class);
        method3.invoke(car, 100);

        return car;

    }

    public static void main(String[] args) throws Exception {

        initCarByDefaultConstrut().introduceCar();
    }

}

运行结果: Car : BMW , black , 100


分析

我们完全可以通过编程方式调用Class的各项功能,与通过构造函数和方法直接调用类的功能的效果是一致的,只不过是间接调用罢了。

几个重要的反射类

  • ClassLoader
  • Class
  • Constructor
  • Method .
    通过这些反射类我们就可以间接的调用目标Class的各项功能。

在(1)处,我们获取当前线程的ClassLoader, 然后通过指全限定类名com.xgj.master.ioc.reflect.Car 来装载Car类对应的反射实例。

在(2)处,我们通过Car的反射类对象获取Car的默认构造函数对象,通过构造函数对象的newInstance()方法实例化Car对象,等同于 new Car()

在(3)处,我们又通过Car的反射类对象的getMethod(String methodName, Class paramsClass)获取属性的Setter方法对象,其中第一个参数是目标Class的方法名,第二个参数是方法入参的对象类型。

在获取到方法反射对象后,就可以通过invoke(Object ob, Object param)方法调用目标类的方法了。 该方法的第一个禅师是操作的目标类对象实例,第二个参数目标方法的入参。


类装载器ClassLoader

工作机制

类装载器就是寻找类的字节码文件并构造类在JVM内部表示对象的组件。

类装载器把一个类装入JVM中,步骤如下:

  1. 装载:查找和导入Class
  2. 链接:执行校验、准备和解析步骤(解析步骤可选)
  3. 初始化:对类的静态变量、静态代码块执行初始化工作
    其中第二步操作包括:
    (1). 检验:检查载入Class文件数据的正确性
    (2). 准备:给类的静态变量分配存储空间
    (3). 解析:将符号引用转换为直接引用

类装载工作由ClassLoader及其子类负责,负责在运行时查找和装入Class直接码文件。


ClassLoader分类

JVM运行期间会产生3个ClassLoader

  • 根装载器
  • ExtClassLoader(扩展类装载器)
  • AppClassLoader(应用类装载器)

    其中 ExtClassLoader和AppClassLoader 是 ClassLoader的子类
    根装载器不是ClassLoader的子类,它是C++编写。

  • 根装载器负责装载JRE的核心类库,比如JRE目标下的JAR

    Java-Java反射_反射

  • ExtClassLoader负责装载JRE扩展目录ext中的JAR包

Java-Java反射_类装载器_02

  • AppClassLoader负责装载应用Classpath路径下的类包

三者关系: 根装载器是ExtClassLoader的父装载器,ExtClassLoader是AppClassLoader的父装载器。 默认情况下,使用AppClassLoader来装载应用程序的类。

验证下:

package com.xgj.master.ioc.classloader;

public class ClassLoaderTest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        System.out.println("current loader:" + classLoader);
        System.out.println("parent laoder:" + classLoader.getParent());
        System.out.println("grandparent laoder:" + classLoader.getParent().getParent());
    }

}

输出:

current loader:sun.misc.Launcher$AppClassLoader@8391b0c
parent laoder:sun.misc.Launcher$ExtClassLoader@5d1eb50b
grandparent laoder:null

根装载器在Java中无法获取到它的句柄,因此返回null .

全盘负责委托机制

JVM装载类时使用“全盘负责委托机制”。

全盘负责:是指当一个ClassLoader装载一个类时,除非显示地使用另外一个ClassLoader,该类所依赖以及引用的类也由这个ClassLoader载入。

委托机制:是指先委托父类装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。

这一点是从安全角度考虑,举个例子,比如有人恶意编写了一个基础类如java.lang.String 并装载到JVM中,如果没有委托机制,jvm就会读取了这个恶意基础类,全盘负责委托机制保证了java.lang.String永远由根装载器来装载,避免了安全隐患的发生。

如何查看JVM从哪个JAR包中加载指定类呢?

请看 Java-查看JVM从哪个JAR包中加载指定类


重要方法

loadClass(String name)

 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

name参数指定类装载器需要装载的类的名字,必须使用全限定类名。

该方法有一个重载方法 loadClass(String name ,boolean resolve) .resolve参数告诉类装载器是否需要解析该类, 如果JVM只需要知道该类是否存在或者找出该类的超类,那么就不需要进行解析。


defineClass(String name, byte[] b, int off, int len)

将类文件的字节数组转换成JVM内部的java.lang.Class对象。 参数name为字节数组对应的全限定类名。


findSystemClass(String name)

protected final Class<?> findSystemClass(String name)
        throws ClassNotFoundException
    {
        ClassLoader system = getSystemClassLoader();
        if (system == null) {
            if (!checkName(name))
                throw new ClassNotFoundException(name);
            Class cls = findBootstrapClass(name);
            if (cls == null) {
                throw new ClassNotFoundException(name);
            }
            return cls;
        }
        return system.loadClass(name);
    }

从本地文件系统装载Class文件,不存在则抛出ClassNotFoundException。 该方法为AJVM默认使用的装载机制。

findLoadedClass(String name)

调用该方法查看ClassLoader是否已经载入某个类,如果载入,返回java.lang.Class对象,否则返回null.

如果强行装载已经存在的类,将抛出链接错误。


getParent()

  @CallerSensitive
    public final ClassLoader getParent() {
        if (parent == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkClassLoaderPermission(parent, Reflection.getCallerClass());
        }
        return parent;
    }

获取类装载器的父装载器。 除了根装载器外,所有的类装载器都有且有且只有一个父装载器。 ExtClassLoader的父装载器是根装载器。 因为根装载器非Java语言编写,因此无法获得,返回null.


总结

除了JVM默认的3个ClassLoader外,用户也可以编写自己的第三方类装载器,以实现一些特殊的需求。

类文件被装载并解析后,在JVM内将拥有一个对应的java.lang.Class类描述对象,该类的实例都拥有指向这个类描述对象的引用,而类描述对象又拥有指向关联ClassLoader的引用。

如下图《类实例、类描述对象及装载器的关系》所示

Java-Java反射_类装载器_03

每个类在JVM中都有一个对应的java.lang.Class对象。它提供了类结构信息的描述。

Class没有public的构造方法,Class对象是在装载类时由JVM通过调用类装载器中的defineClass()方法自动构造的。


Java反射机制

Class反射对象描述类定义结构,可以从Class对象中获取构造函数、成员变量、方法类等类元素的反射对象,并以编程的方式通过这些反射对象对目标类对象进行操作。

这些反射对象定义在java.lang.reflect包中。

三个主要的反射类

Constructor

类的构造函数反射类。

通过Class#getConstructors()方法可以获取类的所有构造函数反射对象数组。

在Java5.0中,还可以通过getConstructor(Class...parameterTypes)获取拥有特定入参的构造函数反射对象。

Constructor的一个主要方法是newInstance(Object[] initargs),通过该方法可以创建一个对象类的实例。相当于new关键字。

在Java5.0中,该方法演化为更为灵活的形式:newInstance(Object...initargs)


Method

类方法的反射类。

通过Class#getDeclaredMethods()方法可以获取类的所有方法反射类对象数组Method[].

在Java5.0中,可以通过getDeclaredMethod(String name,Class...parameterTypes)获取特定签名的方法。其中name为方法名,Class...为方法入参类型列表。

Method最主要的方法是invoke(Object obj , Object[] args) , 其中obj表示操作的目标对象;args为方法入参。

在Java5.0中,该方法调整为invoke(Object obj, Object...args) .

此外,其他比较常用的方法:

  • Class getReturnType():获取方法的返回值烈性
  • Class[] getParamaterTypes():获取方法的入参类型数组
  • Class[] getExceptionTypes() 获取该方法的异常类型数组
  • Annotation[][] getParameterAnnotations() 获取方法的注解洗洗,是Java5.0中新增的方法。

Field

类的成员变量的反射类。

通过Class#getDeclaredFields()方法可以获取类的成员变量反射对象数组,
通过Class#getDeclaredField(String name)则可以获取某个特定名称的成员变量反射对象。

Field类的主要方法是set(Object obj , Object value) 其中obj表示操作的目标对象,通过value为目标对象的成员变量设置值。

如果成员变量为基础类型,则可以使用Field类中提供的带类型名的值设置方法,比如setBoolean(Object obj , Object value)、setInt(Object obj , Object value)等。

此外Java还未包提供了Package反射类,在Java5.0中还未注解提供了AnnotatedElement反射类。

对于private或者procted成员变量和方法,只要JVM的安全机制允许,也可以通过反射调用。比如:

package com.xgj.master.ioc.reflect;

public class PrivateCar {

    private String brand;

    protected void introduce() {
        System.out.println("brand:" + brand);
    }

}
package com.xgj.master.ioc.reflect;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class PrivateCarTest {

    public static void main(String[] args) throws Exception {

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        Class claz = classLoader.loadClass("com.xgj.master.ioc.reflect.PrivateCar");

        PrivateCar pcar = (PrivateCar) claz.newInstance();

        Field field = claz.getDeclaredField("brand");
        // 取消Java语言访问检查以便访问private变量
        field.setAccessible(true);
        field.set(pcar, "BMW");

        Method method = claz.getDeclaredMethod("introduce", (Class[]) null);
        // 取消Java语言访问检查以便访问protected方法
        method.setAccessible(true);
        method.invoke(pcar, (Object[]) null);

    }

}

在访问private 或者 protected成员变量和方法时,必须通过setAccessible(boolean access)方法取消Java语言检查,否则将抛出IllegalAccessException. 如果JVM的安全管理器(SecurityManager)设置了相应的安全机制,那么调用该方法会抛出SecurityException