• 自定义一个类加载器


    为什么要自定义类加载器

    类加载机制:http://www.cnblogs.com/xrq730/p/4844915.html

    类加载器:http://www.cnblogs.com/xrq730/p/4845144.html

    这两篇文章已经详细讲解了类加载机制和类加载器,还剩最后一个问题没有讲解,就是自定义类加载器。为什么我们要自定义类加载器?因为虽然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 order
    15             // 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 null
    com.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对象

  • 相关阅读:
    ----Vue 单页应用的首屏优化----
    ----小程序之rich-text图片宽度自适应----
    ----vue-router 如何在新窗口打开页面----
    ----element-ui实现时间控件开始时间和结束时间关联----
    ----element-ui自定义表单验证----
    ----js中的every和some----
    「Linux学习」之挂载访问samba服务
    「Linux学习」之samba和nfs共享服务搭建
    「Linux学习」之防火墙服务设置
    「linux学习」之批量创建用户脚本
  • 原文地址:https://www.cnblogs.com/xrq730/p/4847337.html
Copyright © 2020-2023  润新知