自定义类加载器

Posted 皇甫哲

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义类加载器相关的知识,希望对你有一定的参考价值。

为什么要自己定义类加载器

为什么我们要自定义类加载器?因为虽然Java中给用户提供了很多类加载器,但是和实际使用比起来,功能还是匮乏。举一个例子来说吧,主流的Java Web服务器,比如Tomcat,都实现了自定义的类加载器(一般都不止一个)。因为一个功能健全的Web服务器,要解决如下几个问题:

1、部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。

2、部署在同一个服务器上的两个Web应用程序所使用的Java类库可以相互共享。这个需求也很常见,比如相同的Spring类库10个应用程序在用,不可能分别存放在各个应用程序的隔离目录中

3、支持热替换,我们知道JSP文件最终要编译成.class文件才能由虚拟机执行,但JSP文件由于其纯文本存储特性,运行时修改的概率远远大于第三方类库或自身.class文件,而且JSP这种网页应用也把修改后无须重启作为一个很大的优势看待

由于存在上述问题,因此Java提供给用户使用的ClassLoader就无法满足需求了。Tomcat服务器就有自己的ClassLoader架构,当然,还是以双亲委派模型为基础的:

 

jdk中classloader

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

自定义类加载器

从上面对于java.lang.ClassLoader的loadClass(String name, boolean resolve)方法的解析来看,可以得出以下2个结论:

1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可

2、如果想打破双亲委派模型,那么就重写整个loadClass方法

当然,我们自定义的ClassLoader不想打破双亲委派模型,所以自定义的ClassLoader继承自java.lang.ClassLoader并且只重写findClass方法。

 第一步 写一个实体类

 

public class Person {
	private String name;

	public Person() {

	}

	public Person(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String toString() {
		return "I am a person, my name is " + name;
	}
}

 

 第二步 自定义类加载器,里面是io和nio的东西,defineClass方法可以把二进制流字节组成文件转换为一个java.lang.Class(只要二进制流符合Class文件规范)

 

package algorithm;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class MyClassLoader extends ClassLoader {

	public MyClassLoader() {
	}
	public MyClassLoader(ClassLoader parent) {
		super(parent);
	}
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		File file = getClassFile(name);
		try {
			byte[] bytes = getClassBytes(file);
			Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
			return c;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return super.findClass(name);
	}
	
	private File getClassFile(String name) {
		File file = new File("/home/tp/Person.class");
		return file;
	}
	
	private byte[] getClassBytes(File file) throws Exception {
		// 这里要读入.class的字节,因此要使用字节流
		FileInputStream fis = new FileInputStream(file);
		FileChannel fc = fis.getChannel();
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		WritableByteChannel wbc = Channels.newChannel(baos);
		ByteBuffer by = ByteBuffer.allocate(1024);
		while (true) {
			int i = fc.read(by);
			if (i == 0 || i == -1)
				break;
			by.flip();
			wbc.write(by);
			by.clear();
		}
		fis.close();
		return baos.toByteArray();
	}

}

 

 第三步 Class.forName有三个参数的重载方法,可以指定类加载器,平时使用的Class.forName使用的是Application ClassLoader

public static void main(String[] args) throws Exception {
		MyClassLoader mcl = new MyClassLoader();
		Class<?> c1 = Class.forName("algorithm.Person", true, mcl);//这里也可以用Class<?> c1 = mcl.loadClass("algorithm.Person");
		Object obj = c1.newInstance();
		System.out.println(obj);
		System.out.println(obj.getClass().getClassLoader());
	}

 

 结果是

 

I am a person, my name is null
sun.misc.Launcher$AppClassLoader@5a74b10b

 这是因为MyEclipse会把Person.java文件编译到classpath路径下面去,那么自然会用Application ClassLoader来加载了。可以把classpath下的Person.class删掉。

1、删除CLASSPATH下的Person.class,CLASSPATH下没有Person.class,Application ClassLoader就把这个.class文件交给下一级用户自定义ClassLoader去加载了

2、TestMyClassLoader类的第5行这么写"MyClassLoader mcl = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent());", 即把自定义ClassLoader的父加载器设置为Extension ClassLoader,这样父加载器加载不到Person.class,就交由子加载器MyClassLoader来加载了

相当于把Application ClassLoader给替换了。

如果把第三行修改为Class<?> c1 = Class.forName("algorithm.Person");就会报ClassNotFoundException,因为给出的几种类加载器范围内都没有该class,如果不指定使用怎么样的类加载器,就会导致找不到class。

 

ClassLoader.getResourceAsStream(String name)方法作用classloader的getResourceAsStream(String name)方法用来读入指定的资源的输入流,并且把该输入流给用户使用,资源可以是图像,声音,.properties文件等,资源名称是以/分隔的标识资源名称的路径名称。

class下也有这个方法,区别在于class的这个方法是,参数不以/开头,默认从此类的.class文件所在package下取资源,否则从classpath下取。ClassLoader下默认从classpath开始取,不可以以/开头。

class调用的还是ClassLoader里的方法。

 

.class和getClass()的区别

两者都可以获得唯一的class对象。但是还是存在一定的区别

1、.class用于类名,getClass()是一个final native的方法,在类实例中调用。(调用方式不一样)

2、.class在编译期间就确定了一个类的java.lang.Class对象,但是getClass()方法在运行期间确定一个类实例的java.lang.Class对象

 

以上是关于自定义类加载器的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段——CSS选择器

VSCode自定义代码片段6——CSS选择器

面试必看-Java类加载器(自定义类加载器)

java 自定义类加载器

(转)JVM——自定义类加载器

双亲委派策略与自定义类加载器