上一篇博文中将了 Spring配置文件,本文主要讲述 Spring如何通过配置文件来实例化类的对象的。这其中的原理还是比较复杂的,需要的知识涉及到Java的类加载机制 和 Java反射的知识。
所以本文主要讲 Java的类加载机制 和 反射机制。
Java类加载机制:
我们编写的程序是.java的后缀文件,首先要通过编译器 编译成 .class的二进制文件。
Java类加载的说的就是读取磁盘中的class文件的内容,解析并存入到JVM内存中的过程。这句话延伸了三个问题:
1. 如何查找磁盘中的class文件。
2. 怎么解析和存入JVM。
3. 存入JVM的内容是什么样的东西,结构是什么样子。
如何查找磁盘中的class文件:
Java提供ClassLoader抽象类及其子类来完成 class文件加载的,这是我听别人说的,所以我找到此类,看下ClassLoader抽象类的定义(截取了一部分):
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
...
}
可以看到 ClassLoader确实是一个抽象类,其成员变量 有一个 parent,类型也是 ClassLoader,说明每个类加载其都有一个父级的类加载器,这点很重要,还有一个 loadClass方法,一般是通过调用此loadClass方法来通过类路径加载此类的。我们等下分析这个方法。
通过下面这段代码,我们看一下线程中的类加载器之间的关系:
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@67b64c45
null
可以看到,当前线程的类加载器 是 sun.misc包下的 Launcher类的内部类 AppClassLoader的实例,而其父级类也是 Launcher类的另外一个内部类ExtClassLoader。
然后我们进入到Launcher类,找到AppClassLoader和ExtClassLoader看一下:
public class Launcher {
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
...
}
static class ExtClassLoader extends URLClassLoader {
private static volatile Launcher.ExtClassLoader instance;
...
}
}
其他部分我们不关注,所以我省略了,可以发现AppClassLoader和ExtClassLoader均是URLClassLoader类的子类,再戳一下URLClassLoader类:
public class URLClassLoader extends SecureClassLoader implements Closeable {
/* The search path for classes and resources */
private final URLClassPath ucp;
/* The context to be used when loading classes and resources */
private final AccessControlContext acc;
...
}
URLClassLoader类又是 SecureClassLoader 类的子类,继续戳:
public class SecureClassLoader extends ClassLoader {
/*
* If initialization succeed this is set to true and security checks will
* succeed. Otherwise the object is not initialized and the object is
* useless.
*/
private final boolean initialized;
...
}
终于找到了 ClassLoader抽象类了。这里总结一下我们看到的:
1. 自己写的Class文件(默认类路径下的)的加载器是 sun.misc.Launcher$AppClassLoader 实例来做的;
2. sun.misc.Launcher$AppClassLoader的父级加载器是 sun.misc.Launcher$ExtClassLoader;
3. AppClassLoader和ExtClassLoader类加载器都是 Launcher的内部类,均是继承自URLClassLoader类,而URLClassLoader类继承自SecureClassLoader类,SecureClassLoader类才最终继承自ClassLoader。
AppClassLoader、ExtClassLoader、SecureClassLoader类都没有重写 loadClass()方法,URLClassLoader类中的loadClass()方法虽然重写了,但是核心也是调用的 ClassLoader 抽象类中的loadClass()方法。所以 无论是哪个类加载器,但是调用的 ClassLoader 抽象类中的loadClass()方法,我们看下源码:
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;
}
}
首先是 这个方法会抛出我们非常熟悉的 ClassNotFoundException 异常,一旦要加载的目标类,不在类加载器扫描的文件下,就会抛此异常。
然后我们看 最核心的一段代码:
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
当前类加载器执行加载任务时,会先调用其 parent 对象的loadClass()方法。如果 parent 对象 是空,会找 BootstrapClass加载器加载。
BootstrapClass加载器是加载JVM虚拟器相关类的加载器,常称为 根目录加载器。它是最顶级的加载器,是 C++写的本地方法。
在往下会看到这样的代码:
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
...
}
parent 对象的loadClass()方法 、根目录加载器都加载不到指定的类,才会调用 自己的 findClass() 来加载。
这种加载方式又称为 :“全盘负责委托机制”、“双亲委派机制”等名字。
我们总结一下这种机制:
ClassLoader抽象类中又定义了一个 ClassLoader类型的parent对象,每次调用当前对象的loadClass()方法加载指定类时,会先递归调用其parent对象的loadClass()方法;当parent对象为空时,会调用 根目录加载器完成加载;如果还未加载成功,才会调用当前对象的 findClass对象来加载。
为什么说是递归的呢?因为 parent对象 中肯定也有 parent对象呗。
为什么要这样设计呢?要弄明白这个问题,我们需要先搞清楚各个类加载器扫描的目录:
我的JDK安装目录:C:\Program Files\Java\jdk1.8.0_201
根目录加载器:\jre\lib下的Jar包,如 rt.jar等,这些Jar包都是JVM能够正常启动的必备核心包。
ExtClassLoader加载器:jre\lib\ext下的Jar包。
AppClassLoader加载器:ClassPath路径下的包(Java程序class文件所在的包)。
了解了每个加载器负责加载class文件的目录后,我们回过头来看,会发现如果不采用 双亲委派机制 来加载类,事情将会变得不可控制:
例如我自己写了一个String 类,编译后放在ClassPath路径下。AppClassLoader加载器将其加载到 JVM中后,JVM中就会有两个 String类,一个我自己的,一个 jdk中的String类。 不仅会造成混乱,甚至引发很多不可预料的错误。
有了 双亲委派机制 来加载类,将会保证,JVM、JDK中的类永远不会被恶意加载进JVM中,有效保证了安全性。
怎么解析和存入JVM:
1. 链接:
校验:校验Class文件的字节流中包含的信息符合当前虚拟机的要求;
准备:为类static、final变量分配内存,并设置默认值,即在方法区中分配这些变量所使用的内存空间;
解析: 将符号引用转换成直接引用;
2. 初始化:client方法的执行,client方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。
存入JVM的内容是什么样的东西,结构是什么样子:
存入的是描述此类信息的元信息对象(程序中就是我们常见的Class对象)。通过这个元信息对象,可以获得类的结构信息,如构造函数、成员变量、方法等信息。
反射机制:
通过类信息的元信息对象(程序中就是我们常见的Class对象),可以获得类的结构信息。这种机制就是Java反射机制。
为什么要用反射?因为反射很灵活,可以在不确定是哪个类的实例对象的情况下,就可以给实例对象的字段赋值,调用实例对象的方法。
比如:
/**********下面是Wheel接口及其两个实现类*****************************/
public interface Wheel {
void trun();
}
/**
* 后驱轮子的实现类
*/
public class RearWheel implements Wheel{
@Override
public void trun() {
System.out.println("rear trun!");
}
}
/**
* 前驱轮的实现类
*/
public class PrecursorWheel implements Wheel{
@Override
public void trun() {
System.out.println("precursor trun!");
}
}
现在有一个Car类:
/**
* Car类是一个调用类,其有一个成员变量wheel,依赖于Wheel接口
*/
public class Car {
private Wheel wheel;
/**
* Car类中通过反射,可以让代码变得灵活
* 在程序运行阶段,在使用 Car类的对象时,通过反射,动态指定,Car类依赖哪个Wheel接口的实现类
* @param className
*/
public void setWheelObj(String className){
try {
//获取类的元信息对象
Class clazz = Class.forName(className);
//获取构造函数对象
Constructor constructor = clazz.getDeclaredConstructor((Class[])null);
/**调用构造函数来获得实例化对象**/
Object obj = constructor.newInstance();
wheel = (Wheel) obj;
} catch (Exception e) {
e.printStackTrace();
}
}
public void run(){
wheel.trun();
}
}
Car类中 通过 setWheelObj方法,可以动态指定 Car类到底依赖Wheel接口的哪个实现类,不管 Wheel接口如何扩展、增加实现类等,Car类都不需要修改。
测试一下:
public static void main(String[] args) {
Car car = new Car();
car.setWheelObj("com.blue.module.ioc.PrecursorWheel");
car.run();
}
发现 car对象 是前驱车。
Java反射还可以获得其他信息,甚至能访问 类的 私有方法和私有构造函数。如:
/**
* 因为上面的构造方法时私有的,java语言访问时不允许创建类的实例对象
* 在运行的时候会出现异常,所以我们需要一个方法去强制取消java语言的访问检查
* public void setAccessible(boolean flag)
* 其中flag值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查
*/
constructor.setAccessible(true);
/**获取成员方法**/
Method method = clazz.getMethod("methodName",String.Class);
//调用反射的方法(假设方法无参,有参就在obj后面继续添加)
method.invoke(obj);
/**获取成员变量**/
Field field = clazz.getField("fieldName");
//设置成员变量值
field.set(obj,"fieldValue");
/**获取所有成员方法和成员变量**/
Method[] methods = clazz.getMethods();
Field[] fields = clazz.getFields();