说说 Java 的反射机制

Java 语言允许通过程序化的方式间接对 Class 进行操作, Class 文件由类装载器装载后,在 JVM 中将形成一份描述 Class 结构的元信息对象,通过该元信息对象可以获知 Class 的结构信息:如构造函数 、 属性和方法等信息 。

1 示例

假设有这样一个类:

public class People {

    /**
     * 姓名
     */
    private String name;

    /**
     * 年龄
     */
    private int age;

    /**
     * 默认构造函数
     */
    public People() {
    }

    /**
     * 带参数的构造函数
     *
     * @param name
     * @param age
     */
    public People(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "People{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

一般情况下,创建实例的方式是:

People people = new People("deniro", 22);

下面我们通过 Java 反射机制以一种更加通用的方式来操作目标类:

ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class clazz = loader.loadClass("net.deniro.springBoot.spring4.IoC.People");

//获取类的默认构造器对象,并使用这个对象实例化类
Constructor constructor = clazz.getDeclaredConstructor((Class[]) null);
People people2 = (People) constructor.newInstance();

//通过反射设置属性值
Method setNameMethod = clazz.getMethod("setName", String.class);
setNameMethod.invoke(people2, "Jack");
System.out.println("people2:" + people2);

这说明我们完全可以通过编程的方式来调用 Class 的各种元素,这和直接通过构造函数和方法调用类的效果是一样的,只不过前者是间接调用,后者是直接调用罢了 。

如果我们把这些信息放在配置文件中,那么就可以使用反射能力编写一段通用的代码对这些类进行实例化及功能调用操作咯 O(∩_∩)O哈哈~

2 类装载器(ClassLoader)

2.1 工作机制

类装载器把一个类装入 JVM 中,要经过以下步骤:

装载步骤

1、装载:查找和导入 Class 文件。
2、 链接:执行校验 、 准备和解析(可选)步骤:
* 校验:检查载入 Class 文件数据的正确性;
* 准备:给类的静态变量分配存储空间;
* 解析:将符号引用转成直接引用;

3、初始化:初始化类的静态变量和静态代码块。

类装载器继承关系

JVM 在运行时会产生三个 ClassLoader :根装载器 、ExtClassLoader (扩展类装载器)和 AppClassLoader (应用类装载器) 。 注意,根装载器不是 ClassLoader 的子类,它使用 C++ 编写,因此在 Java 中看不到它,根装载器负责装载 JRE 的核心类库,如 JRE 目标下的 rt.jar、charsets.jar 等类库 。ExtClassLoader 和 AppClassLoader 都是 ClassLoader 的子类 。
* ExtClassLoader 负责装载 JRE 扩展目录 ext 中的 JAR 类包
* AppClassLoader 负责装载 Classpath 路径下的类包 。

类装载器层级关系

这三个类装载器之间存在父子层级关系,即根装载器是 ExtClassLoader 的父装载器, ExtClassLoader 是 AppClassLoader 的父装载器 。 默认情况下,是使用 AppClassLoader 来装载应用程序的类:

public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println("当前加载器:" + loader);
        final ClassLoader parent = loader.getParent();
        System.out.println("父加载器:" + parent);
        System.out.println("祖父加载器:" + parent.getParent());
    }
}

运行结果:

当前加载器:sun.misc.Launcher$AppClassLoader@63961c42
父加载器:sun.misc.Launcher$ExtClassLoader@681a9515
祖父加载器:null

祖父 ClassLoader 是根类装载器,因为在 Java 中无法获得它的句柄,所以返回的是 null 。

JVM 装载类时使用的是 “ 全盘负责委托机制 ” , “ 全盘负责 ” 是指当一个 ClassLoader 装载一个类的时,除非显式地指定另一个 ClassLoader ,否则该类所依赖以及所引用的类也是由这个 ClassLoader 装载的; “ 委托机制 ” 是指先委托父装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类 。 这一点是从安全角度考虑的,这样避免基本类被恶意篡改。


java.lang.NoSuchMethodError 错误一般是 JVM 的全盘负责委托机制所引发的问题,有可能是因为类路径下放置了多个不同版本的类包导致的问题。

通过以下方法,即可获知当前环境下,某个类是从哪个 JAR 包中加载的信息:

public class ClassLocationUtils {

    /**
     * 获取某个类的所归属的类库路径
     *
     * @param clazz
     * @return
     */
    public static String source(final Class clazz) {
        if (clazz == null) {
            throw new IllegalArgumentException("clazz");
        }

        URL result = null;

        String name = clazz.getName().replace('.', '/').concat(".class");
        ProtectionDomain pd = clazz.getProtectionDomain();//获取保护域

        if (pd != null) {
            CodeSource source = pd.getCodeSource();
            if (source != null && source.getLocation() != null) {
                result = source.getLocation();
                if ("file".equals(result.getProtocol())) {
                    try {
                        if (result.toExternalForm().endsWith(".jar") || result.toExternalForm()
                                .endsWith(".zip")) {
                            result = new URL("jar:" + result.toExternalForm() + "!/" + name);
                        } else if (new File(result.getFile()).isDirectory()) {
                            result = new URL(result, name);
                        }
                    } catch (MalformedURLException e) {
                        throw new ClassLocationUtilsException("构造 URL 类", e);
                    }
                }
            }
        }

        if (result == null) {
            ClassLoader loader = clazz.getClassLoader();
            result = loader != null ? loader.getResource(name) : loader.getSystemResource(name);
        }

        return result.toString();
    }
}

使用示例:

System.out.println(ClassLocationUtils.source(StringUtils.class));

输出结果:

jar:file:/F:/repo/m2/org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar!/org/apache/commons/lang3/StringUtils.class

2.2 方法

方法 说明
Class loadClass(String name) name 参数指定类装载器需要装载类的名字,必须使用全限定类名。 该方法有一个重载方法 loadClass(String name ,boolean resolve) , resolve 参数告诉类装载器是否需要解析该类 。 在初始化类之前,应考虑进行类解析的工作,但并不是所有的类都需要解析,如果 JVM 只需要知道该类是否存在或找出该类的超类,那么就不需要进行解析 。
Class defineClass(String name, byte[] b, int off, int len) 将类文件的字节数组转换成 JVM 内部的 java.lang.Class 对象 。 字节数组可以从本地文件系统 、 远程网络获取 。name 为字节数组对应的全限定类名 。
Class findSystemClass(String name) 从本地文件系统载入 Class 文件,如果本地文件系统不存在该 Class 文件,将抛出 ClassNotFoundException 异常 。 该方法是 JVM 默认使用的装载机制 。
Class findLoadedClass(String name) 调用该方法来查看 ClassLoader 是否已装入某个类 。 如果已装

入,那么返回 java.lang.Class 对象,否则返回 null。 如果强行装载已存在的类,将会抛出链接错误 。
ClassLoader getParent()| 获取类装载器的父装载器,除根装载器外,所有的类装载器都有且仅有一个父装载器, ExtClassLoader 的父装载器是根装载器,因为根装载器非 Java 编写,所以无法获得,将返回 null。

可以编写自己的第三方类装载器,以实现一些特殊的需求 。 类文件被装载并解析后,在 JVM 内将拥有一个对应的 java.lang.Class 类描述对象,该类的实例都拥有指向这个类描述对象的引用,而类描述对象又拥有指向关联 ClassLoader 的引用:

类实例、类描述对象与类装载器之间的关系

每一个类在 JVM 中都拥有一个对应的 java.lang.Class 对象,它提供了类结构信息的描述 。 数组 、 枚举 、 注解以及基本 Java 类型(如 int、double 等),甚至 void 都拥有对应的 Class 对象 。Class 没有 public 的构造方法 。Class 对象是在装载类时由 JVM 通过调用类装载器中的 defineClass() 方法来构造的 。

3 反射机制

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

1、Constructor :类的构造函数反射类,通过 Class#getConstructors() 方法可以获得类的所有方法反射类对象数组 Method[]。 在 Java 5.0 中,还可以通过 getConstructor(Class… parameterTypes) 获取拥有特定入参的构造函数反射对象 。Constructor 的一个主要方法是 newInstance(Object[] initargs) ,通过该方法可以创建一个对象类的实例,相当于 new 关键字 。 在 Java 5.0 中该方法演化为更为灵活的形式: newInstance(Object… initargs)。

2、 Method :类方法的反射类,通过 Class#getDeclaredMethods() 方法可以获取类的所有方法反射类对象数组 Method[]。 在 Java 5.0 中可以通过 getDeclaredMethod(String name, Class… parameterTypes) 获取特定签名的方法, name 为方法名; Class… 为方法入参类型列表 。Method 最主要的方法是 invoke(Object obj, Object[] args) , obj 表示操作的目标对象; args 为方法入参 。 在 Java 5.0 中,该方法的形式调整为 invoke(Object obj, Object… args)。 此外, Method 还有很多用于获取类方法更多信息的方法 -

方法 说明
Class getReturnType() 获取方法的返回值类型。
Class[] getParameterTypes() 获取方法的入参类型数组。
Class[] getExceptionTypes() 获取方法的异常类型数组。
Annotation[][] getParameterAnnotations() 获取方法的注解信息,JDK 5.0 中的新方法。

3、Field:类的成员变量的反射类,通过 Class#getDeclaredFields() 方法可以获取类的成员变量反射对象数组,通过 Class#getDeclaredField(String name) 则可获取某个特定名称的成员变量反射对象 。Field 类最主要的方法是 set(Object obj, Object value) , obj 表示操作的目标对象,通过 value 为目标对象的成员变量设置值 。 如果成员变量为基础类型,用户可以使用 Field 类中提供的带类型名的值设置方法,如 setBoolean(Object obj, boolean value)、setInt(Object obj, int value) 等 。

此外, Java 还为包提供了 Package 反射类,在 JDK 5.0 中还为注解提供了 AnnotatedElement 反射类 。

总之, Java 的反射体系保证了可以通过程序化的方式访问目标类中所有的元素,对于 private 或 protected 的成员变量和方法,只要 JVM 的安全机制允许,也是可以通过反射进行调用的。

public class House {

    /**
     * 私有变量(只能在本类中被访问)
     */
    private String address;

    /**
     * 受保护的方法(只能在子类或者所在的包中被访问)
     */
    protected void decorate() {
        System.out.println("开始装修咯,所在地址:" + address);
    }


}

通过反射机制可以访问这些私有的或受保护的变量与方法:

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException {
    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    Class clazz = loader.loadClass("net.deniro.springBoot.spring4.IoC.House");

    House house = (House) clazz.newInstance();

    //设置 private 变量
    Field field = clazz.getDeclaredField("address");
    field.setAccessible(true);//取消访问检查
    field.set(house, "长安");

    //设置 protected 方法
    Method method = clazz.getDeclaredMethod("decorate");
    method.setAccessible(true);
    method.invoke(house, (Object[]) null);
}

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

猜你喜欢

转载自blog.csdn.net/deniro_li/article/details/79961249