类加载器
- 类加载器简介
- 类加载机制
- 创建并使用自定义的类加载器
- URLClassLoader类
类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。
1.类加载器简介
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。
正如一个对象又唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用全限定类名和其类加载器作为其唯一标识。
当JVM启动时,会形成有三个类加载器组成的初始类加载层次结构。
①Bootstrap ClassLoader:根类加载器
②Extension ClassLoader:扩展类加载器
③System ClassLoader:系统类加载器
Bootstrap ClassLoader被称为引导(也称为原始或根)类加载器,它负责加载Java的核心类。根类加载器非常特殊,它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的,可以加载核心类库,这就是我们可以直接使用String,System这些核心类库(因为这些核心类库都在jdk/jre/lib/rg.jar文件中)。
Extension ClassLoader被称为扩展类加载器,它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中的JAR包的类,通过这种方式,就可以为Java扩展核心类以外的新功能,只要把自己开发的类打包成JAR文件,然后放入JAVA_HOME/jre/lib/ext路径即可。
System ClassLoader被称为系统(也称为应用)类加载器,他负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都已类加载器作为父加载器。
2.类加载机制
JVM的类加载机制主要有如下三种:
①全盘负责。所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
②父类委托。所谓父类委托,则是先让parent(父)类加载器视图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
③缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类的二进制数据,并将其转换成Class对象,存入缓存区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
PS:类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系。
除了可以使用Java提供的类加载器之外,开发和也可以实现自己的类加载器,自定义的类加载器通过集成ClassLoader来实现。JVM中这4种类加载器的层次结构如图:
根类加载器<————扩展类加载器<————系统类加载器<————用户类加载器
public class ClassLoaderPropTest
{
public static void main(String[] args)
throws IOException
{
// 获取系统类加载器
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器:" + systemLoader);
/*
获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定
如果操作系统没有指定CLASSPATH环境变量,默认以当前路径作为
系统类加载器的加载路径
*/
Enumeration<URL> em1 = systemLoader.getResources("");
while(em1.hasMoreElements())
{
System.out.println(em1.nextElement());
}
// 获取系统类加载器的父类加载器:得到扩展类加载器
ClassLoader extensionLader = systemLoader.getParent();
System.out.println("扩展类加载器:" + extensionLader);
System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的parent: "+ extensionLader.getParent());
}
}
从运行结果可以看出,系统类加载器的加载路径是程序运行的当前路径,扩展类加载器的加载路径是D:\Java\jdk\jre\lib\ext,但此处看到扩展类加载器的父加载器是null,并不是根类加载器。这是因为根类加载器并没有继承ClassLoader抽象类,所以扩展类加载器的getParent()方法返回null.但实际上,扩展类加载器的父类夹杂器是根类加载器,只是根类加载器并不是Java实现的。
3.创建并使用自定义的类加载器
JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。查阅API文档中关于ClassLoader的方法不难发现,ClassLoader中包含了大量的portected方法——这些方法都可以别子类重写。
ClassLoader类有如下两个关键方法:
①loadClass(String name,boolean resolve):该方法为ClassLoader的入口点,根据指定名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。
②findClass(String name):根据指定名称来查找类。
如果需要实现自定义的ClassLoader,则可以通过重写以上两个方法来实现,通常推荐重写findClass()方法,而不是重写loadClass()方法。loadClass()方法的执行步骤如下:
①用findLoadedClass(String)来检查是否已经加载类,如果已经加载则直接返回;
②在父类加载器上调用loadClass()方法。如果父类加载器为null,则使用根类加载器来加载;
③调用findClass(String)方法查找类。
从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂。
例子:下面程序开发了一个自定义的ClassLoader,该ClassLoader通过重写findClass()方法来实现自定义的类加载器机制。这个ClassLoader可以在加载类之前先编译该类的源文件,从而实现运行Java之前先编译该程序的目标,这样即可通过该ClassLoader直接运行Java源文件。
public class CompileClassLoader extends ClassLoader
{
// 读取一个文件的内容
private byte[] getBytes(String filename) throws IOException
{
File file = new File(filename);
long len = file.length();
byte[] raw = new byte[(int)len];
try(
FileInputStream fin = new FileInputStream(file))
{
// 一次读取class文件的全部二进制数据
int r = fin.read(raw);
if(r != len)
throw new IOException("无法读取全部文件:"+ r + " != " + len);
return raw;
}
}
// 定义编译指定Java文件的方法
private boolean compile(String javaFile)throws IOException
{
System.out.println("CompileClassLoader:正在编译 " + javaFile + "...");
// 调用系统的javac命令
Process p = Runtime.getRuntime().exec("javac " + javaFile);
try
{
// 其他线程都等待这个线程完成
p.waitFor();
}
catch(InterruptedException ie)
{
System.out.println(ie);
}
// 获取javac线程的退出值
int ret = p.exitValue();
// 返回编译是否成功
return ret == 0;
}
// 重写ClassLoader的findClass方法
protected Class<?> findClass(String name)throws ClassNotFoundException
{
Class clazz = null;
// 将包路径中的点(.)替换成斜线(/)。
String fileStub = name.replace("." , "/");
String javaFilename = fileStub + ".java";
String classFilename = fileStub + ".class";
File javaFile = new File(javaFilename);
File classFile = new File(classFilename);
// 当指定Java源文件存在,且class文件不存在、或者Java源文件
// 的修改时间比class文件修改时间更晚,重新编译
if(javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified()))
{
try
{
// 如果编译失败,或者该Class文件不存在
if(!compile(javaFilename) || !classFile.exists())
{
throw new ClassNotFoundException(
"ClassNotFoundExcetpion:" + javaFilename);
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
// 如果class文件存在,系统负责将该文件转换成Class对象
if (classFile.exists())
{
try
{
// 将class文件的二进制数据读入数组
byte[] raw = getBytes(classFilename);
// 调用ClassLoader的defineClass方法将二进制数据转换成Class对象
clazz = defineClass(name,raw,0,raw.length);
}
catch(IOException ie)
{
ie.printStackTrace();
}
}
// 如果clazz为null,表明加载失败,则抛出异常
if(clazz == null)
{
throw new ClassNotFoundException(name);
}
return clazz;
}
// 定义一个主方法
//java CompileClassLoader Hello XXX
public static void main(String[] args) throws Exception
{
// 如果运行该程序时没有参数,即没有目标类
if (args.length < 1)
{
System.out.println("缺少目标类,请按如下格式运行Java源文件:");
System.out.println("java CompileClassLoader ClassName");
}
// 第一个参数是需要运行的类
String progClass = args[0];
// 剩下的参数将作为运行目标类时的参数,
// 将这些参数复制到一个新数组中
String[] progArgs = new String[args.length-1];
System.arraycopy(args , 1 , progArgs, 0 , progArgs.length);
CompileClassLoader ccl = new CompileClassLoader();
// 加载需要运行的类
Class<?> clazz = ccl.loadClass(progClass);
// 获取需要运行的类的主方法
Method main = clazz.getMethod("main" , (new String[0]).getClass());
Object[] argsArray = {progArgs};
main.invoke(null,argsArray);
}
}
public class Hello
{
public static void main(String[] args)
{
for (String arg : args)
{
System.out.println("运行Hello的参数:" + arg);
}
}
}
4.URLClassLoader类
Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类(此处的父类,就是指类与类之间的继承关系)。URLClassLoader功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。
一旦得到了URLClassLoader对象之后,就可以调用该对象的loadClass()方法来加载指定类。下面程序示范了如何直接从文件系统中加载MySQL驱动,并使用该驱动来获取数据库连接。通过这种方式来实现数据库连接,可以无须将MySQL驱动添加到CLASSPATH环境变量中。
public class URLClassLoaderTest
{
private static Connection conn;
// 定义一个获取数据库连接方法
public static Connection getConn(String url ,String user , String pass) throws Exception
{
if (conn == null)
{
// 创建一个URL数组
URL[] urls = {new URL("file:mysql-connector-java-5.1.30-bin.jar")};
// 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
// 加载MySQL的JDBC驱动,并创建默认实例
Driver driver = (Driver)myClassLoader.loadClass("com.mysql.jdbc.Driver").newInstance();
// 创建一个设置JDBC连接属性的Properties对象
Properties props = new Properties();
// 至少需要为该对象传入user和password两个属性
props.setProperty("user" , user);
props.setProperty("password" , pass);
// 调用Driver对象的connect方法来取得数据库连接
conn = driver.connect(url , props);
}
return conn;
}
public static void main(String[] args)throws Exception
{
System.out.println(getConn("jdbc:mysql://localhost:3306/mysql", "root" , "32147"));
}
}
正如前面所看到的,创建URLClassLoader时传入了一个URL数组参数,该ClassLoader就可以从这系列URL指定的资源中加载指定类,这里的URL可以以 file:为前缀,表明从本地文件系统加载;可以以http:为前缀,表明互联网通过HTTP访问来加载;也可以以ftp:为前缀,表明从互联网通过FTP访问来加载等等,功能非常强大。