Java反射原理、双亲委派
反射
什么是反射
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
优缺点
优点:能够动态获取类的实例,提高灵活性。
缺点:使用反射性能较低,需要解析字节码,将内存中的对象进行解析。可以通过setAccessible(true)关闭 JDK 的安全检查来提升反射速度;多次创建一个类的实例时,有缓存会快很多。
如何获取反射中的Class对象
Class.forName(“类的路径”);当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。
Class clz = Class.forName("java.lang.String");
类名.class。这种方法只适合在编译前就知道操作的 Class。
Class clz = String.class;
对象名.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
类:反射核心类,可以获取类的属性,方法等。
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 的过程:
- 将程序内所有 XML 或 Properties 配置文件加载入内存中;
- Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
- 使用反射机制,根据这个字符串获得某个类的Class实例;
- 动态配置实例的属性。
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优化问题还有权限检查。。。
双亲委派机制
JVM中提供了三层的ClassLoader
:
Bootstrap classLoader
: 主要负责加载核心的类库(java.lang.*
等),构造ExtClassLoader
和APPClassLoader
。
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;
}
}
注释转换成图就是如下图所示:
为什么设计这种机制?
如果有人想替换系统级别的类:String.java,篡改其实现,在这种机制下,系统的类已经被Bootstrap classLoader加载过了,其他类加载器没有机会去加载,一定程度上防止了危险代码植入。也避免了重复加载。