自定义类加载器
为什么要自己定义类加载器
为什么我们要自定义类加载器?因为虽然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对象