黑马程序员——类加载器

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

类加载器
一、类加载器简介
        类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。
        类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。现在的问题是,怎么样才算“同一个类”?
        在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person、pg、kl)。这意味着两个类加载器加载的同名类:(Person、pg、kl)和(Person、pg、kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
        当JVM启动时,会形成由3个类加载器组成的初始类加载器层次结构。
        》Bootstrap ClassLoader:根类加载器。
        》Extension ClassLoader:扩展类加载器。
        》System ClassLoader:系统类加载器。
        Bootstrap ClassLoader被成为引导类(也称为原始或根)类加载器,它负责加载Java的核心类。
        根类加载器非常特殊,它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的。下面程序可以获得根类加载器所加载的核心类库。
public class BootstrapTest
{
	public static void main(String[] args) 
	{
		// 获取根类加载器所加载的全部URL数组
		URL[] urls = sun.misc.Launcher.
		getBootstrapClassPath().getURLs();
		// 遍历、输出根类加载器加载的全部URL
		for (int i = 0; i < urls.length; i++) 
		{
			System.out.println(urls[i].toExternalForm());
		}
	}
}
         从程序运行结果看,可以明白为什么程序可以使用String、System这些核心类库——因为这些核心类库都在D:/Java\jdk1.7.0/jre/lib/rt.jar文件中。
         Extension ClassLoader被成为扩展类加载器,它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中的JAR包的类。
         System ClassLoader被成为系统(也称为应用)类加载器,它负责JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。

二、类加载机制
        JVM的类加载机制主要有如下3中。
        》全盘负责。所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载
            器负责载入,除非显式使用另一个类加载器来载入。
        》父类委托。所谓父类委托,则是先让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的
            类路径中加载该类。
        》缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存中搜寻该
           Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存
           中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
        类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系。如图:
https://i-blog.csdnimg.cn/blog_migrate/6fde9382d1f1992741bb3c248fd9ae55.png
         除了可以使用Java提供的类加载器之外,开发者也可以实现自己的类加载器,自定义的类加载器通过继承ClassLoader来实现。
        下面程序示范了访问JVM的类加载器。
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());
	}
}
        系统类加载器是AppClassLoader的实例,扩展类加载器是ExtClassLoader的实例。实际上,这两个类都是URLClassLoader类的实例。
        类加载器加载Class大致要经过如下8个步骤:
       (1)检测此Class是否载入过(即在缓存区中是否有此Class),如果有则直接进入第8步,否则接着执行第2步。
       (2)如果父类加载器不存在(如果没有父类加载器,则要么parent一定是根类加载器,要么本身就是根类加载器),则跳到第4步执行;如果父类加载器存在,则接着执行第3步。
       (3)请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步。
       (4)请求使用根类加载器载入目标类,如果成功载入则跳到第8步,否则跳到第7步。
       (5)当前类加载器尝试寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步。
       (6)从文件中载入Class,成功载入后跳到第8步。
       (7)抛出ClassNotFoundException异常。
       (8)返回对应的java.lang.Class对象。
        其中,第5、6步允许重写ClassLoader的findClass()方法来实现自己的载入策略,甚至重写loadClass()方法来实现自己的载入过程。
三、创建并使用自定义的类加载器
        可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。
        ClassLoader类有如下两个关键方法:
        》loadClass(String name, boolean resolve):该方法为ClassLoader的入口点,根据指导的二进制名称来加载类,系统就是调用
            ClassLoader的该方法来获取指定类对应的Class对象。
        》findClass(String name):根据二进制名称来查找类。
        如果需要实现自定义的ClassLoader,则可以通过重写以上两个方法来实现,当然重写findClass()方法交好些,loadClass()方法的执行步骤如下:
      (1)用findLoadedClass(Sftring) 来检查是否已经加载类,如果已经加载则直接返回。
      (2)在父类加载器上调用loadClass()方法。如果父类加载器为null,则使用根类加载器来加载。
      (3)调用findClass(String)方法查找类。
        从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂。
        下面程序实现自定义类加载器,运行该类可以不通过编译即可运行一个类:
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;
	}
	// 定义一个主方法
	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);
	}
}
接下来我们可以随意提供一个简单的主类,该主类无须编译就可以使用上面的自定义类加载器CompileClassLoader来运行它。
public class Hello
{ 
	public static void main(String[] args)
	{
		for (String arg : args)
		{
			System.out.println("运行Hello的参数:" + arg);
		}
	}
}
无须编译该Hello.java,可以直接使用如下命令来运行该Hello.java程序。
java CompileClassLoader  Hello  Java程序设计语言 
运行结果如下:
CompileClassLoader:正在编译 Hello.java ...
运行Hello的参数:Java程序设计语言

黑马程序员的tb_brand是指在JavaWeb基础教程中创建的一个表。这个表是用来存储品牌信息的,具体的表结构和数据类型需要和JavaBean类中的成员变量保持一致。\[1\]在这个教程中,使用了Maven来构建项目,并且使用了MyBatis作为持久层框架,通过配置pom.xml文件来引入相关依赖。\[2\] Maven是一个用于管理和构建Java项目的工具,它提供了一套标准化的项目结构、构建流程和依赖管理机制。\[3\] #### 引用[.reference_title] - *1* [【JAVAWEB开发】黑马程序员java web案例资料(含Element的删除与修改)](https://blog.csdn.net/aasd23/article/details/126940147)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [黑马程序员-MyBatis 框架-最全入门笔记、阿伟看了都得说真大、真细、真全!!!](https://blog.csdn.net/qq_57383364/article/details/128103058)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [JavaWeb——黑马程序员课程笔记](https://blog.csdn.net/King_ZACC/article/details/128573804)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值