4.2 在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

  Class.forName(String name)默认会使用调用类的类加载器来进行类加载。

如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

如class.forName()加载驱动jar包时,就违背了类加载机制的默认委派双亲机制。而是采用当前类的加载器去加载。

如class.forName()加载驱动jar包时,就违背了类加载机制的默认委派双亲机制。而是采用当前类的加载器去加载。

如class.forName()加载驱动jar包时,就违背了类加载机制的默认委派双亲机制。而是采用当前类的加载器去加载。

java HelloWorld
命令的时候,JVM会将HelloWorld.class加载到内存中,并形成一个Class的对象HelloWorld.class。
其中的过程就是类加载过程:
1、寻找jre目录,寻找jvm.dll,并初始化JVM;
2、产生一个Bootstrap Loader(启动类加载器);
3、Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为Bootstrap Loader。
4、Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。
5、最后由AppClass Loader加载HelloWorld类。

我们直接来分析一下对应的jdk的代码:

  1. //java.lang.Class.java
  2. publicstatic Class<?> forName(String className) throws ClassNotFoundException {
  3. return forName0(className, true, ClassLoader.getCallerClassLoader());
  4. }
  5. //java.lang.ClassLoader.java
  6. // Returns the invoker's class loader, or null if none.
  7. static ClassLoader getCallerClassLoader() {
  8. // 获取调用类(caller)的类型
  9. Class caller = Reflection.getCallerClass(3);
  10. // This can be null if the VM is requesting it
  11. if (caller == null) {
  12. return null;
  13. }
  14. // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader
  15. return caller.getClassLoader0();
  16. }
  17. //java.lang.Class.java
  18. //虚拟机本地实现,获取当前类的类加载器,前面介绍的Class的getClassLoader()也使用此方法
  19. native ClassLoader getClassLoader0();

5 开发自己的类加载器

  在前面介绍类加载器的代理委派模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。
  方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。
  类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

  在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。

5.1 文件系统类加载器

  第一个类加载器用来加载存储在文件系统上的Java字节代码。完整的实现如下所示。

  1. package classloader;
  2. import java.io.ByteArrayOutputStream;
  3. import java.io.File;
  4. import java.io.FileInputStream;
  5. import java.io.IOException;
  6. import java.io.InputStream;
  7. // 文件系统类加载器
  8. public class FileSystemClassLoader extends ClassLoader {
  9. private String rootDir;
  10. public FileSystemClassLoader(String rootDir) {
  11. this.rootDir = rootDir;
  12. }
  13. // 获取类的字节码
  14. @Override
  15. protected Class<?> findClass(String name) throws ClassNotFoundException {
  16. byte[] classData = getClassData(name);  // 获取类的字节数组
  17. if (classData == null) {
  18. throw new ClassNotFoundException();
  19. } else {
  20. return defineClass(name, classData, 0, classData.length);
  21. }
  22. }
  23. private byte[] getClassData(String className) {
  24. // 读取类文件的字节
  25. String path = classNameToPath(className);
  26. try {
  27. InputStream ins = new FileInputStream(path);
  28. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  29. int bufferSize = 4096;
  30. byte[] buffer = new byte[bufferSize];
  31. int bytesNumRead = 0;
  32. // 读取类文件的字节码
  33. while ((bytesNumRead = ins.read(buffer)) != -1) {
  34. baos.write(buffer, 0, bytesNumRead);
  35. }
  36. return baos.toByteArray();
  37. } catch (IOException e) {
  38. e.printStackTrace();
  39. }
  40. return null;
  41. }
  42. private String classNameToPath(String className) {
  43. // 得到类文件的完全路径
  44. return rootDir + File.separatorChar
  45. + className.replace('.', File.separatorChar) + ".class";
  46. }
  47. }

  如上所示,类 FileSystemClassLoader继承自类java.lang.ClassLoader。在java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。
  类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。

  加载本地文件系统上的类,示例如下:

  1. package com.example;
  2. public class Sample {
  3. private Sample instance;
  4. public void setSample(Object instance) {
  5. System.out.println(instance.toString());
  6. this.instance = (Sample) instance;
  7. }
  8. }
  1. package classloader;
  2. import java.lang.reflect.Method;
  3. public class ClassIdentity {
  4. public static void main(String[] args) {
  5. new ClassIdentity().testClassIdentity();
  6. }
  7. public void testClassIdentity() {
  8. String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes";
  9. FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
  10. FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
  11. String className = "com.example.Sample";
  12. try {
  13. Class<?> class1 = fscl1.loadClass(className);  // 加载Sample类
  14. Object obj1 = class1.newInstance();  // 创建对象
  15. Class<?> class2 = fscl2.loadClass(className);
  16. Object obj2 = class2.newInstance();
  17. Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
  18. setSampleMethod.invoke(obj1, obj2);
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. }

  运行输出:com.example.Sample@7852e922

5.2 网络类加载器

  下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。
  类 NetworkClassLoader负责通过网络下载Java类字节代码并定义出Java类。它的实现与FileSystemClassLoader类似。

  1. package classloader;
  2. import java.io.ByteArrayOutputStream;
  3. import java.io.InputStream;
  4. import java.net.URL;
  5. public class NetworkClassLoader extends ClassLoader {
  6. private String rootUrl;
  7. public NetworkClassLoader(String rootUrl) {
  8. // 指定URL
  9. this.rootUrl = rootUrl;
  10. }
  11. // 获取类的字节码
  12. @Override
  13. protected Class<?> findClass(String name) throws ClassNotFoundException {
  14. byte[] classData = getClassData(name);
  15. if (classData == null) {
  16. throw new ClassNotFoundException();
  17. } else {
  18. return defineClass(name, classData, 0, classData.length);
  19. }
  20. }
  21. private byte[] getClassData(String className) {
  22. // 从网络上读取的类的字节
  23. String path = classNameToPath(className);
  24. try {
  25. URL url = new URL(path);
  26. InputStream ins = url.openStream();
  27. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  28. int bufferSize = 4096;
  29. byte[] buffer = new byte[bufferSize];
  30. int bytesNumRead = 0;
  31. // 读取类文件的字节
  32. while ((bytesNumRead = ins.read(buffer)) != -1) {
  33. baos.write(buffer, 0, bytesNumRead);
  34. }
  35. return baos.toByteArray();
  36. } catch (Exception e) {
  37. e.printStackTrace();
  38. }
  39. return null;
  40. }
  41. private String classNameToPath(String className) {
  42. // 得到类文件的URL
  43. return rootUrl + "/"
  44. + className.replace('.', '/') + ".class";
  45. }
  46. }

  在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用Java反射API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。我们使用接口的方式。示例如下:

  客户端接口:

  1. package classloader;
  2. public interface Versioned {
  3. String getVersion();
  4. }
  1. package classloader;
  2. public interface ICalculator extends Versioned {
  3. String calculate(String expression);
  4. }

  网络上的不同版本的类:

  1. package com.example;
  2. import classloader.ICalculator;
  3. public class CalculatorBasic implements ICalculator {
  4. @Override
  5. public String calculate(String expression) {
  6. return expression;
  7. }
  8. @Override
  9. public String getVersion() {
  10. return "1.0";
  11. }
  12. }
  1. package com.example;
  2. import classloader.ICalculator;
  3. public class CalculatorAdvanced implements ICalculator {
  4. @Override
  5. public String calculate(String expression) {
  6. return "Result is " + expression;
  7. }
  8. @Override
  9. public String getVersion() {
  10. return "2.0";
  11. }
  12. }

  在客户端加载网络上的类的过程:

  1. package classloader;
  2. public class CalculatorTest {
  3. public static void main(String[] args) {
  4. String url = "http://localhost:8080/ClassloaderTest/classes";
  5. NetworkClassLoader ncl = new NetworkClassLoader(url);
  6. String basicClassName = "com.example.CalculatorBasic";
  7. String advancedClassName = "com.example.CalculatorAdvanced";
  8. try {
  9. Class<?> clazz = ncl.loadClass(basicClassName);  // 加载一个版本的类
  10. ICalculator calculator = (ICalculator) clazz.newInstance();  // 创建对象
  11. System.out.println(calculator.getVersion());
  12. clazz = ncl.loadClass(advancedClassName);  // 加载另一个版本的类
  13. calculator = (ICalculator) clazz.newInstance();
  14. System.out.println(calculator.getVersion());
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }

Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?的更多相关文章

  1. Junit 注解 类加载器 .动态代理 jdbc 连接池 DButils 事务 Arraylist Linklist hashset 异常 哈希表的数据结构,存储过程 Map Object String Stringbufere File类 文件过滤器_原理分析 flush方法和close方法 序列号冲突问题

    Junit 注解 3).其它注意事项: 1).@Test运行的方法,不能有形参: 2).@Test运行的方法,不能有返回值: 3).@Test运行的方法,不能是静态方法: 4).在一个类中,可以同时定 ...

  2. (转)为什么不能从静态的方法里面调用非静态方法,或变量and类加载机制

    1. 程序最终都将在内存中执行,变量只有在内存中占有一席之地时才能被访问. 类的静态成员(变量和方法)属于类本身,在类加载的时候就会分配内存,可以通过类名直接去访问:非静态成员(变量和方法)属于类的对 ...

  3. java.lang.Class.forName(String name, boolean initialize, ClassLoader loader)方法

    描述 Java.lang.Class.forName(String name, boolean initialize, ClassLoader loader) 方法返回与给定字符串名的类或接口的Cla ...

  4. String作为方法参数传递 与 引用传递

    String作为方法参数传递 String 和 StringBuffer的区别见这里: http://wenku.baidu.com/view/bb670f2abd64783e09122bcd.htm ...

  5. 科普:String hashCode 方法为什么选择数字31作为乘子

    1. 背景 某天,我在写代码的时候,无意中点开了 String hashCode 方法.然后大致看了一下 hashCode 的实现,发现并不是很复杂.但是我从源码中发现了一个奇怪的数字,也就是本文的主 ...

  6. String hashCode 方法为什么选择数字31作为乘子

    1. 背景 某天,我在写代码的时候,无意中点开了 String hashCode 方法.然后大致看了一下 hashCode 的实现,发现并不是很复杂.但是我从源码中发现了一个奇怪的数字,也就是本文的主 ...

  7. 【Java-Method】读《重构》有感_Java方法到底是传值调用还是传引用调用(传钥匙调用)

    今天读<重构>P279, Separate Query from Modifier,将查询函数和修改函数分离. 问题的产生 突然想到 Java 的传对象作为参数的方法到底是 传引用调用,还 ...

  8. 【转】String hashCode 方法为什么选择数字31作为乘子

    某天,我在写代码的时候,无意中点开了 String hashCode 方法.然后大致看了一下 hashCode 的实现,发现并不是很复杂.但是我从源码中发现了一个奇怪的数字,也就是本文的主角31.这个 ...

  9. 类加载机制 + Classloader.loadClass(String name)和Class.forName(String name)

    Classloader.loadClass(String name)和Class.forName(String name)的区别 Java的类在jvm中的加载大致分为加载,链接或者叫link(里面包含 ...

随机推荐

  1. Linux学习笔记:644、755、777权限详解

    一.问题 1.在Linux或者Android系统下用命令ll或者ls -la的时候会看到前面-rw-rw-r--一串字符,不知道代表什么? 2.新建vi一个文件之后,经常需要chmod 755 fil ...

  2. Linux命令之远程登录与执行远程主机命令

    实现远程登录的命令 ssh.telnet.rlogin (1)ssh命令 ssh命令是openssh套件中的客户端连接工具,可以给予ssh加密协议实现安全的远程登录服务器.ssh命令用于远程登录上Li ...

  3. Master和worker模式

    让和hadoop的设计思想是一样的,Master负责分配任务和获取任务的结果,worker是真正处理业务逻辑的. 使用ConcurrentLikedQueue去承载所有的任务,因为会有多个worker ...

  4. hdu 4788 (2013成都现场赛 H题)

    100MB=10^5KB=10^8B 100MB=100*2^10KB=100*2^20B Sample Input2100[MB]1[B] Sample OutputCase #1: 4.63%Ca ...

  5. 【POJ】1286.Necklace of Beads

    题解 群论,我们只要找出所有的置换群的所有循环节 具体可参照算法艺术与信息学竞赛 旋转的置换有n个,每一个的循环节个数是gcd(N,i),i的范围是0到N - 1 翻转,对于奇数来说固定一个点,然后剩 ...

  6. 使用linux mysql客户端建立表时遇到格式解析的问题

    发现在notepad++写好的建表脚本,粘贴到linux客户端后,执行时总是报我的脚本有问题. 我看了又看,发现建表脚本本身是没有问题,问题出在"Tab"键上和注释上边了. 解决办 ...

  7. Cygwin镜像使用

    前言:Cygwin是一个在windows平台上运行的类UNIX模拟环境,可以自己安装想用的插件 Cygwin镜像使用帮助 收录架构 x86 x86_64 收录版本 所有版本 更新时间 每12小时更新一 ...

  8. 在Ubuntu18.04中QT编程的环境构建(转)

    在Ubuntu18.04中QT编程的环境构建 原点分析 百家号06-2110:14 如果说QT大家觉得陌生的话,那么 Windows 早年推出的C++图形用户界面的应用程序开发框架MFC,应该是耳熟能 ...

  9. 关于mysql中storage_engine中 MYISAM 和 INNODB 的选择

    简单点说 读操作多用myisam 写操作多用innodb 不过现在大家好像基本都用innodb,本人小白一个就直接用InnoDB. MySQL自20多年前成立以来一直支持可插拔存储引擎,但在一段相当长 ...

  10. Python并发编程-IO模型-IO多路复用实现SocketServer

    Server.py import select import socket sk = socket.socket() sk.bind(('127.0.0.1',8080)) sk.setblockin ...