类加载机制:https://www.toutiao.com/i6645589511092830724/
类加载器:https://www.toutiao.com/i6645590482212618760/
这两篇文章已经详细讲解了类加载机制和类加载器,还剩末了一个问题没有讲解,便是自定义类加载器。为什么我们要自定义类加载器?由于虽然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
在实现自己的ClassLoader之前,我们先看一下JDK中的ClassLoader是怎么实现的:
1 protected synchronized Class<?> loadClass(String name, boolean resolve) 2 throws ClassNotFoundException 3 { 4 // First, check if the class has already been loaded 5 Class c = findLoadedClass(name); 6 if (c == null) { 7 try { 8 if (parent != null) { 9 c = parent.loadClass(name, false);10 } else {11 c = findBootstrapClass0(name);12 }13 } catch (ClassNotFoundException e) {14 // If still not found, then invoke findClass in order15 // to find the class.16 c = findClass(name);17 }18 }19 if (resolve) {20 resolveClass(c);21 }22 return c;23 }
方法事理很大略,一步一步阐明一下:
1、第5行,首先查找.class是否被加载过
2、第6行~第12行,如果.class文件没有被加载过,那么会去找加载器的父加载器。如果父加载器不是null(不是Bootstrap ClassLoader),那么就实行父加载器的loadClass方法,把类加载要求一贯向上抛,直到父加载器为null(是Bootstrap ClassLoader)为止
3、第13行~第17行,父加载器开始考试测验加载.class文件,加载成功就返回一个java.lang.Class,加载不堪利就抛出一个ClassNotFoundException,给子加载器去加载
4、第19行~第21行,如果要解析这个.class文件的话,就解析一下,解析的浸染类加载的文章里面也写了,紧张便是将符号引用更换为直接引用的过程
我们看一下findClass这个方法:
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
是的,没有详细实现,只抛了一个非常,而且是protected的,这充分证明了:这个方法便是给开拓者重写用的。
自定义类加载器
从上面对于java.lang.ClassLoader的loadClass(String name, boolean resolve)方法的解析来看,可以得出以下2个结论:
1、如果不想冲破双亲委派模型,那么只须要重写findClass方法即可
2、如果想冲破双亲委派模型,那么就重写全体loadClass方法
当然,我们自定义的ClassLoader不想冲破双亲委派模型,以是自定义的ClassLoader继续自java.lang.ClassLoader并且只重写findClass方法。
第一步,自定义一个实体类Person.java,我把它编译后的Person.class放在D盘根目录下:
1 package com.xrq.classloader; 2 3 public class Person 4 { 5 private String name; 6 7 public Person() 8 { 9 10 }11 12 public Person(String name)13 {14 this.name = name;15 }16 17 public String getName()18 {19 return name;20 }21 22 public void setName(String name)23 {24 this.name = name;25 }26 27 public String toString()28 {29 return \"大众I am a person, my name is \"大众 + name;30 }31 }
第二步,自定义一个类加载器,里面紧张是一些IO和NIO的内容,其余把稳一下defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class----只要二进制字节流的内容符合Class文件规范。我们自定义的MyClassLoader继续自java.lang.ClassLoader,就像上面说的,只实现findClass方法:
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(\"大众D:/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(\"大众XX.XX.XXX\"大众)都是利用的系统类加载器Application ClassLoader。写一个测试类:
1 public class TestMyClassLoader 2 { 3 public static void main(String[] args) throws Exception 4 { 5 MyClassLoader mcl = new MyClassLoader(); 6 Class<?> c1 = Class.forName(\"大众com.xrq.classloader.Person\"大众, true, mcl); 7 Object obj = c1.newInstance(); 8 System.out.println(obj); 9 System.out.println(obj.getClass().getClassLoader());10 }11 }
看一下运行结果:
I am a person, my name is nullcom.xrq.classloader.MyClassLoader@5d888759
个人的履历来看,最随意马虎出问题的点是第二行的打印出来的是\"大众sun.misc.Launcher$AppClassLoader\"大众。造成这个问题的关键在于MyEclipse是自动编译的,Person.java这个类在ctrl+S保存之后或者在Person.java文件不编辑多少秒后,MyEclipse会帮我们用户自动编译Person.java,并天生到CLASSPATH也便是bin目录下。在CLASSPATH下有Person.class,那么自然是由Application ClassLoader来加载这个.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来加载了
ClassLoader.getResourceAsStream(String name)方法浸染
ClassLoader中的getResourceAsStream(String name)实在是一个挺常见的方法,以是要写一下。这个方法是用来读入指定的资源的输入流,并将该输入流返回给用户用的,资源可以是图像、声音、.properties文件等,资源名称因此\"大众/\"大众分隔的标识资源名称的路径名称。
不仅ClassLoader中有getResourceAsStream(String name)方法,Class下也有getResourceAsStream(String name)方法,它们两个方法的差异在于:
1、Class的getResourceAsStream(String name)方法,参数不以\"大众/\"大众开头则默认从此类对应的.class文件所在的packge下取资源,以\"大众/\"大众开头则从CLASSPATH下获取
2、ClassLoader的getResourceAsStream(String name)方法,默认便是从CLASSPATH下获取资源,参数不可以以\"大众/\公众开头
实在,Class的getResourceAsStream(String name)方法,只是将传入的name进行解析一下而已,终极调用的还是ClassLoader的getResourceAsStream(String name),看一下Class的getResourceAsStrea(String name)的源代码:
1 public InputStream getResourceAsStream(String name) { 2 name = resolveName(name); 3 ClassLoader cl = getClassLoader0(); 4 if (cl==null) { 5 // A system class. 6 return ClassLoader.getSystemResourceAsStream(name); 7 } 8 return cl.getResourceAsStream(name); 9 }10 11 private String resolveName(String name) {12 if (name == null) {13 return name;14 }15 if (!name.startsWith(\"大众/\"大众)) {16 Class c = this;17 while (c.isArray()) {18 c = c.getComponentType();19 }20 String baseName = c.getName();21 int index = baseName.lastIndexOf('.');22 if (index != -1) {23 name = baseName.substring(0, index).replace('.', '/')24 +\"大众/\公众+name;25 }26 } else {27 name = name.substring(1);28 }29 return name;30 }
代码不难,该当很好理解,就不阐明了。
.class和getClass()的差异
末了讲解一个内容,.class方法和getClass()的差异,这两个比较像,我自己没对这两个东西总结前,也常弄混。它们二者都可以获取一个唯一的java.lang.Class工具,但是差异在于:
1、.class用于类名,getClass()是一个final native的方法,因此用于类实例
2、.class在编译期间就确定了一个类的java.lang.Class工具,但是getClass()方法在运行期间确定一个类实例的java.lang.Class工具