类加载器

    xiaoxiao2026-01-01  2

    什么是类加载器?

    1、稍微了解一下类加载器

          类加载器从 JDK 1.0 就出现了,最初是为了满足 Java Applet 的需要而开发出来的。Java Applet 需要从远程下载 Java 类文件到浏览器中并执行。现在类加载器在 Web 容器和 OSGi 中得到了广泛的使用。一般来说,Java 应用的开发人员不需要直接同类加载器进行交互。Java 虚拟机默认的行为就已经足够满足大多数情况的需求了。不过如果遇到了需要与类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就很容易花大量的时间去调试ClassNotFoundException和NoClassDefFoundError等异常。本文将详细介绍 Java 的类加载器,帮助读者深刻理解 Java 语言中的这个重要概念。下面首先介绍一些相关的基本概念。

    2、什么是类加载器

         顾名思义,类加载器是用来将java类加载到java虚拟机(jvm)中的。它是Java语言的一个创新,也是Java语言流行的重要原因之一。它使得Java类可以被动态加载到Java虚拟机中并执行。

         一般来说,Java虚拟机使用Java类的方式如下:Java源程序(.java文件)---->经过编译之后---->变成java字节码(.class文件)。类加载器(ClassLoader)负责读取java字节码,并转换成java.lang.Class类的一个实例,每个这样的一个实例用来表示一个java类,通过此实例的newInstance()方法就可以创建出该类的一个对象。

    类加载器的作用

            Java 类加载器的作用就是在运行时加载类(把类的二进制加载到内存中),它可以在将类加载到虚拟机中的时候检查类的完整性。

    java虚拟机中三个主要的类加载器

    1、

          java虚拟机中可以安装多个类加载器,系统默认三个主要的类加载器,每个类加载器负责加载特定位置的类。他们分别是:

    BootStrap、ExtClassLoader、AppClassLoader

         加载器也是一个java类,由于java类要由类加载器来加载,那第一个类加载器又是由谁来加载它呢?这个加载器是BootStrap。

         BootStrap------它不是一个Java类,他是嵌套在jvm(java虚拟机)中的一个用c++编写的二进制代码。

    2、获得自定义类的类加载器

      //得到我们自定义类的字节码,得到此字节码的类加载器,得到类加载器的字节码,得到字节码的名字   System.out.println(MyClassLoaderTest.class.getClassLoader().getClass().getName());   //得到我们自定义类的字节码,得到此字节码的类加载器,得到类加载器的字节码   System.out.println(MyClassLoaderTest.class.getClassLoader().getClass());

    运行输出:

    sun.misc.Launcher$AppClassLoader class sun.misc.Launcher$AppClassLoader

    3、获得System系统类的类加载器

    运行下面代码:

    //得到系统类的字节码,得到此字节码的类加载器 System.out.println(System.class.getClassLoader());-------------------------------------------------1

    //得到系统类的字节码,得到此字节码的类加载器,得到类加载器的字节码,得到字节码的名字 System.out.println(System.class.getClassLoader().getClass().getName());

    运行输出:

    null                                         

    Exception in thread "main" java.lang.NullPointerException at shipin44.MyClassLoaderTest.main(MyClassLoaderTest.java:16)

    程序报空指针异常。

    位置1的代码,运行结果为null,不代表它没类加载器,(如果没有那是谁加载它的呢?)而是加载它的加载器是BootStrap(一个特殊的类加载器,是c++写的二进制代码),BootStrap不是一个java对象,所以上面打印null。

    4、BootStrap、ExtClassLoader、AppClassLoader之间的关系和它们分别加载什么位置的类。

    4-1、类之间的关系和管辖范围图

    4-2、用代码看类加载器的关系示例代码:

    public static void main(String[] args) {      ClassLoader cl = MyClassLoaderTest.class.getClassLoader();   while (cl!=null) {    System.out.println(cl.getClass().getName());    //cl本来就是类加载器了,下面这样的写法是得到父加载器的父加载器多跳了1级    cl = cl.getParent().getClass().getClassLoader();    }   //打印第一个类加载器   System.out.println(cl);   System.out.println("------------正确输出-------------");   cl = MyClassLoaderTest.class.getClassLoader();   while (cl!=null) {    System.out.println(cl.getClass().getName());      cl = cl.getParent();    }   //打印第一个类加载器   System.out.println(cl);  }

    输出结果:

    sun.misc.Launcher$AppClassLoader null ------------正确输出------------- sun.misc.Launcher$AppClassLoader sun.misc.Launcher$ExtClassLoader null

    5、用代码看看ExtClassLoader类加载器,是加载什么位置的类。

    鼠标右键点击MyClassLoaderTest类--->选Export--->java--->JAR file--->Next--->Finish,然后将导出的包copy到jre\lib\ext目录下

    运行输出:

    ------------正确输出------------- sun.misc.Launcher$ExtClassLoader null

    zf.jar包中的MyClassLoaderTest类中的代码如下:

    public static void main(String[] args) {

      ClassLoader cl = MyClassLoaderTest.class.getClassLoader();

      System.out.println("------------正确输出-------------");   cl = MyClassLoaderTest.class.getClassLoader();   while (cl!=null) {    System.out.println(cl.getClass().getName());      cl = cl.getParent();    }   //打印第一个类加载器   System.out.println(cl);  }

    类加载器的委托机制

    1、什么是类加载器的委托机制

          当第一个类加载器要加载类的时候,它先不直接加载类,而是交(委托)给它的父类加载器去加载,而父类加载器又交(委托)给它的父类加载器去加载,就这样一直往上的走,当到了BootStrap这个类加载器时,它没有父加载器,然后它就到自己所管辖的范围去找,找到了就加载出来,没找到它就交给它的儿子加载器去加载,这时候它的儿子加载器才会去管辖的范围找,如果又没找到,那就又给儿子找,就这样一直往下,当最后回到第一个类加载器的时候,如果还是没找到的话就报异常。

          图文解说(2级标题)

    2、类加载器的委托机制有什么好处?

          它的好处是可以集中管理,有这样一个情况,我有一个类要加载,这时候MyClassLoader1找到了这个类并加载了此类,而MyClassLoader2也找到了这个类也加载了此类,这时候内存中就存在了2个一样的字节码,这样就很浪费资源。

    3、当Java虚拟机要加载一个类时,到底调用哪个加载器去加载呢?

           1-1、首先它会调用当前线程的类加载器去加载线程中的第一个类。

                    Thread a = Thread.currentThread();                 System.out.println(a.getContextClassLoader().getClass().getName());

                    运行上面的代码输出:

                    sun.misc.Launcher$AppClassLoader

           1-2、如果类A中引用了类B,Java虚拟机将使用加载类A的加载器来加载类B。        

           1-3、还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。

                   Thread a = Thread.currentThread();                System.out.println(a.getContextClassLoader().getClass().getName());                a.setContextClassLoader(System.class.getClassLoader());---------------------------------这里不写.getClass().getName())是因为null没有字节码,写了程序报空指针异常。                System.out.println(a.getContextClassLoader());

                   运行上面的代码输出:

                   sun.misc.Launcher$AppClassLoader                null

    4、关于类加载器的一道面试题。

          问:能不能自己写一个类叫java.lang.System?

          答:一把情况下不能,因为委托机制的原因,当你写了这个类时,到加载的时候,流程会一直想上走,当到BootStrap时,它发现自己的管辖范围有,于是他就直接加载一个System给你了,而这个System是rt.jar中的类。如果自己写个类加载器就可以了,或者类名相同但是包名不相同也是可以的,但是不能类名和包名都相同。比如楼主可以写个类叫:com.lang.String。

    编写自己的类加载器------------ ( 有包名的类不能调用无包名的类。在帮助文档中一般看到protect方法多的就要想到用extends)

    1、编写自定义的类加载器需要注意的地方。

         1-1、自定义的类加载器必须继承ClassLoader

         1-2、一般尽量不要覆写已有的loadClass()方法中的委托逻辑

                  一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法相比而言,明确提示开发者在开发自定义的类加载器时覆findClass(…)逻辑

    2、findClass、defineClass和loadClass(这里与之相关的设计模式是-----模板方法模式)

            如果,覆盖loadClass方法,程序将不会用委托机制在创建代码,所以一般都覆盖findClass方法。

    3、练习题 

    问题一:自定义一个类,然后编写一个简单的加密算法(把类的字节码中的0换成1,把1换成0),并输出到指定目录下,然后再自定义一个类加载器,用来加载加密过的类。

    1、为一个原始类文件加密,并得到加密过后的类文件。

    创建一个用来加密的原始类,此类继承java.util.Date。并重写toString()方法。代码如下

    package shipin44;

    import java.util.Date;

    public class ClassLoaderAttachment extends Date {  public String toString(){   return "hello,itcast";  } }

    编写加密方法。代码如下:

    /**   * 将传过来的二进制加密(把1变成0,把0变成1)   * cypher:暗号的意思   * @param ips:超类,为了能传多种输入流(里氏代换原则)   * @param ops:超类,为了能传多种输出流(里氏代换原则)   * @throws Exception   */  public static void cypher(InputStream ips,OutputStream ops) throws Exception{   int b = -1; //  ips.read();帮助文档中的解释 //  read //  public abstract int read() //                    throws IOException从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。在输入数据可用、检测到流末尾或者抛出异常前,此方法一直阻塞。 //  子类必须提供此方法的一个实现。  //  返回:下一个数据字节;如果到达流的末尾,则返回 -1。  //  抛出: IOException - 如果发生 I/O 错误。   while ((b = ips.read()) != -1) {    ops.write( b ^ 0xff);    //System.out.println(b+"\t");//打印输出的内容看不懂    //System.out.println(b ^ 0xff);//打印输出的内容看不懂   }  }

    编写执行加密操作的方法。代码如下

    /**   * 开始做加密文件的工作   * @param args:调用min方法时传进来的参数   * @throws Exception    */  public static void doWorkCypher(String[] args) throws Exception{         //源文件路径   String srcPath = args[0];   //文件所在的目录   String destDir = args[1];   //获得文件名   String destFileName = srcPath.substring(srcPath.lastIndexOf('\\')+1);   //目标文件的相对路径   String destPath = destDir + "\\" + destFileName;         //输入流,把东西写入内存中,srcPath原文件的绝对路径   FileInputStream fis = new FileInputStream(srcPath);   //输入流,把东西从内存中写出来,写到硬盘上,destPath加密后的文件的相对路径   FileOutputStream fos = new FileOutputStream(destPath);   //加密类   cypher(fis, fos);   //关闭流   fis.close();   //关闭流   fos.close();  }

    编写main方法。

    public static void main(String[] args) throws Exception{   // TODO Auto-generated method stub   doWorkCypher(args); }

    调用main方法

    1、鼠标右键点工程--->new--->Folder--->输入itcast

    2、鼠标右键点以上方法所属的类--->Run As--->Java Application,程序报错,因为调用min方法时没传相应的参数进去。

    3、鼠标右键点以上方法所属的类--->Run As--->Run Confugurarion--->Arguments--->Program(节目) Arguments(参数)--->输入ClassLoaderAttachment.class所在的绝对路径--->空格--->再输入上面的Folder名--->点apply--->最后Run。

    4、最后点项目工程F5刷新后,会发现itcast文件夹下多了个ClassLoaderAttachment.class文件,此文件是加过密的文件。

    测试结果

    1、在一个min方法中打印System.out.println(new ClassLoaderAttachment().toString());,输出结果是:

    hello,itcast

    这时候的加载的类是原始的类

    2、用itcast文件夹下的ClassLoaderAttachment.class,把F:\java\BianChengRuanJian\myeclipseworkspace\zhangxiaoxiangjichujiaqiang\bin\shipin44\ClassLoaderAttachment.class目录下的原始.class(未加过密的)替换掉,运行System.out.println(new ClassLoaderAttachment().toString());,程序报错。

    2、编写自定义加载器,加载加过密的文件,并解密。

    编写自定义类加载器,此类必须继承ClassLoader。

    重写findClass方法,不能重写loadClass方法。代码如下:

    //目录名(itcast)  String classDir;  /**   * 重写findClass方法   */  @Override  protected Class<?> findClass(String name) throws ClassNotFoundException {   // TODO Auto-generated method stub     String classFileName = classDir + "\\" + name + ".class";   try {    FileInputStream fis = new FileInputStream(classFileName);

    //加密的.class的绝对路径,这样也可以,这位置的ClassLoaderAttachment.class是加过密的 //  classFileName = "C:\\Users\\JSON\\Desktop\\ClassLoaderAttachment.class";    ByteArrayOutputStream bos = new ByteArrayOutputStream();    cypher(fis, bos);    fis.close();    byte[] bytes = bos.toByteArray();    return defineClass(bytes, 0, bytes.length);   } catch (Exception e) {    // TODO Auto-generated catch block    e.printStackTrace();   }   return super.findClass(name);  }

     //构造函数  public MyClassLoader(String classDir){   this.classDir = classDir;  }

    测试结果

    1、在一个min方法中运行System.out.println(new ClassLoaderAttachment().toString());,打印输出:

    hello,itcast

    2、将加过密后的ClassLoaderAttachment.class替换掉未加过密的ClassLoaderAttachment.class文件。在myeclipseworkspace\zhangxiaoxiangjichujiaqiang\bin\shipin44目录下运行程序报错,如下图:

    3、用自定义类加载器MyClassLoader加载,加过密的ClassLoaderAttachment,在min方法中运行如下代码:

      Class clazz = new MyClassLoader("itcast").loadClass("ClassLoaderAttachment");   Date d1 = (Date) clazz.newInstance();   System.out.println(d1.getClass().getClassLoader().getClass().getName());   System.out.println(d1.getClass().getClassLoader().getParent().getClass().getName());   System.out.println(d1.toString());

       输出结果:

    shipin44.MyClassLoader sun.misc.Launcher$AppClassLoader hello,itcast

    23种常用的设计模式之----------模板方法设计模式

    1、模式定义

          模板方法模式(Template Method Pattern),定义一个操作中的算法骨架,而将一些实现步骤延迟到子类当中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。       模板方法模式是比较简单的一种设计模式, 但是它却是代码复用的一项基本的技术,在类库中尤其重要,它遵循“抽象类应当拥有尽可能多的行为,应当拥有尽可能少的数据”的重构原则。作为模板的方法要定义在父类中(并写完此方法),在方法的定义中使用抽象方法,而只看父类的抽象方法是根本不知道怎么处理的,实际具体处理的是子类,在子类中实现具体功能,因此不同的子类执行将会得出不同的实现结果,但是处理流程还是按照父类定制的方式。这就是模板方法的要义所在,定制算法骨架,让子类具体实现。

    2、使用场合

          1-1、一次性实现一个算法不变的部分,并将可变的行为留给子类来实现;       1-2、各子类中具有公共行为的时候,应该被提取出来并集中到一个公共父类中以避免代码重复。       1-3、当需要控制子类扩展的时候。模板方法在特定点调用钩子操作,这样就只允许在这些点进行扩展。

    3、每日的生活行为

          下面简单地描述一下上班族正常一天的生活行为 我们看到,每个人吃早餐和乘坐交通工具的方式都是个性化行为,但是每个人的行为框架确实一致的,那就是:起床、吃早餐、乘坐交通工具、工作。而每个人吃早餐和乘坐交通工具的行为却是要单独实现的。

    4、模板方法的静态建模图。

    5、模板方法的代码实现

    package com.demo; public abstract class AbstractPeople {  /**   * 起床   */  public void getUp(){   System.out.println("起床");  }    /**   * 吃早餐   */  public abstract void haveBreakfast();    /**   * 抽象乘坐交通工具的方法   */  public abstract void transport();    /**   * 工作   */  public void doWork(){   System.out.println("工作");  }    /**   * 模板方法(每天的行为)   */  public void dayLift(){   System.out.println("-----------------------");   getUp();   haveBreakfast();   transport();   doWork();   System.out.println("------------------------");  } } package com.demo; public class PeopleA extends AbstractPeople {    /**   * 具体吃早餐的方法   */  @Override  public void haveBreakfast() {   // TODO Auto-generated method stub   System.out.println("吃三明治,喝牛奶");  }    /**   * 具体做交通工具的方法   */  @Override  public void transport() {   // TODO Auto-generated method stub   System.out.println("开私家车上班");  } } package com.demo; public class PeopleB extends AbstractPeople {    /**   * 具体吃早餐的方法   */  @Override  public void haveBreakfast() {   // TODO Auto-generated method stub   System.out.println("喝粥,吃小菜");  }    /**   * 具体做交通工具的方法   */  @Override  public void transport() {   // TODO Auto-generated method stub   System.out.println("坐公交车上班");  } } package com.demo; public class PeopleC extends AbstractPeople {    /**   * 具体吃早餐的方法   */  @Override  public void haveBreakfast() {   // TODO Auto-generated method stub   System.out.println("吃煎饼,喝豆浆");  }    /**   * 具体做交通工具的方法   */  @Override  public void transport() {   // TODO Auto-generated method stub   System.out.println("做地铁上班");  } } package com.demo; public class Client {  /**   * @param args   */  public static void main(String[] args) {   // TODO Auto-generated method stub   AbstractPeople peopleA = new PeopleA();   AbstractPeople peopleB = new PeopleB();   AbstractPeople peopleC = new PeopleC();      peopleA.dayLift();   peopleB.dayLift();   peopleC.dayLift();  } } 运行Client类输出结果: ----------------------- 起床 吃三明治,喝牛奶 开私家车上班 工作 ------------------------ ----------------------- 起床 喝粥,吃小菜 坐公交车上班 工作 ------------------------ ----------------------- 起床 吃煎饼,喝豆浆 做地铁上班 工作 ------------------------      
    最新回复(0)