类加载器系列(三)——如何自定义类加载器

简介: 类加载器系列(三)——如何自定义类加载器

自定义类加载器场景


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()方法。

1671108214260.jpg

那我们该用那种方法呢?

主要根据实际需求来,

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

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

  1. 如果不想打破双亲委派模型,那么只需要重写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());
    }

1671108229355.jpg


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>

执行逻辑如下:

image.png

  • 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启动拆创建的类加载器加深我们的理解。

目录
相关文章
|
9月前
|
应用服务中间件
自定义类加载器
自定义类加载器
46 0
|
Java
自定义类加载器实现热加载
自定义类加载器实现热加载
125 0
|
前端开发 Java 开发者
什么是类加载器,类加载器有哪些?
什么是类加载器,类加载器有哪些?
112 0
|
前端开发 Java 数据库
什么是类加载器?类加载器有哪些?
类加载器(ClassLoader)是Java虚拟机(JVM)的一部分,用于将类的字节码加载到内存中,并生成对应的Class对象。类加载器负责查找、加载和链接类的过程。
256 0
|
安全 前端开发 Java
双亲委派模型与类加载器
我们都知道类都是通过类加载器被加载进虚拟机中的,那这个类加载器有哪些呢?我们平时写的代码又是通过什么类加载器被加载进虚拟机中的呢?类加载器的工作模式又是什么呢?带着疑问一起去学习下双亲委派模型与类加载器。
140 0
双亲委派模型与类加载器
|
安全 前端开发 Java
双亲委派模型与自定义类加载器
双亲委派模型与自定义类加载器
双亲委派模型与自定义类加载器
|
缓存 前端开发 Java
37. 请你详细说说类加载流程,类加载机制及自定义类加载器 中
37. 请你详细说说类加载流程,类加载机制及自定义类加载器 中
143 0
37. 请你详细说说类加载流程,类加载机制及自定义类加载器 中
|
Java 编译器 API
37. 请你详细说说类加载流程,类加载机制及自定义类加载器 上
37. 请你详细说说类加载流程,类加载机制及自定义类加载器 上
108 0
|
Java
37. 请你详细说说类加载流程,类加载机制及自定义类加载器 下
37. 请你详细说说类加载流程,类加载机制及自定义类加载器 下
108 0