ClassLoader分析(二):具体用途(附源码)

前言

本文主要总结下自定义类加载器的主要用途

文章涉及的源码均已上传到了码云,参考【README.md】文件部署运行即可
demo码云地址: git@gitee.com:tangjingshan/tjs-study-notes.git
手写系列码云地址: git@gitee.com:tangjingshan/tjs-study-mini.git

一.如何手写自定义类加载器

继承Classloader或者java.net.URLClassLoader等,不用打破双亲委派机制,则重写findClass方法即可,如果需要打破则重写loadClass方法

1. 继承Classloader

点击查看详细代码
public class OtherPackagerClassLoader extends ClassLoader {
private String loaderPath;

public OtherPackagerClassLoader(String path) {
    loaderPath = path;
}

@Override
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    Class<?> loadedClass = this.findLoadedClass(name);
    if (loadedClass == null) {
        try {
            // 打破双亲委派机制,优先自己加载
            loadedClass = this.findClass(name);
        } catch (ClassNotFoundException var9) {
            //java.lang等包不能由自定义的类加载器加载
            loadedClass = Class.forName(name, false, this.getParent());
        }
    }

    if (resolve) {
        this.resolveClass(loadedClass);//todo 不知道这个干嘛用的,注释也能正常运行。native方法调用了JVM_ResolveClass,其是做什么的
    }

    return loadedClass;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {

    File file = new File(loaderPath + "/" + name.replaceAll("\.", "/") + ".class");
    if (!file.exists()) {
        throw new ClassNotFoundException(name);//非当前工作目录的类,由父加载器加载
    }
    int len = 0;
    byte[] data = null;
    try (FileInputStream is = new FileInputStream(file);
         ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
        while ((len = is.read()) != -1) {
            bos.write(len);
        }
        data = bos.toByteArray();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return defineClass(name, data, 0, data.length);//装载到jvm中
}

}

2. 继承URLClassLoader

继承URLClassLoader可以更方便的操作jar包,无需自己去实现

点击查看详细代码
public class OtherPackagerUrlClassLoader extends URLClassLoader {
    public OtherPackagerUrlClassLoader(List urlStrs) {
        super(toURLs(urlStrs));
    }
public static URL[] toURLs(List<String> urlStrs) {
    URL[] urls = new URL[urlStrs.size()];
    for (int i = 0; i < urlStrs.size(); i++) {
        try {
            urls[i] = new URL("file:" + urlStrs.get(i));
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
    return urls;
}

public static void main(String[] args) throws ClassNotFoundException {
    OtherPackagerUrlClassLoader urlClassLoader = new OtherPackagerUrlClassLoader(
            Arrays.asList("/Users/piangpiang/Documents/software/apache-maven-3.6.1/my_maven_ws/TJS/study/notes/jvm-utils/v2.0/jvm-utils-v2.0.jar")
    );
   Class<?> clashStringUtils= urlClassLoader.loadClass("tjs.study.notes.jvm.utils.ClashStringUtils");
}

}

3. 其他疑问

  1. 疑问: 如何让所有类都走自定义的加载器加载,而非反射调用
    首先,一个类A引入了类B,则类B会优先用类A的加载器加载,所以问题就是如何让类A(也就是main方法的程序入口类mainClass)使用自定义的加载器加载

   方案就是: 运行正式代码前,使用自定义加载器,重新实例化一个mainClass的Class对象,然后java反射重新再次调用一次main方法即可。即Class.forName("mainClass的全限定类名",false,"自定义的ClassLoader")


这么做就会有一个问题,main方法第二次运行的时候,如何标识让程序不再再次反射调用main方法呢?
可能你会想到,用一个静态的常量firstFlag存储是否初次调用的标识,初次调用才用新的类加载器回调main方法,并重置firstFlag的值为否。可问题是静态类的静态常量,首先是由AppClassLoader加载初始化了,然后第二次由新的类加载器加载,firstFlag其值又会是默认值是

   方案就是: 控制loadClass的逻辑,让持有静态的常量firstFlag的类依旧由AppClassLoader加载,这样就不会重置firstFlag的值

码云项目:tjs-study-notes
码云路径:{@link tjs.study.notes.dotest.jvm.classload.DoTestOfClassLoad#main}

自定义类加载器-点击查看详细代码
public class OtherPackagerUrlClassLoader extends URLClassLoader {
    // OtherPackagerUrlClassLoader由AppClassLoader加载
    public static boolean firstFlag = true;
public OtherPackagerUrlClassLoader(List<String> urlStrs) {
    super(toURLs(urlStrs));
}

public OtherPackagerUrlClassLoader(URL[] urls) {
    super(urls);
}

public static URL[] toURLs(List<String> urlStrs) {
    URL[] urls = new URL[urlStrs.size()];
    for (int i = 0; i < urlStrs.size(); i++) {
        try {
            urls[i] = new URL("file:" + urlStrs.get(i));
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
    return urls;
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    Class<?> loadedClass = this.findLoadedClass(name);
    if("tjs.study.notes.dotest.jvm.classload.OtherPackagerUrlClassLoader".equals(name)){
        // 避免重复运行main方法,持有是否启动标识的类,由AppClassLoader加载
        loadedClass = super.loadClass(name, resolve);
    }
    if (loadedClass == null) {
        try {
            // 优先尝试从当前路径加载
            loadedClass = this.findClass(name);
        } catch (Exception var9) {
            // 当前路径没有,再从父加载器加载
            loadedClass = super.loadClass(name, resolve);
        }
    }

    if (resolve) {
        this.resolveClass(loadedClass);
    }
    return loadedClass;
}

}

测试类-点击查看详细代码
public class DoTestOfClassLoad {
public static void main(String[] args) throws Exception {
    System.out.println("DoTestOfClassLoad的类加载器:" + DoTestOfClassLoad.class.getClassLoader());
    System.out.println("OtherPackagerUrlClassLoader的类加载器:" + OtherPackagerUrlClassLoader.class.getClassLoader());
    System.out.println("引入的UserService的类加载器:" + UserService.class.getClassLoader());
    makeAllUseSelfClassLoader(args);
}

public static void makeAllUseSelfClassLoader(String[] args) throws Exception {
    if (OtherPackagerUrlClassLoader.firstFlag) {
        System.out.println("首次启动,重置main的类加载器");
        OtherPackagerUrlClassLoader.firstFlag = false;

        URL[] appClassLoaderURLs = getAppClassLoaderURLs();//使用appClassLoader的加载路径
        OtherPackagerUrlClassLoader urlClassLoader = new OtherPackagerUrlClassLoader(appClassLoaderURLs);
        Class<?> mainClass = Class.forName(DoTestOfClassLoad.class.getName(), false, urlClassLoader);
        //Class<?> mainClassRestart = urlClassLoader.loadClass(mainClass.getName(), false);
        Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        mainMethod.invoke(null, new Object[]{args});//直接传args,表示多个参数;而实际上只有一个,所以要转成Object
    }
}

public static URL[] getAppClassLoaderURLs() throws Exception {
    ClassLoader classLoader = ClassLoader.getSystemClassLoader();
    // appClassLoader的加载路径存储在ucp属性中
    Field ucpField = classLoader.getClass().getDeclaredField("ucp");
    ucpField.setAccessible(true);
    URLClassPath classPath = (URLClassPath) ucpField.get(classLoader);
    return classPath.getURLs();
}

测试结果如下图
image.png

二.用途-热部署

码云项目:tjs-study-mini
码云路径:{@link tjs.styudy.mini.demo.devtools.DemoDevtoolsApplication#main}

spring-boot-devtools的热部署,就是通过自定义Classloader去实现重启时只加载当前项目工作路径相关的类,其具体实现分析见【手写系列-手写spring-boot-devtools】,核心原理如下

  1. 监听springboot的ApplicationStartingEvent事件,使用自定义的类加载器另起一个线程再次调用main方法(这里会重置线程的上下文类加载器,因为spring加载IOC默认就是优先使用线程的上下文类加载器)
* springIOC加载bean的时候优先使用Thread.currentThread().getContextClassLoader()
* {@link org.springframework.beans.factory.support.AbstractBeanFactory#beanClassLoader}
* {@link org.springframework.util.ClassUtils#getDefaultClassLoader()}

再次调用main方法
image.png

  1. 自定义类加载器的加载逻辑如下
    image.png

  2. 文件发生变更,发布ClassPathChangedEvent事件,重新再次回调main方法

* 文件变更,发布事件
* {@link org.springframework.boot.devtools.classpath.ClassPathFileChangeListener#onChange(java.util.Set)}
* 监听文件变更事件
* {@link org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration.RestartConfiguration#restartingClassPathChangedEventListener(org.springframework.boot.devtools.filewatch.FileSystemWatcherFactory)}
*

二.用途-解决jar包共存问题

码云项目:tjs-study-notes
码云路径:{@link tjs.study.notes.jvm.clash.dotest.DoTestOfClashc#main}

2.1 何为jar包共存

何为jar包共存,一句话描述就是一个class可以以多个不同版本存在于一个应用程序中,就比如有一个公共模块jvm-utils,子模块jvm-clash-a依赖于jvm-utils-v1.0版本,子模块jvm-clash-b依赖于jvm-utils-v2.0版本,当前模块jvm-clash-dotest依赖于、jvm-clash-a、jvm-clash-b以及jvm-utils-v1.0版本。
根据maven的jar包冲突策略最短原则,优先原则,可知最终jvm-clash-dotest只会引用jvm-utils-v1.0版本

image.png

jvm-utils-v1.0其有一个类ClashStringUtils,在v1版本的时候,代码如下

public class ClashStringUtils {

    public VersionObj addWhenV1() {
        System.out.println("v1时新增的方法...");
        return new VersionObj("v1-obj");
    }

}

在v2版本的时候,代码如下

public class ClashStringUtils {

    public VersionObj addWhenV1() {
        System.out.println("v1时新增的方法...");
        return new VersionObj("v1-obj");
    }

    public VersionObj addWhenV2() {
        System.out.println("v2时新增的方法...");
        return new VersionObj("v2-obj");
    }

}

jvm-clash-b有一个类如下

public class DoTestOfClashb {
    public void doSomethingB() {
        new ClashStringUtils().addWhenV2();
    }

    public Object doSomethingBRetObj() {
        return this.doSomethingBRet();
    }

    public VersionObj doSomethingBRet() {
        ClashStringUtils clashStringUtils = new ClashStringUtils();
        return clashStringUtils.addWhenV2();
    }
}

现在jvm-clash-dotest需要调用DoTestOfClashb.doSomethingB方法,由于当前ClassLoader只加载了v1版本的jvm-utils-v1.0,所以必然是没有ClashStringUtils().addWhenV2这个方法的
image.png

2.2 如何解决

  1. 升级当前项目的jvm-utils-v1.0到2.0,升级版本是有一定风险的,非必要不应该突然升级
  2. 使用ClassLoader的隔离性,将jvm-clash-b整个jar包相关的类都用独立的类加载器A去加载,那样就可以AppClassLoader加载v1版本的ClashStringUtils,独立的类加载器A加载了v2版本的ClashStringUtils,独立的两个不同的类加载器,两者互不干扰,不冲突,完美解决
    具体的实现,代码量较多,请运行上文中的码云路径代码自行研究,这里只贴出核心代码

image.png
image.png

2.3 引发的其他问题

比如jvm-clash-b中的下面的代码,由于jvm-clash-b用独立的类加载器A去加载的,所以类加载器A中会有一个VersionObj的class对象,如果这个时候jvm-clash-dotest要接收doSomethingBRet的返回值怎么办

public VersionObj doSomethingBRet() {
    ClashStringUtils clashStringUtils = new ClashStringUtils();
    return clashStringUtils.addWhenV2();
}

分析jvm-clash-dotest中的下面代码

VersionObj versionObjectB = new DoTestOfClashb().doSomethingBRet();

首先程序左边,会使用当前类加载器AppClassLoader去加载VersionObj
然后程序右边,又会使用独立的类加载器A去加载VersionObj
最终导致AppClassLoader.Class!=独立的类加载器A.Class,抛出LinkageError异常
如何解决?

  1. 如果VersionObj在v1和v2中都没有变化,则控制loadClass方法的逻辑,保证VersionObj永远只会被一个类加载器加载

image.png

  1. 使用java反射调用

image.png

三.用途-加密/解密

码云项目:tjs-study-notes
码云路径:{@link tjs.study.notes.dotest.jvm.classload.encode.DoTestOfEncryption#main}

  1. 首先要知道,一个数和另外一个数进行两次异或后,是原数本身
点击查看详细代码

 public class DoTestOfEncryption {
    public static void main(String[] args) {
        testEncode();
    }
public static void testEncode() {
    String sourceStr = "Hellow world";
    String encode = Xor(sourceStr, 123456);//加密
    System.out.println(encode);//
    String decode = Xor(encode, 123456);//解密
    System.out.println(decode);//Hellow world
}

public static String Xor(String input, int key) {
    char[] chs = input.toCharArray();
    for (int i = 0; i < chs.length; i++) {
        chs[i] = (char) (chs[i] ^ key);
    }
    return new String(chs);
}

}

  1. 然后编写解密类加载器
点击查看详细代码

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class DecodeClassLoader extends ClassLoader {
private String loaderPath;

public DecodeClassLoader(String path) {
    loaderPath = path;
}

@Override
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    Class<?> loadedClass = this.findLoadedClass(name);
    if (loadedClass == null) {
        try {
            // 打破双亲委派机制,优先自己加载
            loadedClass = this.findClass(name);
        } catch (ClassNotFoundException var9) {
            //java.lang等包不能由自定义的类加载器加载
            loadedClass = Class.forName(name, false, this.getParent());
        }
    }
    if (resolve) {
        this.resolveClass(loadedClass);
    }

    return loadedClass;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {

    File file = new File(loaderPath + "/" + name.replaceAll("\.", "/") + ".class_encode");
    if (!file.exists()) {
        throw new ClassNotFoundException(name);
    }
    System.out.println("开始解密文件:" + file.getPath());
    int len = 0;
    byte[] data = null;
    try (FileInputStream is = new FileInputStream(file);
         ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
        while ((len = is.read()) != -1) {
            // fixme 解密-二次异或运算
            bos.write(len ^ DoTestOfEncryption.ENCODE_KEY);
        }
        data = bos.toByteArray();
    } catch (IOException e) {
        e.printStackTrace();
    }

    return defineClass(name, data, 0, data.length);
}

}

  1. 然后编写测试类
点击查看详细代码

public class DoTestOfEncryption {
    static final int ENCODE_KEY = 123456789;
public static void main(String[] args) throws Exception {
    //testEncodeStr();
    // 1.先加密目标class
    encodeFile(TjsStudyNotesApplication.work_space+"/jvm/classload/encode/UserService.class");

    // 2.再使用自定义的解密类加载器加载目标class
    DecodeClassLoader decodeClassLoader = new DecodeClassLoader(TjsStudyNotesApplication.work_space_pre+"/src/main/java");
    Class<?> userServiceClass = decodeClassLoader.loadClass("tjs.study.notes.dotest.jvm.classload.encode.UserService");
    Object userService =  userServiceClass.newInstance();
    userServiceClass.getDeclaredMethod("login").invoke(userService);
}

public static void testEncodeStr() {
    String sourceStr = "Hellow world";
    String encode = Xor(sourceStr, ENCODE_KEY);//加密
    System.out.println(encode);//
    String decode = Xor(encode, ENCODE_KEY);//解密
    System.out.println(decode);//Hellow world
}

public static String Xor(String input, int key) {
    char[] chs = input.toCharArray();
    for (int i = 0; i < chs.length; i++) {
        chs[i] = (char) (chs[i] ^ key);
    }
    return new String(chs);
}

public static void encodeFile(String fileName) {
    System.out.println("开始加密文件:" + fileName);
    try (FileInputStream fileInputStream = new FileInputStream(new File(fileName));
         FileOutputStream outputStream = new FileOutputStream(new File(fileName + "_encode"))) {
        int len = 0;
        while ((len = fileInputStream.read()) != -1) {
            // fixme 加密-首次异或运算
            outputStream.write(len ^ DoTestOfEncryption.ENCODE_KEY);
        }
        outputStream.flush();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

}

  1. 运行截图
    image.png

四.用途-toamact中的类加载器

首先,在像springboot这种内嵌tomact的程序中,并不是调用org.apache.catalina.startup.Bootstrap#main来启动程序的,而是直接new Tomact()实例来启动toamct的,所以内嵌toamct中,tomact相关的类默认也都是由AppClassLoader去加载的

/**
 * 实例化tomact
 * {@link org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#getWebServer(org.springframework.boot.web.servlet.ServletContextInitializer...)}
 * 启动tomact
 * {@link org.springframework.boot.web.embedded.tomcat.TomcatWebServer#initialize()}
 */

image.png

目前流行的趋势依然还是springboot,所以就简单的总结下tomact中的类加载器
toamct程序的入口类是org.apache.catalina.startup.Bootstrap#main,这个方法会调用org.apache.catalina.startup.Bootstrap#init()初始化相关的构造器,其核心代码如下

image.png

image.png

五.其他简单用途

  • 加载当前项目classPath之外的包
  • 加载网络传输传过来的class文件

六.jdk9中ClassLoader变化

  1. 扩展类加载器extClassLoader 替换成 平台类加载器PlatformClassLoader
  2. appClassLoader在委托给PlatformClassLoader加载器前,会先交由归属模块的加载器尝试加载

image.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值