黑马程序员技术交流社区

标题: 视频问题收集第十贴之类加载器 [打印本页]

作者: xiewen    时间: 2013-5-29 22:48
标题: 视频问题收集第十贴之类加载器

1、什么是类加载器

顾名思义,类加载器(classloader)用来加载Java 类到Java 虚拟机中。一般来说,Java虚拟机 使用 Java 类的方式如下:

Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。

基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。

2、类加载器处在java中的什么位置



3、三个主要加载类

Java虚拟机中可以安装多个类加载器,系统默认三个主要的类加载器,每个类负责加载特定位置的类:
BootStrap,ExtClassLoader,AppClassLoader
·类加载器也是Java类,因为其是java类的类加载器本身也要被类加载器加载,显然必须有第一个类加载器不是java类,这正是BootStrap
·Java虚拟机中的所有类装载器采用具有父子关系的树形结构进行组织,在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象或者默认采用系统类装载器为其父级类加载

类加载器之间的父子关系和管辖范围图



作者: xiewen    时间: 2013-5-29 22:50
4、演示类加载器的树状结构

每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 getClassLoader()方法就可以获取到此引用下面通过递归调用 getParent()方法来输出全部的父类加载器。
  1. public class ClassLoaderTree {   
  2.   
  3.     public static void main(String[] args) {   
  4.         ClassLoader loader = ClassLoaderTree.class.getClassLoader();   
  5.         while (loader != null) {   
  6.             System.out.println(loader.toString());   
  7.             loader = loader.getParent();   
  8.         }   
  9.     }   
  10. }  
复制代码

输出结果

sun.misc.Launcher$AppClassLoader@9304b1  sun.misc.Launcher$ExtClassLoader@190d11

第一个输出的是 ClassLoaderTree类的类加载器,即系统类加载器。它是 sun.misc.Launcher$AppClassLoader类的实例;第二个输出的是扩展类加载器,是 sun.misc.Launcher$ExtClassLoader类的实例。需要注意的是这里并没有输出引导类加载器,这是由于有些 JDK 的实现对于父类加载器是引导类加载器的情况,getParent()方法返回 null

5、加载流程分析


当Java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?

首先当前线程的类加载器去加载线程中的第一个类。

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

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

每个类加载器加载类时,又先委托给其上级类加载器。

当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则抛ClassNotFoundException,不是再去找发起者类加载器的儿子,因为没有getChild方法,即使有,那有多个儿子,找哪一个呢?

对着类加载器的层次结构图和委托加载原理,解释先前将ClassLoaderTest输出成jre/lib/ext目录下的itcast.jar包中后,运行结果为ExtClassLoader的原因。

每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类装载器去加载类,这就是类加载器的委托模式。类装载器一级级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子孙类装载器去进行真正的加载。当回退到最初的类装载器时,如果它自己也不能完成类的装载,那就应报告ClassNotFoundException异常。


有一道面试,能不能自己写个类叫java.lang.System。

通常是不可以的,由于类加载器的委托机制,会先将System这个类一级级委托给最顶级的BootStrap,由于BootStrap在其指定的目录中加载的是rt.jar中的类,且其中有System这个类,那么就会直接加载自己目录中的,也就是Java已经定义好的System这个类,而不会加载自定义的这个System。

但是还是有办法加载这个自定义的System类的,此时就不能交给上级加载了,需要用自定义的类加载器加载,这就需要有特殊的写法才能去加载这个自定义的System类的。


6、自定义类加载器

1)、自定义的类加载器必须继承抽象类ClassLoader,要覆写其中的findClass(Stringname)方法,而不用覆写loadClass()方法。

2)、覆写findClass(Stringname)方法的原因:

是要保留loadClass()方法中的流程,因为loadClass()中调用了findClass(Stringname)这个方法,此方法返回的就是去寻找父级的类加载器。

在loadClass()内部是会先委托给父级,当父级找到后就会调用findClass(Stringname)方法,而找不到时就会用子级的类加载器,再找不到就报异常了,所以只需要覆写findClass方法,那么就具有了实现用自定义的类加载器加载类的目的。

流程:

父级-->loadClass-->findClass-->得到Class文件后转化成字节码-->defind()。

3)、编程步骤:

编写一个对文件内容进行简单加盟的程序

编写好了一个自己的类加载器,可实现对加密过来的类进行装载和解密。

编写一个程序,调用类加载器加载类,在源程序中不能用该类名定义引用变量,因为编译器无法识别这个类,程序中除了可使用ClassLoader的load方法外,还能使用放置线程的上线文类加载器加载或系统类加载器,然后在使用forName得到字节码文件。








作者: xiewen    时间: 2013-5-29 22:51
  1. import java.util.Date;  
  2.   
  3. public class ClassLoaderAttachment extends Date {  
  4.     //对此类进行加密  
  5.         public String toString(){  
  6.             return "hello world";  
  7.         }  
  8.         public static void main(String [] args){  
  9.               
  10.         }  
  11. }//自定义类加载器  
  12. import java.io.*;  
  13. //继承抽象类ClassLoader  
  14. public class MyClassLoader  extends ClassLoader {  
  15.     public static void main(String[] args) throws Exception {  
  16.         //传入两个参数,源和目标  
  17.         String scrPath = args[0];  
  18.         String destDir = args[1];  
  19.         //将数据读取到输入流中,并写入到输出流中  
  20.         FileInputStream fis = new FileInputStream(scrPath);  
  21.         String destFileName =   
  22.                 scrPath.substring(scrPath.lastIndexOf('\\')+1);  
  23.         String destPath = destDir + "\\" + destFileName;  
  24.         FileOutputStream fos = new FileOutputStream(destPath);  
  25.         //加密数据  
  26.         cypher(fis,fos);  
  27.         fis.close();  
  28.         fos.close();  
  29.     }  
  30.     //定义加密数据的方法  
  31.     private static void cypher(InputStream ips,OutputStream ops)throws Exception{  
  32.         int b = 0;  
  33.         while((b=ips.read())!=-1){  
  34.             ops.write(b ^ 0xff);  
  35.         }  
  36.     }  
  37.     //定义全局变量  
  38.     private String classDir;  
  39.     @Override//覆写findClass方法,自定义类加载器  
  40.     protected Class<?> findClass(String name) throws ClassNotFoundException {  
  41.         String classFileName = classDir + "\\" + name + ".class";   
  42.         try {  
  43.             //将要加载的文件读取到流中,并写入字节流中  
  44.             FileInputStream fis = new FileInputStream(classFileName);  
  45.             ByteArrayOutputStream bos = new ByteArrayOutputStream();  
  46.             cypher(fis,bos);  
  47.             fis.close();  
  48.             byte[] bytes = bos.toByteArray();  
  49.             return defineClass(bytes, 0, bytes.length);  
  50.               
  51.         } catch (Exception e) {  
  52.             // TODO Auto-generated catch block  
  53.             e.printStackTrace();  
  54.         }  
  55.         //如果没找到类,则用父级类加载器加载  
  56.         return super.findClass(name);  
  57.     }  
  58.     //构造函数  
  59.     public MyClassLoader(){}  
  60.     public MyClassLoader(String classDir){  
  61.         this.classDir = classDir;  
  62.     }  
  63. }  
复制代码

作者: xiewen    时间: 2013-5-29 22:57
本帖最后由 xiewen 于 2013-5-29 23:03 编辑

下面程序开发了一个自定义的ClassLoader,该ClassLoader通过重写findClass()方法来实现自定义的类加载器机制。这个ClassLoader可以在加载类之前先编译该类的源文件,从而实现运行java之前先编译该程序的目标,这样即可通过该ClassLoader直接运行java源文件

  1. import java.io.*;
  2. import java.lang.reflect.*;
  3. public class CompileClassLoader extends ClassLoader
  4. {
  5.         // 读取一个文件的内容
  6.         private byte[] getBytes(String filename)
  7.                 throws IOException
  8.         {
  9.                 File file = new File(filename);
  10.                 long len = file.length();
  11.                 byte[] raw = new byte[(int)len];
  12.                 try(
  13.                         FileInputStream fin = new FileInputStream(file))
  14.                 {
  15.                         // 一次读取class文件的全部二进制数据
  16.                         int r = fin.read(raw);
  17.                         if(r != len)
  18.                         throw new IOException("无法读取全部文件:"
  19.                                 + r + " != " + len);
  20.                         return raw;
  21.                 }
  22.         }
  23.         // 定义编译指定Java文件的方法
  24.         private boolean compile(String javaFile)
  25.                 throws IOException
  26.         {
  27.                 System.out.println("CompileClassLoader:正在编译 "
  28.                         + javaFile + "...");
  29.                 // 调用系统的javac命令
  30.                 Process p = Runtime.getRuntime().exec("javac " + javaFile);
  31.                 try
  32.                 {   
  33.                         // 其他线程都等待这个线程完成
  34.                         p.waitFor();
  35.                 }
  36.                 catch(InterruptedException ie)
  37.                 {           
  38.                         System.out.println(ie);
  39.                 }
  40.                 // 获取javac线程的退出值
  41.                 int ret = p.exitValue();
  42.                 // 返回编译是否成功
  43.                 return ret == 0;
  44.         }
  45.         // 重写ClassLoader的findClass方法
  46.         protected Class<?> findClass(String name)
  47.                 throws ClassNotFoundException
  48.         {
  49.                 Class clazz = null;
  50.                 // 将包路径中的点(.)替换成斜线(/)。
  51.                 String fileStub = name.replace("." , "/");
  52.                 String javaFilename = fileStub + ".java";
  53.                 String classFilename = fileStub + ".class";
  54.                 File javaFile = new File(javaFilename);
  55.                 File classFile = new File(classFilename);
  56.                 // 当指定Java源文件存在,且class文件不存在、或者Java源文件
  57.                 // 的修改时间比class文件修改时间更晚,重新编译
  58.                 if(javaFile.exists() && (!classFile.exists()
  59.                         || javaFile.lastModified() > classFile.lastModified()))
  60.                 {
  61.                         try
  62.                         {
  63.                                 // 如果编译失败,或者该Class文件不存在
  64.                                 if(!compile(javaFilename) || !classFile.exists())
  65.                                 {
  66.                                         throw new ClassNotFoundException(
  67.                                                 "ClassNotFoundExcetpion:" + javaFilename);
  68.                                 }
  69.                         }
  70.                         catch (IOException ex)
  71.                         {
  72.                                 ex.printStackTrace();
  73.                         }
  74.                 }
  75.                 // 如果class文件存在,系统负责将该文件转换成Class对象
  76.                 if (classFile.exists())
  77.                 {
  78.                         try
  79.                         {
  80.                                 // 将class文件的二进制数据读入数组
  81.                                 byte[] raw = getBytes(classFilename);
  82.                                 // 调用ClassLoader的defineClass方法将二进制数据转换成Class对象
  83.                                 clazz = defineClass(name,raw,0,raw.length);
  84.                         }
  85.                         catch(IOException ie)
  86.                         {
  87.                                 ie.printStackTrace();
  88.                         }
  89.                 }
  90.                 // 如果clazz为null,表明加载失败,则抛出异常
  91.                 if(clazz == null)
  92.                 {
  93.                         throw new ClassNotFoundException(name);
  94.                 }
  95.                 return clazz;
  96.         }
  97.         // 定义一个主方法
  98.         public static void main(String[] args) throws Exception
  99.         {
  100.                 // 如果运行该程序时没有参数,即没有目标类
  101.                 if (args.length < 1)
  102.                 {
  103.                         System.out.println("缺少目标类,请按如下格式运行Java源文件:");
  104.                         System.out.println("java CompileClassLoader ClassName");
  105.                 }
  106.                 // 第一个参数是需要运行的类
  107.                 String progClass = args[0];
  108.                 // 剩下的参数将作为运行目标类时的参数,
  109.                 // 将这些参数复制到一个新数组中
  110.                 String[] progArgs = new String[args.length-1];
  111.                 System.arraycopy(args , 1 , progArgs
  112.                         , 0 , progArgs.length);
  113.                 CompileClassLoader ccl = new CompileClassLoader();
  114.                 // 加载需要运行的类
  115.                 Class<?> clazz = ccl.loadClass(progClass);
  116.                 // 获取需要运行的类的主方法
  117.                 Method main = clazz.getMethod("main" , (new String[0]).getClass());
  118.                 Object[] argsArray = {progArgs};
  119.                 main.invoke(null,argsArray);
  120.         }
  121. }
复制代码

作者: xiewen    时间: 2013-5-29 23:03
接下来我们可以随意提供一个简单的主类,该主类无需编译就可以使用上面的CompileClassLoader来运行它。、
  1. public class Hello
  2. {
  3.         public static void main(String[] args)
  4.         {
  5.                 for (String arg : args)
  6.                 {
  7.                         System.out.println("运行Hello的参数:" + arg);
  8.                 }
  9.         }
  10. }
复制代码





欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2