Java面向对象系列[v1.0.0][类加载器]

类加载器负责将.class文件加载到内存中,并为它生成对应的java.lang.Class对象

类加载机制

类加载器负责加载所有的类,并未所有被载入内存中的类生成一个java.lang.Class实例,一个载入JVM的类有一个唯一的标识从而避免被重复加载,在java中一个类的唯一标识就是其全限定类名,也就是包名+类名,到了JVM中一个类用其全限定类名和其类加载器作为唯一标识。这也就是说代码中相同的类,使用不同的加载器加载到JVM中,他们也是不同的类且互相不兼容

当JVM启动的时候,会形成由三个类加载器组成的初始类加载器层次结构:

  • Bootstrap ClassLoader:根类加载器
  • Extension ClassLoader:扩展类加载器
  • System ClassLoader:系统类加载器

Bootstrap ClassLoader被称为引导类加载器也叫做根加载器,它负责加载Java的核心类,在Sun的JVM中,当执行java.exe命令时,使用-Xbootclasspath或者-D选项指定sun.boot.class.path系统属性值可以指定加载附加的类

JVM的类加载机制主要有如下三种:

  • 全盘负责:就是当一个类加载器负责加载某个Class时,该Class所有依赖和引用的其他Class也将由该类加载器负责载入,除非显示使用另外的类加载器来载入
  • 父类委托:就是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制:缓存机制会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中找该Class,如果缓存区不存在该Class对象,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区,这也是为什么修改了Class文件后,需要重启JVM,程序才会响应修改

类加载器之间的父子关系并不是类继承上的父子关系,是类加载器实例之间的父子关系
除了使用Java提供的类加载器之外,也可以使用自定义的类加载器,自定义的类加载器通过继承ClassLoader来实现
JVM中4类加载器的层次结构如图所示:
在这里插入图片描述

import java.util.*;
import java.net.*;
import java.io.*;

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());
	}
}

类加载器加载Class大致要经过如下步骤:

  • 检测此Class是否载入过,如果在如果则返回对应的java.lang.Class对象,否则
  • 如果父类加载器不存在(要么父类一定是根类加载器,要么本身就是根类加载器),则请求使用跟类加载器来载入目标类,成功载入则返回java.lang.Class对象,载入失败抛出ClassNotFoundException
  • 如果父类加载器存在,则请求使用父类加载器去载入目标类,载入成功则返回java.lang.Class对象,载入失败则当前类加载器尝试寻找Class文件(从与此ClassLoader相关的路径中寻找),如果找到则从文件中载入Class,成功则返回java.lang.Class对象,如果找不到则抛出ClassNotFoundException

自定义加载器

JVM中除了根加载器以外的所有类加载器都是ClassLoader子类的实例,可以扩展Classloader类,并重写ClassLoader所包含的方法来实现自定义的类加载器,ClassLoader类中有两个关键方法:

  • loadClass(String name, boolean resolve):该方法为ClassLoader的入口点,根据指定名称来加载类,系统就是通过调用ClassLoader的该方法来获取指定类对应的Class对象的
  • findClass(String name):根据指定名称来查找类

如果需要实现自定义的ClassLoader,则可以通过重写这两个方法来实现,loadClass()方法的执行步骤如下

  • 用findLoadedClass(String)来检查是否已经加载类,如果已经加载则直接返回
  • 在父类加载器上调用loadClass()方法,如果父类加载器为null,则使用根加载器来加载
  • 调用findClass(String)方法查找类
    从这个执行步骤可以看出,如果直接重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略,而如果重写loadClass()方法则实现逻辑更为复杂
    在ClassLoader里有一个核心方法:Class defineClass(String name, byte[] b, int off, int len)该方法负责将指定类的字节码文件就是Class文件,读入字节数组byte[] b内,并把它转换为Class对象,该字节码文件可以来源于文件、网络等
    defineClass()方法管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等等,该方法是final的无法重写,除了这个方法外,ClassLoader里还包含如下普通方法:
  • findSystemClass(String name):从本地文件系统装入文件,它从本地文件系统寻找类文件
  • static getSystemClassLoader():返回系统类加载器
  • getParent():获取该类加载器的父类加载器
  • resolveClass(Class<?> c):链接指定的类,类加载器可以使用此方法来链接类C
  • findLoadedClass(String name):如果此JVM已经加载了名为name的类,则直接返回该类对应的Class实例,否则返回null

import java.io.*;
import java.lang.reflect.*;

public class CompileClassLoader extends ClassLoader
{
	// 读取一个文件的内容
	private byte[] getBytes(String filename)
		throws IOException
	{
		var file = new File(filename);
		long len = file.length();
		var raw = new byte[(int) len];
		try (
			var 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";
		var javaFile = new File(javaFilename);
		var 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;
	}
	// 定义一个主方法
	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];
		// 剩下的参数将作为运行目标类时的参数,
		// 将这些参数复制到一个新数组中
		var progArgs = new String[args.length-1];
		System.arraycopy(args, 1, progArgs, 0, progArgs.length);
		var ccl = new CompileClassLoader();
		// 加载需要运行的类
		Class<?> clazz = ccl.loadClass(progClass);
		// 获取需要运行的类的主方法
		Method main = clazz.getMethod("main", (new String[0]).getClass());
		Object[] argsArray = {progArgs};
		main.invoke(null, argsArray);
	}
}

程序重写了findClass()方法,通过重写该方法就可以实现自定义的类加载机制,在该代码中findClass()方法先检查需要加载类的Class文件是否存在,如果不存在则先编译源文件,再调用ClassLoader的defineClass()方法来加载这个Class文件,并生成相应的Class对象
main()方法使用了反射来调用方法

public class Hello
{
	public static void main(String[] args)
	{
		for (var arg : args)
		{
			System.out.println("运行Hello的参数:" + arg);
		}
	}
}

使用上面的CompileClass Loader在命令行来运行该代码,

java CompileClassLoader Hello 我欲乘风归去

实际上使用自定义的类加载器还可以做更多

  • 执行代码前自动验证数字签名
  • 根据用户提供的密码解密代码,从而可以实现代码混淆器来避免反编译*.class文件
  • 根据用户需求来动态地加载类
  • 根据应用需求把其他数据以字节码的形式加载到应用中

URLClassLoader类

Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类
URLClassLoader类提供了两个构造器:

  • URLClassLoader(URL[] urls):使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询并加载类
  • URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父类加载器创建一个ClassLoader对象,其他功能与前一个构造器相同
import java.sql.*;
import java.util.*;
import java.net.*;

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-8.0.13.jar")};
			// 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
			var myClassLoader = new URLClassLoader(urls);
			// 加载MySQL的JDBC驱动,并创建默认实例
			var driver = (Driver) myClassLoader.loadClass("com.mysql.cj.jdbc.Driver")
											.getConstructor().newInstance();
			// 创建一个设置JDBC连接属性的Properties对象
			var 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?serverTimezone=UTC", "root", "32147"));
	}
}
// 创建一个URL数组
URL[] urls = {new URL("file:mysql-connector-java-8.0.13.jar")};
// 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
var myClassLoader = new URLClassLoader(urls);

创建了一个URLClassLoader对象,该对象使用默认的父类加载器,该类加载器类加载路径是当前路径下的mysql-connector-java-8.0.13.jar文件

// 加载MySQL的JDBC驱动,并创建默认实例
var driver = (Driver) myClassLoader.loadClass("com.mysql.cj.jdbc.Driver").getConstructor().newInstance();

使用ClassLoader的loadClass()加载指定类,并调用Class对象的newInstance()方法创建了一个该类的默认实例,也就是com.mysql.jdbc.Driver类的对象,该类则是在mysql-connector-java-8.0.13.jar文件中的,com.mysql.jdbc.Driver类的对象的实现类实现了java.sql.Driver接口,所以程序将其强制类型转换为Driver了。

如程序中所示,创建URLClassLoader时传入了一个URL数组参数,该ClassLoader就可以从这些列URL指定的资源中加载指定类,这里的URL可以以file:为前缀,表明从本地文件系统加载;可以是http:前缀,表明从互联网通过HTTP访问来加载:还可以是ftp:表明从互联网通过FTP访问来加载…

猜你喜欢

转载自blog.csdn.net/dawei_yang000000/article/details/105922411
今日推荐