Java类加载器之线程上下文类加载器

原文链接,感谢作者,讲得很清楚:http://blog.onlycatch.com/post/Java%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6

Thread.setContextClassLoader(ClassLoader cl)

在Java中提供了对于线程设置ContextClassLoader的方法,关于上下文类加载器,下面摘抄的内容将的比较明白:

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。 
前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。 
线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

简单来说,Java上下文类加载器的作用就是为了SPI机制才存在的,在Java的类加载机制中,默认都是双亲委派机制,即一个类加载器在加载类的时候,会首先委派其父加载器去加载,如果父加载器加载不到,才会自己加载。即: 
ClassLoader A -> System class loader -> Extension class loader -> Bootstrap class loader 
在加载类的时候,是从左到右委派的,但是对于SPI来说,有些接口是java核心类库提供的,而java核心类库的java类是由引导类加载器负责加载的,而这些接口的实现类却来自不同的jar包,java的类引导加载器是不会加载其他来源的jar包的,这样传统的双亲委派机制就不能满足SPI的需求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。 
java提供是jdbc Driver就是基于SPI的,我们可以用它做例子。

jdbc Driver加载

我们可以通过java提供的ServiceLoader来实现一下,从而更好的理解java上下文类加载器的作用。 
首先我们在项目中加入Mysql connector jar包:

 
 
  1. <dependency>
  2. <groupId>mysql</groupId>
  3. <artifactId>mysql-connector-java</artifactId>
  4. <version>5.1.18</version>
  5. </dependency>

然后我们通道ServiceLoader加载Driver类的实现类。

通过默认的系统类加载器加载

 
 
  1. @Test
  2. public void testClassLoader() {
  3. ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
  4. Iterator<Driver> iterator = loader.iterator();
  5. while (iterator.hasNext()) {
  6. Driver driver = (Driver) iterator.next();
  7. System.out.println("driver:" + driver.getClass() + ",loader:" + driver.getClass().getClassLoader());
  8. }
  9. }

为什么说是用系统默认的类加载器?我们看一下ServiceLoader.load方法:

 
 
  1. public static <S> ServiceLoader<S> load(Class<S> service) {
  2. ClassLoader cl = Thread.currentThread().getContextClassLoader();
  3. return ServiceLoader.load(service, cl);
  4. }

可以看到,它的实现就是首先获取了当前线程的上下文类加载器,而上面的代码,当前线程的上下文类加载器就是ClassLoader.getSystemClassLoader()。 
运行结果:

 
 
  1. driver:class sun.jdbc.odbc.JdbcOdbcDriver,loader:null
  2. driver:class com.mysql.jdbc.Driver,loader:sun.misc.Launcher$AppClassLoader@53004901

可以看到,有两个driver的实现,一个是jdk自带的,它是由引导类加载器加载的(null就表示引导类加载器),而后面的mysql实现的Driver的加载器就是系统类加载器。

现在我们如果人工干预一下,不使用当前线程的上下文类加载器,我们看看能否加载到mysql提供的实现类:

 
 
  1. @Test
  2. public void testClassLoader() {
  3. ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class, ClassLoader.getSystemClassLoader().getParent());
  4. Iterator<Driver> iterator = loader.iterator();
  5. while (iterator.hasNext()) {
  6. Driver driver = (Driver) iterator.next();
  7. System.out.println("driver:" + driver.getClass() + ",loader:" + driver.getClass().getClassLoader());
  8. }
  9. System.out.println("system loader:" + ClassLoader.getSystemClassLoader());
  10. System.out.println("system loader's parent:" + ClassLoader.getSystemClassLoader().getParent());
  11. }

这次我们将ServiceLoader使用的加载器人工设置成当前系统加载器的父加载器,然后再看下输出结果:

 
 
  1. driver:class sun.jdbc.odbc.JdbcOdbcDriver,loader:null
  2. system loader:sun.misc.Launcher$AppClassLoader@53004901
  3. system loader's parent:sun.misc.Launcher$ExtClassLoader@37b90b39

可以看到,这里java自带的JdbcOdbcDriver还是可以加载的,但是mysql的却没有加载到,因为我们使用的是ExtClassLoader,而该ClassLoader只会加载java自己扩展库中的类,而不会加载mysql实现的类。

ClassLoader.getSystemClassLoader()

该方法会返回当前系统默认的类加载器。

 
 
  1. public class Main {
  2. public static void main(String[] args) {
  3. System.out.println(ClassLoader.getSystemClassLoader());
  4. }
  5. }

结果:

 
 
  1. sun.misc.Launcher$AppClassLoader@53004901

当时该方法并不是在所有情况下都返回APPClassLoader,如果用户自定义一个ClassLoader,则可以通过设置JVM参数java.system.class.loader替换当前默认的ClassLoader。

自定义ClassLoader实现

首先我们自定义一个ClassLoader,然后代码运行的时候设置JVM参数java.system.class.loader为我们自定义的ClassLoader 
我们自己定义的ClassLoader

 
 
  1. package loader;
  2. public class MyClassLoader extends ClassLoader {
  3. //继承了ClassLoader,但是没有任何实现
  4. }

测试类:

 
 
  1. package loader;
  2. public class Main {
  3. public static void main(String[] args) {
  4. System.out.println(ClassLoader.getSystemClassLoader());
  5. }
  6. }

在运行之前,修改系统参数java.system.class.loader的值为loader.MyClassLoader,eclipse中可以通过如下方式设置: 
设置JVM参数

设置完成之后我们点击运行,输出下面的结果:

 
 
  1. Error occurred during initialization of VM
  2. java.lang.Error: java.lang.NoSuchMethodException: loader.MyClassLoader.<init>(java.lang.ClassLoader)
  3. at java.lang.ClassLoader.initSystemClassLoader(Unknown Source)
  4. at java.lang.ClassLoader.getSystemClassLoader(Unknown Source)
  5. Caused by: java.lang.NoSuchMethodException: loader.MyClassLoader.<init>(java.lang.ClassLoader)
  6. at java.lang.Class.getConstructor0(Unknown Source)
  7. at java.lang.Class.getDeclaredConstructor(Unknown Source)
  8. at java.lang.SystemClassLoaderAction.run(Unknown Source)
  9. at java.security.AccessController.doPrivileged(Native Method)
  10. at java.lang.ClassLoader.initSystemClassLoader(Unknown Source)
  11. at java.lang.ClassLoader.getSystemClassLoader(Unknown Source)

竟然出错了,意思是没有init方法,我们查看ClassLoader.getSystemClassLoader()方法的文档,有下面一段:

If the system property “java.system.class.loader” is defined when this method is first invoked then the value of that property is taken to be the name of a class that will be returned as the system class loader. The class is loaded using the default system class loader and must define a public constructor that takes a single parameter of type ClassLoader which is used as the delegation parent. An instance is then created using this constructor with the default system class loader as the parameter. The resulting class loader is defined to be the system class loader.

意思就是,如果在ClassLoader.getSystemClassLoader()方法第一次执行的时候,指定了系统变量java.system.class.loader的值(就是说用户自己设置了SystemClassLoader,那么该方法将会返回用户定义的ClassLoader,但是用户自定义的ClassLoader必须定义一个公有的构造方法,该方法需要接受一个ClassLoader类型的参数,该参数将作为用户自定义ClassLoader的父ClassLoader。

很明显,我们上面自己实现的MyClassLoader并没有任何实现,所以也就违背了这个规则,所以就会报错。我们将代码修改一下:

 
 
  1. package loader;
  2. public class MyClassLoader extends ClassLoader {
  3. public MyClassLoader() {
  4. super();
  5. }
  6. public MyClassLoader(ClassLoader parent) {
  7. super(parent);
  8. }
  9. }

其他地方不变,然后再次执行,结果如下:

 
 
  1. loader.MyClassLoader@2e6e1408

可以看到,通过设置系统变量java.system.class.loader为我们自定义的ClassLoader,ClassLoader.getSystemClassLoader()返回的就是用户自己定义的ClassLoader。

但是:用户自己定义的ClassLoader是由哪个ClassLoader加载的呢? 
我们可以通过下面的代码输出来看一下:

 
 
  1. package loader;
  2. public class Main {
  3. public static void main(String[] args) {
  4. System.out.println("system loader:" + ClassLoader.getSystemClassLoader());
  5. System.out.println("system loader's parent:" + ClassLoader.getSystemClassLoader().getParent());
  6. System.out.println("system loader's loader:" + ClassLoader.getSystemClassLoader().getClass().getClassLoader());
  7. }
  8. }

结果如下:

 
 
  1. system loader:loader.MyClassLoader@5d888759
  2. system loader's parent:sun.misc.Launcher$AppClassLoader@425224ee
  3. system loader's loader:sun.misc.Launcher$AppClassLoader@425224ee

所以说,实际上用户自己定义的ClassLoader是由原来的SystemClassLoader来加载的。

需要注意的是,上面设置系统变量java.system.class.loader的时候是通过上图中的JVM参数指定的,如果通过program arguments则不会生效,同样道理,下面的设置方式也是不生效的:

 
 
  1. public class Main {
  2. public static void main(String[] args) {
  3. System.setProperty("java.system.class.loader", "loader.MyClassLoader");
  4. System.out.println(ClassLoader.getSystemClassLoader());
  5. }
  6. }

参考资料

  1. http://7xo4v8.com1.z0.glb.clouddn.com/%40%2Fdocs%2FDynamicClassLoadingInTheJavaVirtualMachine.pdf
  2. http://docs.oracle.com/javase/7/docs/technotes/tools/findingclasses.html#srcfiles
  3. http://stackoverflow.com/questions/11395074/what-loads-the-java-system-classloader
  4. https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
  5. http://www.javaworld.com/article/2077344/core-java/find-a-way-out-of-the-classloader-maze.html
  6. http://stackoverflow.com/questions/7039467/java-serviceloader-with-multiple-classloaders
  7. http://www.cnblogs.com/549294286/p/3714692.html
  8. http://www.ibm.com/developerworks/cn/java/j-dyn0429/
  9. http://renchx.com/java-spi-serviceloader/

猜你喜欢

转载自blog.csdn.net/dhfzhishi/article/details/80713896