阅读文本大概需要3分钟。
类装载器是 Java 中的一项创新,它使得 Java 虚拟机可以在执行的过程中再把一个 Java 类读入虚拟机,提高了程序的灵活性。在Java中,类的信息是被保存在方法区中的。在介绍类装载器之前,我们先了解一下 Java 中类的生命周期。Java 中一个类的生命周期可以划分为以下 6 个步骤:
<clinit>()
,则执行此方法(初始化方法由编译器生成,程序员不可手动在 Java 源代码中添加);new
, newInstance
, clone
, getObject
,则意味着需要在堆内存中创建一个对象,创建对象时会调用到<init>()
方法(对应类的构造方法),初始化方法执行前必须先调用父类的初始化方法;void finalize()
方法(如果定义了该方法的话);其中,以上的 1 ~ 3 步可以统称为类的初始化,类的初始化只可能在以下 5 种情况中发生(类初始化只会执行一次,如果类已经初始化了并且没有被卸载,则下次使用时不需要再进行初始化):
new
, getstatic
, putstatic
, invokestatic
关键字的时候;java.lang.reflect
包对类进行反射调用的时候;main()
方法的那个类会被初始化;java.lang.invoke.MethodHandle
实例的最后解析结果是 REF_getStatic
, REF_putStatic
, REF_invokeStatic
的方法句柄的时候;
这篇文章所要讨论的类装载器对应的是类的生命周期中的第一步:装载。
顾名思义,类装载器的作用就是把一个Java的字节码数据加载到JVM中,并且生成一个java.lang.Class
类的实例。每个这样的实例用来表示一个 Java 类,通过此实例的 newInstance()
方法就可以创建出该类的一个对象。
我们可以通过java.lang.ClassLoader
类来对一个字节码数据进行加载,该类主要包含以下和类加载相关的方法:
方法 | 说明 |
---|---|
getParent() | 返回该类加载器的父类加载器 |
loadClass(String name) | 加载名称为 name的类,返回的结果是 java.lang.Class类的实例 |
findClass(String name) | 查找名称为 name的类,返回的结果是 java.lang.Class类的实例 |
findLoadedClass(String name) | 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的 |
resolveClass(Class<?> c) | 链接指定的 Java 类 |
其中最核心的就是方法是defineClass
方法,它负责把一连串的字节码二进制数据转化为一个Class
类的实例,而不论的这些字节码来自于什么地方。
系统类加载器是由 JVM 提供的、可以直接使用的类加载器,JVM中的系统类加载器有如下三个:
%JRE_HOME/lib/
目录下的rt.jar
、resources.jar
、charsets.ja
r和class
等,如果想要使用引导类加载器来加载我们自己的jar包,可以使用如下的方式来实现
我们可以在运行时使用如下参数:
-Xbootclasspath:完全取代系统Java classpath.最好不用。
-Xbootclasspath/a: 在系统class加载后加载。一般用这个。
-Xbootclasspath/p: 在系统class加载前加载,注意使用,和系统类冲突就不好了.
win32 java -Xbootclasspath/a: some.jar;some2.jar; -jar test.jar
unix java -Xbootclasspath/a: some.jar:some2.jar: -jar test.jar
win32系统每个jar用分号隔开,unix系统下用冒号隔开
%JRE_HOME/lib/ext
目录下的jar和class文件,你可以把需要加载的jar都扔到%JRE_HOME%/lib/ext
下面,这个目录下的jar包会在Bootstrap Classloader工作完后由Extension Classloader来加载。ClassLoader.getSystemClassLoader()
来获取它。如果想要让指定的jar被加载,只需要在MANIFEST.MF
中添加如下代码:Class-Path: lib/demo.jar lib/demo1.jar
,就可以把指定的jar添加到CLASSPATH中了。其中,Bootstrap ClassLoader由JVM启动,然后初始化sun.misc.Launcher ,sun.misc.Launcher初始化Extension ClassLoader、App ClassLoader。除了Bootstrap ClassLoader,其余的类加载器本身也由其它的类加载器进行加载,所以某个类加载器的父类加载器就是加载了这个类加载器的那个加载器。在JVM提供的加载器中,系统类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是引导类加载器。
如果JVM提供的类加载器无法满足我们的需求,那我们就需要实现自己的类加载器。
自定义类加载器十分简单,只需要通过调用ClassLoader
类的Class<?> java.lang.ClassLoader.defineClass(String name, byte[] b, int off, int len)
方法即可,不过由于该方法是final
并且protected
的,所以我们必须要继承ClassLoader
类并且使用super.xxx()
的格式来调用。下面是一个demo
import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Method;
public class MyClassloader {
public static void main(String[] args) throws Exception {
ClassLoaderSub classLoaderSub = new ClassLoaderSub();
Class<?> clazz = classLoaderSub.getClassByFile("C:\\Users\\admin\\Desktop\\A.class");
System.out.println("类的名字:" + clazz.getName());
System.out.println("类的加载器:" + clazz.getClassLoader());
Object obj = clazz.newInstance();
Method method = clazz.getMethod("test");
method.invoke(obj, null);
}
}
class ClassLoaderSub extends ClassLoader {
/**
* 调用父类的 defineClass 来生成 Class 实例
* @param name
* @param b
* @param off
* @param len
* @return
*/
public Class<?> defineClassByName(String name, byte[] b, int off, int len) {
Class<?> clazz = super.defineClass(name, b, off, len);
return clazz;
}
/**
* 读入字节码文件并把其转换为字节数组
*
* @param fileName
* @return
* @throws Exception
*/
@SuppressWarnings("finally")
public Class<?> getClassByFile(String fileName) throws Exception {
File classFile = new File(fileName);
byte bytes[] = new byte[1024 * 100];
FileInputStream fis = null;
Class<?> clazz = null;
try {
fis = new FileInputStream(classFile);
int j = 0;
while (true) {
int i = fis.read(bytes);
if (i == -1)
break;
j += i;
}
clazz = defineClassByName(null, bytes, 0, j);
} finally {
fis.close();
return clazz;
}
}
}
A.class的内容很简单,编译前的源码如下:
public class A {
public void test() {
System.out.println("我被加载成功并且方法执行了!");
}
}
执行main()
方法,打印出以下内容:
类的名字:A
类的加载器:ClassLoaderSub@15db9742
我被加载成功并且方法执行了!
以上代码的主要功能就是把 A.class
的字节码读入到JVM中并且创建一个对应该字节码所对应的类的Class实例。然后根据该类来创建一个该类的对象并且调用其test()
方法,方法成功执行。自定义的类加载器的核心组件就是defineClass
方法,这个需要重点理解。
如果把JVM类加载器和自定义类加载器结合起来看的话,那么会构成一个继承的层次结构。我们已经知道,JVM的三个类加载器有继承关系,那么加上自定义类加载器之后继承关系会变成什么样呢,下面这张图很清晰的描述了这种结构
由于这种目录结构,JVM提出了类加载器的双亲委派机制,即
null
,那么就直接调用bootstrap class loader
来进行类加载操作),一直向上直到bootstrap class loader
被调用了,那么bootstrap class loader
不会再调用父类加载器(也没有可以调用的),而是会自己对该类进行加载;bootstrap class loader
的类加载操作失败了,那么就会调用其子类加载器进行加载;如果还是失败,就继续向下调用,直到成功为止。如果一直无法成功,则会抛出找不到类的异常。双亲委派机制保证了JVM的安全性,因为恶意程序无法把自己伪造成JVM所信任的类。例如,我伪造了一个java.lang.Object
类,想让JVM把它加载进去,但是由于双亲委派机制的存在,JVM默认会使用bootstrap class loader
来加载java.lang.Object
类,而因为bootstrap class loader
默认会加载%JRE_HOME/lib/
下的 java.lang.Object
文件,所以我的攻击自然失效。
那么,如果我更换一种攻击方式呢。我想让启动类加载器加载一个由我书写的名为java.lang.Attack
的带有攻击代码类,那么我的攻击能成功吗?答案是不能。因为对于不同的类加载器所加载的类,它们将属于不同的运行时包。运行时包这个词在《Java虚拟机规范第2版》中第一次出现,如果两个类是由不同的类加载器进行加载的,那么他们就不可以进行相互访问。更典型的,如果我使用了两个类加载器加载了同一个类,那么这两个类是不一样的,如果让这两个类之中的某一个类的对象由另一个类来进行强制类型转换,会产生异常。
Class.forName()
是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)
和 Class.forName(String className)
。第一种形式的参数 name表示的是类的全名,initialize表示是否是初始化类,loader表示加载时使用的类加载器;第二种形式则相当于设置了参数 initialize的值为 true,loader的值为当前类的类加载器。
Class.forName()
方法本身已经包含了类的加载过程。除此之外,Class.forName()
还包括了第0节中的第2、3步操作,也就是说Class.forName()
方法不仅会加载一个类,还会初始化这个类。这个方法一般被用于加载数据库的驱动,我们可以打开MySQL的驱动com.mysql.jdbc.Driver
的源码看一下,可以发现如下代码:
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
如上static代码块中的代码仅会在类初始化时才能执行,所以只能使用Class.forName()
方法才能加载数据库的驱动。如果单纯的使用ClassLoader来加载数据库驱动,因为缺失了类初始化的操作,所以驱动加载将会失败。
本文链接:http://www.nosuchfield.com/2017/10/15/Spring-Boot-Starters/