概述

前面讲解了类加载器的双亲委派模型,提到了自定义类加载器,那么我们现在讲解下如何自定义类加载器。

自定义类加载器场景

java内置的类加载器不满足于我们加载类的需求,这种情况下需要我们自定义一个类加载器,通常有一下几种情况:

  1. 扩展类加载源

通常情况下我们写的java类文件存放在classpath下,由应用类加载器AppClassLoader加载。我们可以自定义类加载器从数据库、网络等其他地方加载我们我们的类。

  1. 隔离类

比如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加载,这和双亲委派刚好相反。

  1. 防止源码泄露

某些情况下,我们的源码是商业机密,不能外泄,这种情况下会进行编译加密。那么在类加载的时候,需要进行解密还原,这种情况下就要自定义类加载器了。

实现方式

Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。
在自定义ClassLoader的子类时候,我们常见的会有两种做法:
● 重写loadClass()方法
● 重写findClass()方法

loadClass() 和 findClass()

查看源码,我们发现loadClass()最终调用的还是findClass()方法。
image.png
那我们该用那种方法呢?
主要根据实际需求来,

  1. 如果想打破双亲委派模型,那么就重写整个loadClass方法

loadClass()中封装了双亲委派模型的核心逻辑,如果我们确实有需求,需要打破这样的机制,那么就需要重写loadClass()方法。

  1. 如果不想打破双亲委派模型,那么只需要重写findClass方法即可

但是大部分情况下,我们建议的做法还是重写findClass()自定义类的加载方法,根据指定的类名,返回对应的Class对象的引用。因为任意打破双亲委派模型可能容易带来问题,我们重写findClass()是在双亲委派模型的框架下进行小范围的改动。

自定义一个类加载器

需求:加载本地磁盘D盘目录下的class文件。
分析:该需求只是从其他一个额外路径下加载class文件,不需要打破双亲委派模型,可以直接定义loadClass()方法。

  1. public class FileReadClassLoader extends ClassLoader {
  2. private String dir;
  3. public FileReadClassLoader(String dir) {
  4. this.dir = dir;
  5. }
  6. public FileReadClassLoader(String dir, ClassLoader parent) {
  7. super(parent);
  8. this.dir = dir;
  9. }
  10. @Override
  11. protected Class<?> findClass(String name) throws ClassNotFoundException {
  12. try {
  13. // 读取class
  14. byte[] bytes = getClassBytes(name);
  15. // 将二进制class转换为class对象
  16. Class<?> c = this.defineClass(null, bytes, 0, bytes.length);
  17. return c;
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. return super.findClass(name);
  22. }
  23. private byte[] getClassBytes(String name) throws Exception {
  24. // 这里要读入.class的字节,因此要使用字节流
  25. FileInputStream fis = new FileInputStream(new File(this.dir + File.separator + name + ".class"));
  26. FileChannel fc = fis.getChannel();
  27. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  28. WritableByteChannel wbc = Channels.newChannel(baos);
  29. ByteBuffer by = ByteBuffer.allocate(1024);
  30. while (true) {
  31. int i = fc.read(by);
  32. if (i == 0 || i == -1)
  33. break;
  34. by.flip();
  35. wbc.write(by);
  36. by.clear();
  37. }
  38. fis.close();
  39. return baos.toByteArray();
  40. }
  41. }

测试:

  1. public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
  2. // 创建自定义的类加载器
  3. FileReadClassLoader fileReadClassLoader = new FileReadClassLoader("D:\\classes");
  4. // 加载类
  5. Class<?> account = fileReadClassLoader.loadClass("Account");
  6. Object o = account.newInstance();
  7. ClassLoader classLoader = o.getClass().getClassLoader();
  8. System.out.println("加载当前类的类加载器为:" + classLoader);
  9. System.out.println("父类加载器为:" + classLoader.getParent());
  10. }

image.png

SpringBoot自定义ClassLoader

springboot想必大家都使用过,它也用到了自定义的类加载器。springboot最终打成一个fat jar,通过java -jar xxx.jar,就可以快速方便的启动应用。
fat jar目录结构:

  1. ├───BOOT-INF
  2. ├───classes
  3. application.properties
  4. └───com
  5. └───alvin
  6. Application.class
  7. └───lib
  8. .......(省略)
  9. spring-aop-5.1.2.RELEASE.jar
  10. spring-beans-5.1.2.RELEASE.jar
  11. spring-boot-2.1.0.RELEASE.jar
  12. spring-boot-actuator-2.1.0.RELEASE.jar
  13. ├───META-INF
  14. MANIFEST.MF
  15. └───maven
  16. └───com.gpcoding
  17. └───spring-boot-exception
  18. pom.properties
  19. pom.xml
  20. └───org
  21. └───springframework
  22. └───boot
  23. └───loader
  24. ExecutableArchiveLauncher.class
  25. JarLauncher.class
  26. LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
  27. LaunchedURLClassLoader.class
  28. Launcher.class
  29. MainMethodRunner.class
  30. PropertiesLauncher$1.class
  31. PropertiesLauncher$ArchiveEntryFilter.class
  32. PropertiesLauncher$PrefixMatchingArchiveFilter.class
  33. PropertiesLauncher.class
  34. WarLauncher.class
  35. ├───archive
  36. Archive$Entry.class
  37. 。。。(省略)
  38. ├───data
  39. RandomAccessData.class
  40. 。。。(省略)
  41. ├───jar
  42. AsciiBytes.class
  43. 。。。(省略)
  44. └───util
  45. SystemPropertyUtils.class

从目录结构我们看出:

  1. |--BOOT-INF
  2. |--BOOT-INF\classes 该文件下的文件是我们最后需要执行的代码
  3. |--BOOT-INF\lib 该文件下的文件是我们最后需要执行的代码的依赖
  4. |--META-INF
  5. |--MANIFEST.MF 该文件指定了版本以及Start-ClassMain-Class
  6. |--org 该文件下的文件是一个spring loader文件,应用类加载器首先会加载执行该目录下的代码

显然,这样的目录结构,需要我们通过自定义类加载器,去加载其中的类。
查看目录结构中的MANIFEST.MF文件,如下:

  1. Manifest-Version: 1.0
  2. Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
  3. Implementation-Title: springboot-01-helloworld
  4. Implementation-Version: 1.0-SNAPSHOT
  5. Spring-Boot-Layers-Index: BOOT-INF/layers.idx
  6. Start-Class: com.alvinlkk.HelloWorldApplication
  7. Spring-Boot-Classes: BOOT-INF/classes/
  8. Spring-Boot-Lib: BOOT-INF/lib/
  9. Build-Jdk-Spec: 1.8
  10. Spring-Boot-Version: 2.5.6
  11. Created-By: Maven JAR Plugin 3.2.2
  12. Main-Class: org.springframework.boot.loader.JarLauncher

可以看到,实际的启动的入口类为org.springframework.boot.loader.JarLauncher,在解压的目录中可见。
为了方便查看源码,我们需要在工程中引入下面的依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-loader</artifactId>
  4. <version>2.7.0</version>
  5. </dependency>

执行逻辑如下: 类加载器系类(三)——自定义类加载器 - 图3

  • Lanuncher#launch()方法中创建了LaunchedURLClassLoader类加载器,设置到线程上下文类加载器中。
  • 通过反射获取带有SpringBootApplication注解的启动类,执行main方法。

    LaunchedURLClassLoader解析

    org.springframework.boot.loader.LaunchedURLClassLoaderspring-boot-loader 中自定义的类加载器,实现对 jar 包中 BOOT-INF/classes 目录下的类和 BOOT-INF/lib 下第三方 jar 包中的类的加载。
    LaunchedURLClassLoader重写了loadClass方法,打破了双亲委派模型。

    1. /**
    2. * 重写类加载器中加载 Class 类对象方法
    3. */
    4. @Override
    5. protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    6. // 如果名称是以org.springframework.boot.loader.jarmode开头的直接加载类
    7. if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
    8. try {
    9. Class<?> result = loadClassInLaunchedClassLoader(name);
    10. if (resolve) {
    11. resolveClass(result);
    12. }
    13. return result;
    14. }
    15. catch (ClassNotFoundException ex) {
    16. }
    17. }
    18. if (this.exploded) {
    19. return super.loadClass(name, resolve);
    20. }
    21. Handler.setUseFastConnectionExceptions(true);
    22. try {
    23. try {
    24. // 判断这个类是否有对应的package包
    25. // 没有的话会从所有 URL(包括内部引入的所有 jar 包)中找到对应的 Package 包并进行设置
    26. definePackageIfNecessary(name);
    27. }
    28. catch (IllegalArgumentException ex) {
    29. // Tolerate race condition due to being parallel capable
    30. if (getPackage(name) == null) {
    31. // This should never happen as the IllegalArgumentException indicates
    32. // that the package has already been defined and, therefore,
    33. // getPackage(name) should not return null.
    34. throw new AssertionError("Package " + name + " has already been defined but it could not be found");
    35. }
    36. }
    37. // 调用父类的加载器
    38. return super.loadClass(name, resolve);
    39. }
    40. finally {
    41. Handler.setUseFastConnectionExceptions(false);
    42. }
    43. }
    44. /**
    45. * Define a package before a {@code findClass} call is made. This is necessary to
    46. * ensure that the appropriate manifest for nested JARs is associated with the
    47. * package.
    48. * @param className the class name being found
    49. */
    50. private void definePackageIfNecessary(String className) {
    51. int lastDot = className.lastIndexOf('.');
    52. if (lastDot >= 0) {
    53. // 获取包名
    54. String packageName = className.substring(0, lastDot);
    55. // 没有找到对应的包名,则进行解析
    56. if (getPackage(packageName) == null) {
    57. try {
    58. // 遍历所有的 URL,从所有的 jar 包中找到这个类对应的 Package 包并进行设置
    59. definePackage(className, packageName);
    60. }
    61. catch (IllegalArgumentException ex) {
    62. // Tolerate race condition due to being parallel capable
    63. if (getPackage(packageName) == null) {
    64. // This should never happen as the IllegalArgumentException
    65. // indicates that the package has already been defined and,
    66. // therefore, getPackage(name) should not have returned null.
    67. throw new AssertionError(
    68. "Package " + packageName + " has already been defined but it could not be found");
    69. }
    70. }
    71. }
    72. }
    73. }
    74. private void definePackage(String className, String packageName) {
    75. try {
    76. AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
    77. // 把类路径解析成类名并加上 .class 后缀
    78. String packageEntryName = packageName.replace('.', '/') + "/";
    79. String classEntryName = className.replace('.', '/') + ".class";
    80. // 遍历所有的 URL(包括应用内部引入的所有 jar 包)
    81. for (URL url : getURLs()) {
    82. try {
    83. URLConnection connection = url.openConnection();
    84. if (connection instanceof JarURLConnection) {
    85. JarFile jarFile = ((JarURLConnection) connection).getJarFile();
    86. // 如果这个 jar 中存在这个类名,且有对应的 Manifest
    87. if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null
    88. && jarFile.getManifest() != null) {
    89. // 定义这个类对应的 Package 包
    90. definePackage(packageName, jarFile.getManifest(), url);
    91. return null;
    92. }
    93. }
    94. }
    95. catch (IOException ex) {
    96. // Ignore
    97. }
    98. }
    99. return null;
    100. }, AccessController.getContext());
    101. }
    102. catch (java.security.PrivilegedActionException ex) {
    103. // Ignore
    104. }
    105. }

    总结

    本文阐述如何创建自定义类加载器,以及在什么场景下创建自定义类加载器,同时通过springboot启动拆创建的类加载器加深我们的理解。

    参考

    https://www.jianshu.com/p/9c07ced8de14
    https://www.cnblogs.com/lifullmoon/p/14953064.html
    https://www.cnblogs.com/xrq730/p/4847337.html