前言
本文主要总结下自定义类加载器的主要用途
文章涉及的源码均已上传到了码云,参考【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. 其他疑问
- 疑问: 如何让所有类都走自定义的加载器加载,而非反射调用
首先,一个类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(); }
测试结果如下图
二.用途-热部署
码云项目:tjs-study-mini
码云路径:{@link tjs.styudy.mini.demo.devtools.DemoDevtoolsApplication#main}
spring-boot-devtools的热部署,就是通过自定义Classloader去实现重启时只加载当前项目工作路径相关的类,其具体实现分析见【手写系列-手写spring-boot-devtools】,核心原理如下
- 监听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方法
-
自定义类加载器的加载逻辑如下
-
文件发生变更,发布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版本
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这个方法的
2.2 如何解决
- 升级当前项目的jvm-utils-v1.0到2.0,升级版本是有一定风险的,非必要不应该突然升级
- 使用ClassLoader的隔离性,将jvm-clash-b整个jar包相关的类都用独立的类加载器A去加载,那样就可以AppClassLoader加载v1版本的ClashStringUtils,独立的类加载器A加载了v2版本的ClashStringUtils,独立的两个不同的类加载器,两者互不干扰,不冲突,完美解决
具体的实现,代码量较多,请运行上文中的码云路径代码自行研究,这里只贴出核心代码
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异常
如何解决?
- 如果VersionObj在v1和v2中都没有变化,则控制loadClass方法的逻辑,保证VersionObj永远只会被一个类加载器加载
- 使用java反射调用
三.用途-加密/解密
码云项目:tjs-study-notes
码云路径:{@link tjs.study.notes.dotest.jvm.classload.encode.DoTestOfEncryption#main}
- 首先要知道,一个数和另外一个数进行两次异或后,是原数本身
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); }
}
- 然后编写解密类加载器
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);
}
}
- 然后编写测试类
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(); } }
}
- 运行截图
四.用途-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()}
*/
目前流行的趋势依然还是springboot,所以就简单的总结下tomact中的类加载器
toamct程序的入口类是org.apache.catalina.startup.Bootstrap#main
,这个方法会调用org.apache.catalina.startup.Bootstrap#init()
初始化相关的构造器,其核心代码如下
五.其他简单用途
- 加载当前项目classPath之外的包
- 加载网络传输传过来的class文件
六.jdk9中ClassLoader变化
- 扩展类加载器extClassLoader 替换成 平台类加载器PlatformClassLoader
- appClassLoader在委托给PlatformClassLoader加载器前,会先交由归属模块的加载器尝试加载