Java类加载器、自定义类加载器

文章目录

  • 概述
  • 一、类加载的过程
    • 1.加载
    • 2.链接
      • 1)验证
      • 2)准备
      • 3)解析
    • 3.初始化
  • 二、类加载时机
  • 三、Class.forName()和ClassLoader.LoadClass()的区别
    • 1、Class.forName(className)方法
    • 2、ClassLoader.loadClass(className)方法
    • 代码测试示例
  • 四、类加载器
    • 1)根类加载器(bootstrap class loader)
    • 2)扩展类加载器(extensions class loader)
    • 3)系统类加载器(system class loader)
    • 4)自定义类加载器
    • 5)线程上下文类加载器
    • 类加载器工具类
  • 五、类加载机制
    • 全盘负责
    • 双亲委派
    • 缓存机制
    • 双亲委派过程
  • 六、自定义类加载器示例
    • 1、直接使用URLClassLoader的方式
      • 测试用例
    • 2、继承URLClassLoader的方式
      • 实现类
      • 测试用例
    • 3、继承ClassLoader的方式
      • 实现类
      • 测试用例
  • 参考文献

概述

Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类和资源文件内容到Java虚拟机的内存空间中。

一、类加载的过程

  1. 加载
  2. 链接:此步骤包括了:验证、准备和解析
  3. 初始化

1.加载

把原始字节码读入内存
加载指的是将类的class字节码内容读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。

  • 从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
  • 从JAR包加载class文件,这种方式也是很常见的,JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
  • 通过网络加载class文件。
  • 把一个Java源文件动态编译,并执行加载。

2.链接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。

1)验证

验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。Java是相对C++语言是安全的语言,例如它有C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

四种验证做进一步说明:

  • 文件格式验证

主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。

  • 元数据验证

对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。

  • 字节码验证

最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。

  • 符号引用验证

主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

2)准备

类准备阶段负责为类的静态变量分配内存,并设置默认初始值。

3)解析

将类的二进制数据中的符号引用替换成直接引用。

说明一下:

  • 符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。
  • 直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。

3.初始化

初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

二、类加载时机

  • 创建类的实例,也就是new一个对象

  • 访问某个类或接口的静态变量,或者对该静态变量赋值

  • 调用类的静态方法

  • 反射(Class.forName(“classNamePath”))

  • 初始化一个类的子类(会首先初始化子类的父类)

  • JVM启动时标明的启动类,即文件名和类名相同的那个类

除此之外,下面几种情形需要特别指出:

对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

三、Class.forName()和ClassLoader.LoadClass()的区别

1、Class.forName(className)方法

源码如下:

@CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

  • 内部实际调用的方法是 Class.forName0(className, true,
    ClassLoader.getClassLoader(caller), caller);
  • 第2个boolean参数表示类是否需要初始化, Class.forName(className)默认是需要初始化。
    一旦初始化,就会触发目标对象的 static块代码执行。
  • 第3个参数说明,使用的类加载器是加载调用者类的类加载器。

2、ClassLoader.loadClass(className)方法

内部实际调用的方法是 ClassLoader.loadClass(className,false);
第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,由上面介绍可以,
不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行

代码测试示例

public class TestClassLoaderBean {
    public static final int finalIntData = 10;
    public static final String finalStrData = "字符串数据";
    public static int intData = 100;
    public static String strData = "字符串数据";

    static {
        intData = 1000;
        strData = "abcde";
        System.out.println("执行了静态代码块");
    }

}

public class TestClassLoaderBeanTest {

    /**
     * 使用Class.forName,会执行静态代码块
     */
    @Test
    public void test1() throws ClassNotFoundException {
        Class.forName(TestClassLoaderBean.class.getName());
    }
    /**
     * 使用ClassLoader.loadClass不会初始化静态代码块
     */
    @Test
    public void test2() throws ClassNotFoundException {
        Thread.currentThread().getContextClassLoader().loadClass(TestClassLoaderBean.class.getName());
    }

    /**
     * 访问静态变量,不会初始化类
     */
    @Test
    public void test3(){
        System.out.println(TestClassLoaderBean.finalIntData);
        System.out.println(TestClassLoaderBean.finalStrData);
    }

    /**
     * 访问静态变量,会初始化类
     */
    @Test
    public void test4(){
        System.out.println(TestClassLoaderBean.intData);
        System.out.println(TestClassLoaderBean.strData);
    }

    /**
     * 先用ClassLoader.loadClass,后访问静态变量,会初始化类
     */
    @Test
    public void test5() throws ClassNotFoundException {
        Thread.currentThread().getContextClassLoader().loadClass(TestClassLoaderBean.class.getName());
        System.out.println(TestClassLoaderBean.intData);
        System.out.println(TestClassLoaderBean.strData);
    }
}

四、类加载器

类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器,JVM核心类加载器的初始化,在sun.misc.Launcher的构造方法完成。

1)根类加载器(bootstrap class loader)

BootstrapClassLoader,也叫引导或启动类加载器,它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

2)扩展类加载器(extensions class loader)

ExtClassLoader,它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由sun.misc.Launcher.ExtClassLoader实现,父类加载器为null,可以查看:sun.misc.Launcher.ExtClassLoader#ExtClassLoader构造方法得知super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);。

3)系统类加载器(system class loader)

AppClassLoader,被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

4)自定义类加载器

通过继承ClassLoader或者建议继承URLClassLoader,下面称为MyClassLoader。
自定义类加载器的作用:jvm自带的三个加载器只能加载指定路径下的类字节码。 如果某个情况下,我们需要加载应用程序之外的类文件呢?比如本地D盘下的, 或者去加载网络上的某个类文件,这种情况就可以使用自定义加载器了。

当所需要的资源不存在于BootstrapClassLoader、ExtClassLoader、AppClassLoader所能加载的范围, 那就需要使用自定义类加载器,用于加载其他资源。
需要指定父类加载器(一般指定AppClassLoader作为父加载器),
实例化自定义类加载器时,若不指定父类加载器(不把父ClassLoader传入构造函数)的情况下, 默认采用系统类加载器(AppClassLoader)作为父类加载器。

真实场景:典型的有动态代理。一般程序启动的时候都会把所需的全部资源(jar)都指定到classpath中, 但是偏偏有些其他资源文件是在程序运行过程中动态生成在其他目录,或者其他程序上传到某个位置, 让我们的程序在程序已经运行后再动态识别和加载。

5)线程上下文类加载器

获取当前线程的类加载器:
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

与前面的四种类加载器不同,他不是一种新的类加载器,而是一种获取类加载器的机制,
最初的目的是为了实现一种机制,让上层类加载器能使用到下层的类加载器对象,
默认使用当前执行的代码所在应用的系统类加载器AppClassLoader。

典型的例子:是注册数据库驱动实现类的时候:Class.forName(“com.mysql.jdbc.Driver”),
从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,现在不需要写这句代码了,
读取jar包内META-INF/services下文件中的类名(文件名是spi类名,jdbc驱动类是:java.sql.Driver,文件内容是实现类路径:com.mysql.jdbc.Driver),
然后使用线程上下文类加载器(默认是系统类加载器)加载SPI文件中指定的实现类,这样就达到了,Bootstrap类加载器在运行时使用了系统类加载器加载类。

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由**启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)**来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。

而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

类加载器工具类

/**
 * 
 * 类加载器工具
 *
 * 
* * @Author: LanDingDong * @Date: 2019-01-12 14:42 * @Version: 1.0 */
public class ClassLoaderUtils { private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderUtils.class); /** * 获取启动类的类加载器,最顶层类加载器 * 1、方式一:通过rt.jar中的类的类加载器就是BootStrap类加载器 * @return 正常返回null,该加载器使用C/C++编写,所以不是一个对象,此方法只是为了测试效果,不作实际使用 */ public static ClassLoader getBootStrapClassLoader() { return Thread.class.getClassLoader(); } /** * 获取系统类加载器,启动类用到的类加载器 * @return */ public static ClassLoader getExtClassLoader() { return ClassLoader.getSystemClassLoader().getParent(); } /** * 获取系统类加载器,也就是AppClassLoader * @return */ public static ClassLoader getAppClassLoader() { return ClassLoader.getSystemClassLoader(); } /** * 获取默认类加载器 * 1、优先线程上下文类加载器; * 2、使用类的类加载器; * 3、使用系统类加载器 * @return */ public static ClassLoader getDefaultClassLoader() { ClassLoader cl = null; try { cl = Thread.currentThread().getContextClassLoader(); } catch (Throwable ex) { // Cannot access thread context ClassLoader - falling back... } if (cl == null) { // No thread context class loader -> use class loader of this class. cl = ClassLoaderUtils.class.getClassLoader(); if (cl == null) { // getClassLoader() returning null indicates the bootstrap ClassLoader try { cl = ClassLoader.getSystemClassLoader(); } catch (Throwable ex) { // Cannot access system ClassLoader - oh well, maybe the caller can live with null... } } } return cl; } /** * 获取当前线程上下文类加载器 * @return */ public static ClassLoader getCurrentThreadContextClassLoader() { return Thread.currentThread().getContextClassLoader(); } /** * 获取当前线程上下文类加载器 * @return */ public static void setCurrentThreadContextClassLoader(ClassLoader classLoader) { Thread.currentThread().setContextClassLoader(classLoader); } /** * 指定类获取类加载器 * @return */ public static ClassLoader getClassLoaderByClass(Class clazz) { return clazz.getClassLoader(); } /** * 获取类加载器的父类加载器 * @return */ public static ClassLoader getParentClassLoader(ClassLoader classLoader) { return classLoader.getParent(); } /** * 通过AppClassLoader来加载一个类 * @param className * @return */ public static Class<?> getClassByAppClassLoader(String className){ try { return getAppClassLoader().loadClass(className); } catch (ClassNotFoundException e) { LOG.error(e); } return null; } /** * 通过指定类加载器来加载一个类 * @param classLoader * @param className * @return */ public static Class<?> getClassByClassLoader(ClassLoader classLoader, String className){ try { return classLoader.loadClass(className); } catch (ClassNotFoundException e) { LOG.error(e); } return null; } }

五、类加载机制

JVM的类加载机制主要有如下3种。

全盘负责

所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

双亲委派

下面的类加载器父子关系并不是继承的意思,它们都是ClassLoader抽象类的实现,因此都含有一个ClassLoader parent成员变量,该变量指向其父加载器,加载时先判断父加载器存不存在,类似单向链表,实现逻辑在java.lang.ClassLoader#loadClass方法中,

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

双亲委派定义,先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

双亲委派机制的工作原理,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

缓存机制

缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

双亲委派过程

下面通过有自定义类加载器的场景进行说明

用户类加载器
系统类加载器
扩展类加载器
根类加载器

整个过程是一个递归过程。加载的时候,用户类加载器先查看自己是否有父类加载器,如果有,那就委托给父类加载器(系统类加载器)进行加载,父类加载器查看自身是否的父类加载器,一直递归到没有父类加载器的类加载器(比如:扩展类加载器),就使用根类加载器加载,如果根类加载器加载不到,那么此时就是递归回流执行过程,此时代码会回到没有父类加载器的这个类加载器尝试加载,加载不到就回到调用它的子类加载器尝试加载,如果加载不到,一直递归回到最初的类加载器(比如:用户类加载器])。

六、自定义类加载器示例

以下示例代码在:GitHub代码

1、直接使用URLClassLoader的方式

URLClassLoader这个类加载器已经能满足大部分需求。

下面示例是读取本地jar文件构造类加载器的示例

File file = new File(jar文件路径);
URL url = file.toURL();
URLClassLoader loader = new URLClassLoader(new URL[] { url }); (因为传入的是URL,其实也可以是网络文件)
Class tidyClazz = loader.loadClass(所需class的含包名的全名);

测试用例

@Test
    public void testURLClassLoader() throws Exception {
        File file = new File("./doc/latico.jar");
        URL uri = file.toURL();
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{uri});
        Class c = urlClassLoader.loadClass("com.latico.web.Main");
        System.out.println(c);
        if (c != null) {
            System.out.println(c.getClassLoader().toString());
        }
    }

2、继承URLClassLoader的方式

实现类

使用了单例模式管理,具体可以根据需求修改

public class URLClassLoaderImpl extends URLClassLoader {
    public static final Class clazz = URLClassLoaderImpl.class;
    private static final Logger LOG = LoggerFactory.getLogger(clazz);

    private static volatile URLClassLoaderImpl INSTANCE;

    /**
     * class文件的字节码内容,用字节数组装起来,key是类内容的MD5值,用于去重
     */
    private ConcurrentSkipListMap<String, byte[]> classByteCodes = new ConcurrentSkipListMap<>();

    /**
     * URL的唯一值,用于判断是否加载过
     */
    private Set<String> urlKeys = new ConcurrentSkipListSet<>();

    private URLClassLoaderImpl() {
        //指定父类加载器
        super(new URL[]{}, Thread.currentThread().getContextClassLoader());
    }

    /**
     * @return 单例
     */
    public static URLClassLoaderImpl getInstance() {
        if (INSTANCE == null) {
            synchronized (clazz) {
                if (INSTANCE == null) {
                    INSTANCE = new URLClassLoaderImpl();
                }
            }
        }
        return INSTANCE;
    }


    /**
     * @param jarFilePaths jar文件系统路径,可以多个
     */
    public void addResourcesByJarFilePath(String... jarFilePaths) {
        if (jarFilePaths == null) {
            return;
        }
        for (String filePath : jarFilePaths) {
            if (filePath == null) {
                continue;
            }
            try {
                URL url = UriUtils.convertFilePathToUrl(filePath);
                addURL(url);
            } catch (Throwable e) {
                LOG.error(e);
            }
        }
    }

    /**
     * @param url jar文件的URL
     */
    @Override
    public void addURL(URL url) {
        if (url == null) {
            return;
        }
        if(urlKeys.contains(url.toString())){
            return;
        }else{
            urlKeys.add(url.toString());
        }
        LOG.info("开始添加URL到类加载器,URL:{}", url);
        super.addURL(url);
        LOG.info("添加URL到类加载器成功,URL:{}", url);
    }

    /**
     * 添加字节码内容到类加载器
     * @param ClassCodes 字节码内容,可多个
     */
    public void addResourcesByClassCode(byte[]... ClassCodes) {
        if (ClassCodes == null) {
            return;
        }

        for (byte[] ClassCode : ClassCodes) {
            if (ClassCode == null) {
                continue;
            }
            try {
                String md5 = getResourcesKey(ClassCode);
                if (classByteCodes.containsKey(md5)) {
                    continue;
                }
                classByteCodes.put(md5, ClassCode);
                LOG.info("添加字节码到类加载器成功,字节码内容MD5:{}", md5);
            } catch (Throwable e) {
                LOG.error(e);
            }
        }
    }
    /**
     * 增加class文件进来,可以多个
     * @param classFilePaths class文件的系统文件路径,可以多个
     * @return
     */
    public void addResourcesByClassFilePath(String... classFilePaths) {
        if (classFilePaths == null) {
            return;
        }

        for (String classFilePath : classFilePaths) {
            if (classFilePath == null) {
                continue;
            }
            try {
                byte[] bytes = FileUtils.readFileToByteArray(classFilePath);
                String md5 = getResourcesKey(bytes);
                if (classByteCodes.containsKey(md5)) {
                    continue;
                }
                classByteCodes.put(md5, bytes);
                LOG.info("添加字节码文件到类加载器成功,字节码内容MD5:{}, 字节码文件路径:{}", md5, classFilePath);
            } catch (Throwable e) {
                LOG.error(e);
            }
        }
    }
    /**
     * 计算资源key
     * @param bytes
     * @return
     */
    private String getResourcesKey(byte[] bytes) {
        return MD5Utils.toLowerCaseMd5(bytes);
    }

    /**
     * 先从单个class文件列表中加载,获取不到就从URLClassLoader中的扫描jar包加载
     * @param name java类路径
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        LOG.info("开始查找类:{}", name);

        //逐个遍历classs查找
        for (byte[] classByte : classByteCodes.values()) {
            try {
                Class<?> defineClass = defineClass(name, classByte, 0, classByte.length);
                if (defineClass != null) {
                    //如果在classs列表中找到,直接返回
                    return defineClass;
                }
            } catch (Throwable t) {
            }
        }

        //URLClassLoader中的扫描jar包加载
        return super.findClass(name);
    }

    /**
     * 清除类加载器中的资源
     */
    public void clearResources() {
        classByteCodes.clear();
    }

}


测试用例

public class URLClassLoaderImplTest {
    /**
     * 测试从jar包里面加载类
     * @throws Exception
     */
    @Test
    public void testLoadByJar() throws Exception {
        URLClassLoaderImpl myClassLoader1 = URLClassLoaderImpl.getInstance();
        myClassLoader1.addResourcesByJarFilePath(".\\doc\\latico.jar");
        Class c = myClassLoader1.loadClass("com.latico.web.Main");
        System.out.println(c);
        if (c != null) {
            System.out.println(c.getClassLoader().toString());
        }
    }
    /**
     * 测试从class文件读取类
     * @throws Exception
     */
    @Test
    public void testLoadByClassFile() throws Exception {
        URLClassLoaderImpl myClassLoader = URLClassLoaderImpl.getInstance();

        //自定义类加载器的加载路径
        myClassLoader.addResourcesByClassFilePath(".\\doc\\Test.class", ".\\doc\\Test2.class");
        Class clazz = null;

        clazz = myClassLoader.loadClass("com.latico.commons.common.util.system.classloader.Test");
        if (clazz != null) {
            System.out.println(clazz.getClassLoader().toString());

        }

        clazz = myClassLoader.loadClass("com.latico.commons.common.util.system.classloader.Test2");

        if (clazz != null) {
            System.out.println(clazz.getClassLoader().toString());
        }
    }

    /**
     * 测试从jar包加载资源文件读
     * @throws Exception
     */
    @Test
    public void testLoadResourceByJar() throws Exception {
        URLClassLoaderImpl myClassLoader1 = URLClassLoaderImpl.getInstance();
        myClassLoader1.addResourcesByJarFilePath(".\\doc\\latico.jar");
        URL c = myClassLoader1.getResource("config.properties");
        System.out.println(c);

    }

    @Test
    public void testJarUrl() throws Exception {
//        URL url = PathUtils.convertFilePathToUrl(".\\doc\\latico.jar");
//        System.out.println(url.getPath());
//        System.out.println(url.getFile());
//        System.out.println(url.toString());
        URL url = new URL("jar:file:/.\\doc\\latico.jar!/config.properties");
        System.out.println(url);
    }

}

3、继承ClassLoader的方式

实现类

使用了单例模式管理,具体可以根据需求修改

public class ClassLoaderImpl extends ClassLoader {
    public static final Class clazz = ClassLoaderImpl.class;
    private static final Logger LOG = LoggerFactory.getLogger(clazz);

    private static volatile ClassLoaderImpl INSTANCE;

    /**
     * class文件字节码内容用字节数组装起来,key是类内容的MD5值
     */
    private ConcurrentSkipListMap<String, byte[]> classByteCodes = new ConcurrentSkipListMap<>();

    /**
     * jar文件URL
     */
    private ConcurrentSkipListMap<String, URL> urls = new ConcurrentSkipListMap<>();

    private ClassLoaderImpl() {
        //指定父类加载器
        super(Thread.currentThread().getContextClassLoader());
    }

    /**
     * @return 单例
     */
    public static ClassLoaderImpl getInstance() {
        if (INSTANCE == null) {
            synchronized (clazz) {
                if (INSTANCE == null) {
                    INSTANCE = new ClassLoaderImpl();
                }
            }
        }
        return INSTANCE;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        LOG.info("开始查找类:{}", name);

        //逐个classByteCodes遍历查找
        for (byte[] classByte : classByteCodes.values()) {
            try {
                Class<?> defineClass = defineClass(name, classByte, 0, classByte.length);
                if (defineClass != null) {
                    return defineClass;
                }
            } catch (Throwable t) {
            }
        }

    //从URL的jar中查找
        for (URL url : urls.values()) {
            try {
                List<String> resourcesNames = ResourcesUtils.getResourcesNameByUrL(url, null, name + "\\.class", true);
                System.out.println(resourcesNames);
                if (resourcesNames != null) {
                    for (String resourcesName : resourcesNames) {
                        if (resourcesName == null) {
                            continue;
                        }

                        //把资源名称转换成类名
                        resourcesName = ResourcesUtils.convertResourcesNameToClassName(resourcesName);
                        //匹配类名
                        if (resourcesName.equals(name)) {
                            //读取jar中的类作为字节数组
                            URL newUrl = new URL(url.toString() + name.replace(".", "/") + ".class");
                            byte[] bytes = IOUtils.readUrlToByteArray(newUrl);

                            //然后调用defineClass生成类
                            Class<?> defineClass = defineClass(name, bytes, 0, bytes.length);
                            if (defineClass != null) {
                                return defineClass;
                            }
                        }
                    }
                }

            } catch (Throwable e) {
            }
        }

        //找不到使用父类
        return super.findClass(name);
    }

    @Override
    protected URL findResource(final String name) {

        LOG.info("开始查找资源:{}", name);

        //因为jar的URL中已经有以/结尾,所以这里要去掉
        String newName = name;
        if (name.startsWith("/")) {
            newName = newName.substring(1, name.length());
        }

        //为了方便匹配,把/转换成.已达到跟ResourcesUtils.getResourcesNameByUrL检索的结果进行匹配
        final String nameLikeClass = newName.replace("/", ".");

        for (URL url : urls.values()) {
            try {
                //查找不以.class结尾是所有资源
                List<String> resourcesNames = ResourcesUtils.getResourcesNameByUrL(url, null, ".+?(?, true);
                if (resourcesNames != null) {
                    for (String resourcesName : resourcesNames) {
                        if (resourcesName == null) {
                            continue;
                        }
                        if (nameLikeClass.equals(resourcesName)) {

                            URL newUrl = new URL(url.toString() + newName);
                            return newUrl;
                        }
                    }
                }

            } catch (Throwable e) {
            }
        }
        return super.findResource(name);
    }

    /**
     * 添加字节码内容到类加载器
     * @param ClassCodes 字节码内容,可多个
     */
    public void addResourcesByClassCode(byte[]... ClassCodes) {
        if (ClassCodes == null) {
            return;
        }

        for (byte[] ClassCode : ClassCodes) {
            if (ClassCode == null) {
                continue;
            }
            try {
                String md5 = getResourcesKey(ClassCode);
                if (classByteCodes.containsKey(md5)) {
                    continue;
                }
                classByteCodes.put(md5, ClassCode);
                LOG.info("添加字节码到类加载器成功,字节码内容MD5:{}", md5);
            } catch (Throwable e) {
                LOG.error(e);
            }
        }
    }
    /**
     * 增加class文件进来,可以多个
     * @param classFilePaths class文件的系统文件路径,可以多个
     * @return
     */
    public void addResourcesByClassFilePath(String... classFilePaths) {
        if (classFilePaths == null) {
            return;
        }

        for (String classFilePath : classFilePaths) {
            if (classFilePath == null) {
                continue;
            }
            try {
                byte[] bytes = FileUtils.readFileToByteArray(classFilePath);
                String md5 = getResourcesKey(bytes);
                if (classByteCodes.containsKey(md5)) {
                    continue;
                }
                classByteCodes.put(md5, bytes);
                LOG.info("添加字节码文件到类加载器成功,字节码内容MD5:{}, 字节码文件路径:{}", md5, classFilePath);
            } catch (Throwable e) {
                LOG.error(e);
            }
        }
    }

    /**
     * 计算资源key
     * @param bytes
     * @return
     */
    private String getResourcesKey(byte[] bytes) {
        return MD5Utils.toLowerCaseMd5(bytes);
    }

    /**
     * @param url jar文件的URL
     */
    public void addResourcesByJarURL(URL url) {
        if (url == null) {
            return;
        }
        try {
            url = UriUtils.convertFileUrlToJarUrl(url);
            if(urls.containsKey(url.toString())){
                return;
            }
            LOG.info("开始添加URL到类加载器,URL:{}", url);
            urls.put(url.toString(), url);
            LOG.info("添加URL到类加载器成功,URL:{}", url);
        } catch (Exception e) {
            LOG.error(e);
        }

    }

    /**
     * 增加资源文件进来,可以多个
     * @param jarFilePaths 资源文件的系统文件路径,可以多个
     * @return
     */
    public void addResourcesByJarFilePath(String... jarFilePaths) {
        if (jarFilePaths == null) {
            return;
        }

        for (String jarFilePath : jarFilePaths) {
            if (jarFilePath == null) {
                continue;
            }
            try {
                URL url = UriUtils.convertFilePathToJarUrl(jarFilePath);
                addResourcesByJarURL(url);
            } catch (Throwable e) {
                LOG.error(e);
            }
        }
    }
}

测试用例

public class ClassLoaderImplTest {
    /**
     * 测试从jar包里面加载类
     * @throws Exception
     */
    @Test
    public void testLoadByJar() throws Exception {
        ClassLoaderImpl myClassLoader1 = ClassLoaderImpl.getInstance();
        myClassLoader1.addResourcesByJarFilePath(".\\doc\\latico.jar");
        Class c = myClassLoader1.loadClass("com.latico.web.Main");
        System.out.println(c);
        if (c != null) {
            System.out.println(c.getClassLoader().toString());
        }

    }

    /**
     * 测试从class文件读取类
     * @throws Exception
     */
    @Test
    public void testLoadByClassFile() throws Exception {
        LogUtils.loadLogBackConfigDefault();
        ClassLoaderImpl myClassLoader = ClassLoaderImpl.getInstance();

        //自定义类加载器的加载路径
        myClassLoader.addResourcesByClassFilePath(".\\doc\\Test.class", ".\\doc\\Test2.class");
        Class clazz = null;

        clazz = myClassLoader.loadClass("com.latico.commons.common.util.system.classloader.Test");
        if (clazz != null) {
            System.out.println(clazz.getClassLoader().toString());
        }

        clazz = myClassLoader.loadClass("com.latico.commons.common.util.system.classloader.Test2");

        if (clazz != null) {
            System.out.println(clazz.getClassLoader().toString());
        }

    }

    /**
     * 测试从jar包加载资源文件读
     * @throws Exception
     */
    @Test
    public void testLoadResourceByJar() throws Exception {
        ClassLoaderImpl myClassLoader1 = ClassLoaderImpl.getInstance();
        myClassLoader1.addResourcesByJarFilePath(".\\doc\\latico.jar");
        System.out.println(IOUtils.resourceToString("config.properties", myClassLoader1));
    }

}

参考文献

  • jvm之java类加载机制和类加载器(ClassLoader)的详解

你可能感兴趣的:(Java)