Java类加载器二

有关类加载的内容前面已经介绍了两篇博文,但是仍然没有介绍到类加载的在平常开发中的使用场景,包括如何自定义类加载器。本篇文章继续介绍类加载器的相关内容,包括如何自定义类加载器,并通过简单示例介绍一下类加载器的应用。最后在介绍一下线程上下文类加载器以及类加载器在Web容器和OSGi中的应用。

Class.forName

Class.forName()是一个静态方法,同样可以加载类。Class.forName()方法有如下两种形式:

public static Class forName(String className)
public static Class forName(String name, boolean initialize, ClassLoader loader)

第一种形式的参数 name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。第二种形式则相当于设置了参数initialize的值为true,loader的值为当前类的类加载器。Class.forName()最常见的一个应用就是加载数据库驱动的时候。平常基本使用的是第一种格式的方法,虽然Class类中的静态方法Class.forName()和ClassLoader中的loadClass()都可以用于加载类,但是还是有区别的,示例代码如下:

public class User {
	
	static {
        System.out.println("init static");
    }

}
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
String className = User.class.getName();
Class clazz = classLoader.loadClass(className);

ClassLoader的loadClass()方法不会执行类的static初始化代码,但是Class.forName(className)方法则不然,它会默认执行static初始化代码。

运行代码发现没有任何输出,通过上一篇文章对ClassLoader源码知道,loadClass()默认将resolve参数设置为了false,所以不会解析静态初始化代码。但是如果使用Class.forName(className),默认会输出"init static"。

自定义ClassLoader

首先定义一个用来可以加载存储在文件系统上的Java字节代码。

public class FileSystemClassLoader extends ClassLoader {

	//类加载根目录
	private String rootDir;

	public FileSystemClassLoader(String rootDir) {
		this.rootDir = rootDir;
	}

	@Override
	protected Class findClass(String name) throws ClassNotFoundException {
		byte[] classData = getClassData(name);
		if (classData == null) {
			throw new ClassNotFoundException();
		} else {
			return defineClass(name, classData, 0, classData.length);
		}
	}

	//根据className获取到一个字节数组
	private byte[] getClassData(String className) {
		String path = classNameToPath(className);
		InputStream ins = null;
		try {
			ins = new FileInputStream(path);
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			int bufferSize = 4096;
			byte[] buffer = new byte[bufferSize];
			int bytesNumRead = 0;
			while ((bytesNumRead = ins.read(buffer)) != -1) {
				baos.write(buffer, 0, bytesNumRead);
			}
			return baos.toByteArray();
		} catch (IOException e) {
			e.printStackTrace();
			IOUtils.closeQuietly(ins);
		}
		return null;
	}

	//将全类名转换为一个文件路径
	private String classNameToPath(String className) {
		return rootDir + File.separatorChar
				+ className.replace('.', File.separatorChar) + ".class";
	}

}

上一篇文章中我们说了在JVM中真正确定一个类是根据“命名空间”来的,也就是全类名+ClassLoader确定的,这里我们写一个简单的demo通过我们的自定义FileSystemClassLoader验证一下。

public class User {

	private User user;

	public void setUser(Object object) {
		this.user = (User) object;
	}

}

将这个类的编译后的class文件放入一个目录下,然后通过如下代码运行。

public class MainTest {

	public static void main(String[] args) {

		testClassIdentity();

	}
	
	public static void testClassIdentity() { 
	    String classDataRootPath = "D:\\workspace\\test"; 
	    FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); 
	    FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath); 
	    String className = "com.sunny.demo.bean.User"; 	
	    try { 
	        Class class1 = fscl1.loadClass(className); 
	        Object obj1 = class1.newInstance(); 
	        Class class2 = fscl2.loadClass(className); 
	        Object obj2 = class2.newInstance(); 
	        Method setSampleMethod = class1.getMethod("setUser", Object.class); 
	        setSampleMethod.invoke(obj1, obj2); 
	    } catch (Exception e) { 
	        e.printStackTrace(); 
	    } 
	 }
}

这里使用两个FileSystemClassLoader实例分别加载com.sunny.demo.bean.User,得到两个不同的java.lang.Class的实例,并通过newInstance()方法分别生成两个实例obj1和obj2,最后通过Java的反射 API在对象obj1上调用方法setUser,试图把对象obj2赋值给obj1内部的instance对象。

最后运行结果抛出如下异常:

java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.sunny.demo.MainTest.testClassIdentity(MainTest.java:28)
	at com.sunny.demo.MainTest.main(MainTest.java:13)
Caused by: java.lang.ClassCastException: com.sunny.demo.bean.User cannot be cast to com.sunny.demo.bean.User
	at com.sunny.demo.bean.User.setUser(User.java:12)

运行抛出了java.lang.ClassCastException异常,如果在平常开发中直接抛出这种异常开发者一定会感到很诧异。虽然两个相同的全类名类型对象相互转化,但是这两个类是由不同的类加载器实例来加载的,因此不被Java虚拟机认为是相同的。

除了上面自定义的文件类加载器FileSystemClassLoader,类似的也可以很简单的定义一个网络类加载器。通过上面自定义类加载器知道,核心问题就是如何拿到一个类的字节数组,可以通过HTTP获取到服务端的class文件的文件流,然后转换为字节数组,其它的都跟文件类加载器类似,所以代码这里就不贴出来了。

由于Java代码很容易被人反编译,所以在自定义类加载器时我们也可以按照一定的规则对class文件进行加解密。最简单的一种实现方式就是直接对class文件内容按照某种加密算法加密,然后在实现的自定义类加载器上对加密内容进行还原。

线程上下文类加载器

线程上下文类加载器(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 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由引导类加载器来加载的;SPI的实现类是由系统类加载器来加载的。引导类加载器是无法找到SPI的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类

上面这一段如果仅仅根据文字描述可能很难理解,这里简单分析一下数据库驱动管理类java.sql.DriverManager,部分代码如下:

private static Connection getConnection(
     String url, java.util.Properties info, Class caller) throws SQLException {
     /* 传入的caller由Reflection.getCallerClass()得到,该方法
      * 可获取到调用本方法的Class类,这儿调用者是java.sql.DriverManager(位于/lib/rt.jar中),
      * 也就是说caller.getClassLoader()本应得到Bootstrap启动类加载器
      * 但是在上一篇文章中讲到过启动类加载器无法被程序获取,所以只会得到null
      */
     ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
     synchronized(DriverManager.class) {
         // 获取线程上下文类加载器,用于后续校验
         if (callerCL == null) {
             callerCL = Thread.currentThread().getContextClassLoader();
         }
     }
	 ...
}

虽然线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。但是在使用线程上下文加载类,也要注意保证多个需要通信的线程间的类加载器应该是同一个,防止因为不同的类加载器导致类型转换异常(ClassCastException)。

类加载器与Web容器

对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat来说,每个 Web应用都有一个对应的类加载器实例。该类加载器也使用委托机制,所不同的是它是首先尝试去加载某个类,如果找不到再委托给父类加载器。这与一般类加载器的顺序是相反的。这是Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种委托模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证Java核心库的类型安全。

绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:

  • 每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
  • 多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
  • 当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。

类加载器与 OSGi

OSGi是OSGi(Open Service Gateway Initiative)技术是Java动态化模块化系统的一系列规范。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGi 已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse 就是基于 OSGi 技术来构建的。

OSGi 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载。只需要设置系统属性 org.osgi.framework.bootdelegation的值即可。

假设有两个模块 bundleA 和 bundleB,它们都有自己对应的类加载器 classLoaderA 和 classLoaderB。在 bundleA 中包含类com.bundleA.Sample,并且该类被声明为导出的,也就是说可以被其它模块所使用的。bundleB 声明了导入 bundleA 提供的类com.bundleA.Sample,并包含一个类 com.bundleB.NewSample继承自 com.bundleA.Sample。在 bundleB 启动的时候,其类加载器 classLoaderB 需要加载类 com.bundleB.NewSample,进而需要加载类 com.bundleA.Sample。由于 bundleB 声明了类 com.bundleA.Sample是导入的,classLoaderB 把加载类 com.bundleA.Sample的工作代理给导出该类的 bundleA 的类加载器 classLoaderA。classLoaderA 在其模块内部查找类 com.bundleA.Sample并定义它,所得到的类 com.bundleA.Sample实例就可以被所有声明导入了此类的模块使用。对于以 java开头的类,都是由父类加载器来加载的。如果声明了系统属性 org.osgi.framework.bootdelegation=com.example.core.*,那么对于包com.example.core中的类,都是由父类加载器来完成的。

OSGi 模块的这种类加载器结构,使得一个类的不同版本可以共存在 Java 虚拟机中,带来了很大的灵活性。不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:

  • 如果一个类库只有一个模块使用,把该类库的 jar 包放在模块中,在 Bundle-ClassPath中指明即可。
  • 如果一个类库被多个模块共用,可以为这个类库单独的创建一个模块,把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。
  • 如果类库提供了 SPI 接口,并且利用线程上下文类加载器来加载 SPI 实现的 Java 类,有可能会找不到 Java 类。如果出现了NoClassDefFoundError异常,首先检查当前线程的上下文类加载器是否正确。通过 Thread.currentThread().getContextClassLoader()就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的话,可以首先通过 class.getClassLoader()来得到模块对应的类加载器,再通过 Thread.currentThread().setContextClassLoader()来设置当前线程的上下文类加载器。

参考资料

Java类加载器

深入探讨 Java 类加载器

深入探讨 Java 类加载器

深入理解Java类加载器(1):Java类加载原理解析

java 类加载器

Java类加载器ClassLoader总结

类加载机制:全盘负责和双亲委托

java的线程上下文类加载器

真正理解线程上下文类加载器(多案例分析)

http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b27/sun/misc/Launcher.java

评论

您确定要删除吗?删除之后不可恢复