自定义类加载器场景
java内置的类加载器不满足于我们加载类的需求,这种情况下需要我们自定义一个类加载器,通常有一下几种情况:
- 扩展类加载源
通常情况下我们写的java类文件存放在classpath下,由应用类加载器AppClassLoader加载。我们可以自定义类加载器从数据库、网络等其他地方加载我们我们的类。
- 隔离类
比如tomcat这种web容器中,会部署多个应用程序,比如应用程序A依赖了一个三方jar的1.0.0版本, 应用程序B依赖了同一个三方jar的版本1.1.0, 他们使用了同一个类User.class,
这个类在两个版本jar中有内容上的差别,如果不做隔离处理的话,程序A加载了1.0.0版本中的User.class
, 此时程序B也去加载时,发现已经有了User.class,
它实际就不会去加载1.1.0版本中的User.class,最终导致严重的后果。所以隔离类在这种情况还是很有必要的。
为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader
负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader
加载,这和双亲委派刚好相反。
- 防止源码泄露
某些情况下,我们的源码是商业机密,不能外泄,这种情况下会进行编译加密。那么在类加载的时候,需要进行解密还原,这种情况下就要自定义类加载器了。
实现方式
Java提供了抽象类java.lang.ClassLoader
,所有用户自定义的类加载器都应该继承ClassLoader
类。
在自定义ClassLoader
的子类时候,我们常见的会有两种做法:
● 重写loadClass()方法
● 重写findClass()方法
loadClass() 和 findClass()
查看源码,我们发现loadClass()最终调用的还是findClass()方法。
那我们该用那种方法呢?
主要根据实际需求来,
- 如果想打破双亲委派模型,那么就重写整个loadClass方法
loadClass()
中封装了双亲委派模型的核心逻辑,如果我们确实有需求,需要打破这样的机制,那么就需要重写loadClass()
方法。
- 如果不想打破双亲委派模型,那么只需要重写findClass方法即可
但是大部分情况下,我们建议的做法还是重写findClass()
自定义类的加载方法,根据指定的类名,返回对应的Class对象的引用。因为任意打破双亲委派模型可能容易带来问题,我们重写findClass()
是在双亲委派模型的框架下进行小范围的改动。
自定义一个类加载器
需求: 加载本地磁盘D盘目录下的class文件。
分析: 该需求只是从其他一个额外路径下加载class文件,不需要打破双亲委派模型,可以直接定义loadClass()
方法。
public class FileReadClassLoader extends ClassLoader { private String dir; public FileReadClassLoader(String dir) { this.dir = dir; } public FileReadClassLoader(String dir, ClassLoader parent) { super(parent); this.dir = dir; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { // 读取class byte[] bytes = getClassBytes(name); // 将二进制class转换为class对象 Class<?> c = this.defineClass(null, bytes, 0, bytes.length); return c; } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } private byte[] getClassBytes(String name) throws Exception { // 这里要读入.class的字节,因此要使用字节流 FileInputStream fis = new FileInputStream(new File(this.dir + File.separator + name + ".class")); FileChannel fc = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel wbc = Channels.newChannel(baos); ByteBuffer by = ByteBuffer.allocate(1024); while (true) { int i = fc.read(by); if (i == 0 || i == -1) break; by.flip(); wbc.write(by); by.clear(); } fis.close(); return baos.toByteArray(); } }
测试:
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { // 创建自定义的类加载器 FileReadClassLoader fileReadClassLoader = new FileReadClassLoader("D:\classes"); // 加载类 Class<?> account = fileReadClassLoader.loadClass("Account"); Object o = account.newInstance(); ClassLoader classLoader = o.getClass().getClassLoader(); System.out.println("加载当前类的类加载器为:" + classLoader); System.out.println("父类加载器为:" + classLoader.getParent()); }
SpringBoot自定义ClassLoader
springboot想必大家都使用过,它也用到了自定义的类加载器。springboot最终打成一个fat jar,通过java -jar xxx.jar
,就可以快速方便的启动应用。
fat jar目录结构:
├───BOOT-INF │ ├───classes │ │ │ application.properties │ │ │ │ │ └───com │ │ └───alvin │ │ │ Application.class │ │ │ └───lib │ .......(省略) │ spring-aop-5.1.2.RELEASE.jar │ spring-beans-5.1.2.RELEASE.jar │ spring-boot-2.1.0.RELEASE.jar │ spring-boot-actuator-2.1.0.RELEASE.jar │ ├───META-INF │ │ MANIFEST.MF │ │ │ └───maven │ └───com.gpcoding │ └───spring-boot-exception │ pom.properties │ pom.xml │ └───org └───springframework └───boot └───loader │ ExecutableArchiveLauncher.class │ JarLauncher.class │ LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class │ LaunchedURLClassLoader.class │ Launcher.class │ MainMethodRunner.class │ PropertiesLauncher$1.class │ PropertiesLauncher$ArchiveEntryFilter.class │ PropertiesLauncher$PrefixMatchingArchiveFilter.class │ PropertiesLauncher.class │ WarLauncher.class │ ├───archive │ Archive$Entry.class │ 。。。(省略) │ ├───data │ RandomAccessData.class │ 。。。(省略) ├───jar │ AsciiBytes.class │ 。。。(省略) └───util SystemPropertyUtils.class
从目录结构我们看出:
|--BOOT-INF |--BOOT-INF\classes 该文件下的文件是我们最后需要执行的代码 |--BOOT-INF\lib 该文件下的文件是我们最后需要执行的代码的依赖 |--META-INF |--MANIFEST.MF 该文件指定了版本以及Start-Class,Main-Class |--org 该文件下的文件是一个spring loader文件,应用类加载器首先会加载执行该目录下的代码
显然,这样的目录结构,需要我们通过自定义类加载器,去加载其中的类。
查看目录结构中的MANIFEST.MF文件,如下:
Manifest-Version: 1.0 Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx Implementation-Title: springboot-01-helloworld Implementation-Version: 1.0-SNAPSHOT Spring-Boot-Layers-Index: BOOT-INF/layers.idx Start-Class: com.alvinlkk.HelloWorldApplication Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Build-Jdk-Spec: 1.8 Spring-Boot-Version: 2.5.6 Created-By: Maven JAR Plugin 3.2.2 Main-Class: org.springframework.boot.loader.JarLauncher
可以看到,实际的启动的入口类为org.springframework.boot.loader.JarLauncher
,在解压的目录中可见。
为了方便查看源码,我们需要在工程中引入下面的依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-loader</artifactId> <version>2.7.0</version> </dependency>
执行逻辑如下:
Lanuncher#launch()
方法中创建了LaunchedURLClassLoader
类加载器,设置到线程上下文类加载器中。- 通过反射获取带有
SpringBootApplication
注解的启动类,执行main方法。
LaunchedURLClassLoader解析
org.springframework.boot.loader.LaunchedURLClassLoader
是 spring-boot-loader
中自定义的类加载器,实现对 jar 包中 BOOT-INF/classes 目录下的类和 BOOT-INF/lib 下第三方 jar 包中的类的加载。
LaunchedURLClassLoader
重写了loadClass
方法,打破了双亲委派模型。
/** * 重写类加载器中加载 Class 类对象方法 */ @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 如果名称是以org.springframework.boot.loader.jarmode开头的直接加载类 if (name.startsWith("org.springframework.boot.loader.jarmode.")) { try { Class<?> result = loadClassInLaunchedClassLoader(name); if (resolve) { resolveClass(result); } return result; } catch (ClassNotFoundException ex) { } } if (this.exploded) { return super.loadClass(name, resolve); } Handler.setUseFastConnectionExceptions(true); try { try { // 判断这个类是否有对应的package包 // 没有的话会从所有 URL(包括内部引入的所有 jar 包)中找到对应的 Package 包并进行设置 definePackageIfNecessary(name); } catch (IllegalArgumentException ex) { // Tolerate race condition due to being parallel capable if (getPackage(name) == null) { // This should never happen as the IllegalArgumentException indicates // that the package has already been defined and, therefore, // getPackage(name) should not return null. throw new AssertionError("Package " + name + " has already been defined but it could not be found"); } } // 调用父类的加载器 return super.loadClass(name, resolve); } finally { Handler.setUseFastConnectionExceptions(false); } } /** * Define a package before a {@code findClass} call is made. This is necessary to * ensure that the appropriate manifest for nested JARs is associated with the * package. * @param className the class name being found */ private void definePackageIfNecessary(String className) { int lastDot = className.lastIndexOf('.'); if (lastDot >= 0) { // 获取包名 String packageName = className.substring(0, lastDot); // 没有找到对应的包名,则进行解析 if (getPackage(packageName) == null) { try { // 遍历所有的 URL,从所有的 jar 包中找到这个类对应的 Package 包并进行设置 definePackage(className, packageName); } catch (IllegalArgumentException ex) { // Tolerate race condition due to being parallel capable if (getPackage(packageName) == null) { // This should never happen as the IllegalArgumentException // indicates that the package has already been defined and, // therefore, getPackage(name) should not have returned null. throw new AssertionError( "Package " + packageName + " has already been defined but it could not be found"); } } } } } private void definePackage(String className, String packageName) { try { AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> { // 把类路径解析成类名并加上 .class 后缀 String packageEntryName = packageName.replace('.', '/') + "/"; String classEntryName = className.replace('.', '/') + ".class"; // 遍历所有的 URL(包括应用内部引入的所有 jar 包) for (URL url : getURLs()) { try { URLConnection connection = url.openConnection(); if (connection instanceof JarURLConnection) { JarFile jarFile = ((JarURLConnection) connection).getJarFile(); // 如果这个 jar 中存在这个类名,且有对应的 Manifest if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null && jarFile.getManifest() != null) { // 定义这个类对应的 Package 包 definePackage(packageName, jarFile.getManifest(), url); return null; } } } catch (IOException ex) { // Ignore } } return null; }, AccessController.getContext()); } catch (java.security.PrivilegedActionException ex) { // Ignore } }
总结
本文阐述如何创建自定义类加载器,以及在什么场景下创建自定义类加载器,同时通过springboot启动拆创建的类加载器加深我们的理解。