Java反射原理、双亲委派


反射

什么是反射

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

优缺点

优点:能够动态获取类的实例,提高灵活性。

缺点:使用反射性能较低,需要解析字节码,将内存中的对象进行解析。可以通过setAccessible(true)关闭 JDK 的安全检查来提升反射速度;多次创建一个类的实例时,有缓存会快很多。

如何获取反射中的Class对象

  1. Class.forName(“类的路径”);当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。

    Class clz = Class.forName("java.lang.String");
    
  2. 类名.class。这种方法只适合在编译前就知道操作的 Class。

    Class clz = String.class;
    
  3. 对象名.getClass()。

    String str = new String("Hello");
    Class clz = str.getClass();
    

怎么用Class对象

Class.forName

Class clz = Class.forName("java.lang.String");
// 调用的是默认空构造函数,这里就是空字符串
String a = (String) clz.newInstance();
// clz.newInstance() 创建空字符串

如果没有空构造函数呢?

// Class.getConstructors() 获取所有构造函数
// Class.getConstructor(Class... paramTypes) 获取指定参数的构造函数

如查看所有构造函数:

for(Constructor constructor : clz.getConstructors()) {
    System.out.println(constructor);
}

输出:

public java.lang.String(byte[],int,int)
public java.lang.String(byte[],java.nio.charset.Charset)
public java.lang.String(byte[],java.lang.String) throws java.io.UnsupportedEncodingException
public java.lang.String(byte[],int,int,java.nio.charset.Charset)
public java.lang.String(byte[],int,int,java.lang.String) throws java.io.UnsupportedEncodingException
public java.lang.String(java.lang.StringBuilder)
public java.lang.String(java.lang.StringBuffer)
public java.lang.String(byte[])
...

要获取指定参数的构造函数呢?

Constructor constructor = clz.getConstructor(); // 默认构造函数
System.out.println(constructor); // public java.lang.String()

constructor = clz.getConstructor(byte[].class,int.class,int.class);
System.out.println(constructor); // public java.lang.String(byte[],int,int)

构造函数获取到了,怎么用?

constructor = clz.getConstructor(byte[].class,int.class,int.class);
System.out.println(constructor); // public java.lang.String(byte[],int,int)
a = (String) constructor.newInstance("hqinglau orzlinux.cn".getBytes(),9,11);
System.out.println(a); // orzlinux.cn

Java反射API

Class类:反射核心类,可以获取类的属性,方法等。

image-20211118115420070

Field类:获取和设置属性值。

Method: 获取类中的方法信息或者执行方法。

Constructor: 类的构造方法。

反射使用示例

// 创建class对象
Class clz = Class.forName("cn.orzlinux.skjava.base.ReflectDemo");

// 获取set方法
Method setPriceMethod = clz.getMethod("setPrice", int.class);

// 构造器,构造对象
Constructor constructor = clz.getConstructor();
ReflectDemo demo = (ReflectDemo) constructor.newInstance();

// 调用set方法
setPriceMethod.invoke(demo,14);

// get方法
Method getPriceMethod = clz.getMethod("getPrice");
System.out.println(getPriceMethod.invoke(demo));

引入反射的原因及应用示例

原因:

  • 反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。
  • 反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法。以帮助开发者写出正确的代码。
  • 测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率。

示例一:JDBC数据库连接

原文链接:segmentfault

try {
    Class.forName("com.mysql.jdbc.Driver") ;   
} catch(ClassNotFoundException e) {   
    System.out.println("找不到驱动程序类 ,加载驱动失败!");
    return;  // or do something else
}

上面一段代码的作用是在运行期以反射的方式来检测JDBC驱动主类com.mysql.jdbc.Driver是否存在。若不存则表示运行环境中没有这个驱动,进入catch段。如果你确定一定以及肯定它会存在,可以直接写成

import com.mysql.jdbc.Driver;

效果基本是一样的(只是在编译期及运行期要都保证此类存在classpath中)。

所以,以反射形式加载的一个好处是当驱动jar包不存在时,我们可以做更多的操作。(要知道,在很久很久以前,jdbc驱动一般都是放在运行环境的classpath中的,如tomcat/lib

另外一个很重要的原因是解耦。

首先要明白JDBC是Java的一种规范,通俗一点说就是JDK在java.sql.*下提供了一系列的接口(interface),但没有提供任何实现(也就是类)。 所以任何人都可以在接口规范之下写自己的JDBC实现(如MySQL)。而若调用者也只调用接口上的方法(如我们),那么当未来有任何变更需要时(例如要从MySQL迁移至Oracle),则理论上不需要对代码做任何修改就能直接切换(可惜SQL语法没能统一规范)

这意味着什么?意味着你的代码中不应该引用任何与实现相关的东西,你的代码只知道java.sql.*,而不应该知道com.mysql.*或是com.oracle.*,以避免或减少未来切换数据源时对代码的变更。

注意,我们使用的所有其他API包括Connection/Statement/ResultSet等都是java.sql.*的东西,甚至com.mysql.jdbc.Driver类也是:

package com.mysql.jdbc;
public class Driver ... implements java.sql.Driver {
    ...
}

因此,直接import com.mysql.jdbc.Driver;违反了开闭原则(OCP,对扩展开放,对修改关闭)。(有人说我用反射也必须要修改代码呀,事实上你可以将类名字符串存储至.properties文件,和数据库用户名密码放在一起,就像Hibernate做的那样)

引申问题

如果我可以保证JDBC驱动一定在classpath下,是不是可以不写这段反射代码,也不引用任何的Driver类?答案是否定的,请看下面这段代码源自com.mysql.jdbc.Driver

package com.mysql.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    ...
}

static代码块会在类加载时就被执行——也就是当我们执行Class.forName("com.mysql.jdbc.Driver")时(或import com.mysql.jdbc.Driver

示例二:Spring框架的使用,xml配置模式

Spring 通过 XML 配置模式装载 Bean 的过程:

  1. 将程序内所有 XML 或 Properties 配置文件加载入内存中;
  2. Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
  3. 使用反射机制,根据这个字符串获得某个类的Class实例;
  4. 动态配置实例的属性。

Spring这样做的好处是:

  • 不用每一次都要在代码里面去new或者做其他的事情;
  • 以后要改的话直接改配置文件,代码维护起来就很方便了;
  • 有时为了适应某些需求,Java类里面不一定能直接调用另外的方法,可以通过反射机制来实现。

反射机制的原理

获取Class对象:

Class clz = Class.forName("cn.orzlinux.skjava.base.ReflectDemo");

forName方法:

@CallerSensitive
public static Class<?> forName(String className)
            throws ClassNotFoundException {
    // 调用本地方法,获取调用者的类信息
    // @CallerSensitive
    // public static native Class<?> getCallerClass();
    Class<?> caller = Reflection.getCallerClass();
    
    // private static native Class<?> forName0(String name, boolean initialize,
    //                                        ClassLoader loader,
    //                                        Class<?> caller)
    //    throws ClassNotFoundException;
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

可以发现 Class.forName 方法上有 @CallerSensitive 注解, 因为代码里的Reflection.getCallerClass()这个native方法要求。

jdk内有些方法,jvm的开发者认为这些方法危险,不希望开发者调用,就把这种危险的方法用 @CallerSensitive修饰,并在 jvm 级别检查。

Reflection.getCallerClass()方法规定,调用它的对象,必须有 @CallerSensitive 注解,否则 报异常 Exception in thread "main" java.lang.InternalError: CallerSensitive annotation expected at frame 1 @CallerSensitive 有个特殊之处,必须由 启动类classloader加载(如rt.jar ),才可以被识别。 所以rt.jar下面的注解可以正常使用。

开发者自己写的@CallerSensitive 不可以被识别。 但是,可以利用jvm参数 -Xbootclasspath/a: path 假装自己的程序是启动类。

forName()反射获取类信息,将其交给了jvm去加载。加载类又回调ClassLoader。详见下文:双亲委派模式

getConstructor:

@CallerSensitive
public Constructor<T> getConstructor(Class<?>... parameterTypes)
    throws NoSuchMethodException, SecurityException {
    // 权限检查
    checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
    return getConstructor0(parameterTypes, Member.PUBLIC);
}
// 遍历所有的构造器,比较参数,符合就返回
private Constructor<T> getConstructor0(Class<?>[] parameterTypes,
                                       int which) throws NoSuchMethodException
{
    Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC));
    for (Constructor<T> constructor : constructors) {
        if (arrayContentsEq(parameterTypes,
                            constructor.getParameterTypes())) {
            return getReflectionFactory().copyConstructor(constructor);
        }
    }
    throw new NoSuchMethodException(getName() + ".<init>" + argumentTypesToString(parameterTypes));
}

getMethod:

@CallerSensitive
public Method getMethod(String name, Class<?>... parameterTypes)
    throws NoSuchMethodException, SecurityException {
    // 权限检查
    checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
    Method method = getMethod0(name, parameterTypes, true);
    if (method == null) {
        throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
    }
    return method;
}
// getMethod -> getMethod0 -> privateGetMethodRecursive
private Method privateGetMethodRecursive(String name,
                                         Class<?>[] parameterTypes,
                                         boolean includeStaticMethods,
                                         MethodArray allInterfaceCandidates) {
    Method res;
    // 寻找方法
    if ((res = searchMethods(privateGetDeclaredMethods(true),
                             name,
                             parameterTypes)) != null) {
        if (includeStaticMethods || !Modifier.isStatic(res.getModifiers()))
            return res;
    }
    // Search superclass's methods
    if (!isInterface()) {
        Class<? super T> c = getSuperclass();
        if (c != null) {
            if ((res = c.getMethod0(name, parameterTypes, true)) != null) {
                return res;
            }
        }
    }
    // Search superinterfaces' methods
    Class<?>[] interfaces = getInterfaces();
    for (Class<?> c : interfaces)
        if ((res = c.getMethod0(name, parameterTypes, false)) != null)
            allInterfaceCandidates.add(res);
    // Not found
    return null;
}

// 遍历比较方法名和参数类型
private static Method searchMethods(Method[] methods,
                                    String name,
                                    Class<?>[] parameterTypes)
{
    Method res = null;
    String internedName = name.intern();
    for (int i = 0; i < methods.length; i++) {
        Method m = methods[i];
        if (m.getName() == internedName
            && arrayContentsEq(parameterTypes, m.getParameterTypes())
            && (res == null
                || res.getReturnType().isAssignableFrom(m.getReturnType())))
            res = m;
    }

    return (res == null ? res : getReflectionFactory().copyMethod(res));
}

通过源码可以看出,getMethod最后还是遍历所有方法,先比较方法名,然后比较参数类型来找方法的

invoke最后调用本地方法。

为什么慢?

看网上R佬所说,反射慢大概是JIT优化问题还有权限检查。。。

image-20211118160216387

双亲委派机制

JVM中提供了三层的ClassLoader

Bootstrap classLoader: 主要负责加载核心的类库(java.lang.*等),构造ExtClassLoaderAPPClassLoader

ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。

AppClassLoader:主要负责加载应用程序的主函数类

那如果有一个我们写的Hello.java编译成的Hello.class文件,它是如何被加载到JVM中的呢?别着急,请继续往下看。

// java.lang.ClassLoader
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 {
                // 递归的给父加载器
                // 直到到达Bootstrap classLoader之前,都是在
                // 检查是否加载过,并不会选择自己去加载。
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 没爹,意味着到顶了,就给Bootstrap类加载器
                    // 这时候开始考虑自己加载了,自己无法加载,就下沉到子加载器
                    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;
    }
}

注释转换成图就是如下图所示:

image-20211118151958215

为什么设计这种机制?

如果有人想替换系统级别的类:String.java,篡改其实现,在这种机制下,系统的类已经被Bootstrap classLoader加载过了,其他类加载器没有机会去加载,一定程度上防止了危险代码植入。也避免了重复加载。

参考

Java反射——创建对象实例

java基础

segmentfault

深入理解java反射原理

@CallerSensitive 注解的作用

通俗易懂的双亲委派机制