看过很多文章,也自己尝试手写过类加载器的一些简单代码示例,就以为自己懂了类加载器,双亲委派、父类优先加载一些名词常常挂在嘴边,然而当真正去看一些springboot类加载器或者项目中为了中间件之间类隔离加载器的源代码时还是会有很多困惑,似乎之前那些技术专家把代码搞的很复杂,这个周末静下心来手撕代码,自已尝试动手利用自定义类加载器实现插件间隔离的需求。
类和类加载器共同确定了类的唯一性
类和类加载器共同确定了类的唯一性,换个方式表达就是不同的类加载器加载的同一个类,通过instanceof 关键字判断时是会返回false的,强制进行类型转换是会报classcastexception的,详见如下几个例子:
系统类加载器与自定义类加载器
package com.cc.demo.v1;
import java.io.IOException;
import java.io.InputStream;
public class CustomClassLoaderTest1 {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
InstantiationException {
//自定义一个classloader
ClassLoader myClassLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
};
Class myClass = myClassLoader.loadClass(CustomClassLoaderTest1.class.getName());
System.out.println("myClassLoader:" + myClassLoader);
//此处结果为 myClass:class com.alioo.classloader.v1.CustomClassLoaderTest
System.out.println("myClass:" + myClass);
ClassLoader systemClassLoader = CustomClassLoaderTest1.class.getClassLoader();
//此处结果为 systemClassLoader:jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17
System.out.println("systemClassLoader:" + systemClassLoader);
Object myInstance = myClass.newInstance();
//此处值为:false
System.out.println(myInstance instanceof CustomClassLoaderTest1);
//此处会报java.lang.ClassCastException(原因,虽然是同一个类,但是被不同类加载器加载也不行)
CustomClassLoaderTest1 a = (CustomClassLoaderTest1) myInstance;
}
}
这段代码最终会报如下异常ClassCastException,完整的堆栈如下(这里埋一个伏笔,既然是这样,如果一个类既在插件中会用到,又在业务代码中会用到,该如何办?)
Exception in thread "main" java.lang.ClassCastException: class com.cc.demo.v1.CustomClassLoaderTest1 cannot be cast to class com.cc.demo.v1.CustomClassLoaderTest1 (com.cc.demo.v1.CustomClassLoaderTest1 is in unnamed module of loader com.cc.demo.v1.CustomClassLoaderTest1$1 @1e643faf; com.cc.demo.v1.CustomClassLoaderTest1 is in unnamed module of loader 'app')
at com.cc.demo.v1.CustomClassLoaderTest1.main(CustomClassLoaderTest1.java:52)
多个自定义类加载器之间
package com.cc.demo.v2;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class CustomClassLoaderTest2 {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
InstantiationException, NoSuchMethodException, InvocationTargetException {
//自定义2个classloader
ClassLoader myClassLoader1 = new ClassLoader() {
@Override
public Class<?> loadClass(String name) {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
};
ClassLoader myClassLoader2 = new ClassLoader() {
@Override
public Class<?> loadClass(String name) {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
};
//用类加载1加载Factory类
Class factoryClass = myClassLoader1.loadClass(Factory.class.getName());
Method createGoodsMethod = factoryClass.getMethod("createGoods");
//反射调用createGoods方法,得到对象goods1
Object goods1 = createGoodsMethod.invoke(factoryClass.newInstance());
//用类加载2加载Person类
Class personClass = myClassLoader2.loadClass(Person.class.getName());
//用类加载2加载Goods类
Class goodsClass = myClassLoader2.loadClass(Goods.class.getName());
//用类加载2加载Goods类
Object goods2 = goodsClass.newInstance();
Method setGoodsMethod = personClass.getMethod("setGoods", goodsClass);
//用类加载2得到的trunkClass、goods2,反射调用setGoods方法 执行结果:正常
setGoodsMethod.invoke(personClass.newInstance(), goods2);
//用类加载2得到的personClass、用类加载器1得到goods1 执行结果:报错 IllegalArgumentException: argument type mismatch
setGoodsMethod.invoke(personClass.newInstance(), goods1);
}
}
这段代码会报IllegalArgumentException,如果2个插件之间的类需要彼此之间调用该怎么办呢
Exception in thread "main" java.lang.IllegalArgumentException: argument type mismatch
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at com.cc.demo.v2.CustomClassLoaderTest2.main(CustomClassLoaderTest2.java:73)
插件开发实践
在进行真正插件开发前,针对前面提到的2个问题,这里统一回答下
-
如果一个类既在插件中会用到,又在业务代码中会用到,该如何办?
允许,比如jdk自带的类HashMap、再或者apache中commons-lang3包中的类StringUtil都很常见,很有可能在业务代码与插件中都会用到,故应当允许这种情况,针对这种情况,可以采取
○ jdk自带的类HashMap,都通过jdk自带的类加载器完成加载,比如appClassLoader、extClassLoader
○ 三方包比如commons-lang3,业务代码与插件中各自使用自己的类加载器进行使用 -
如果2个插件之间的类需要彼此之间调用该怎么办呢
不允许,每个插件既然需要独立成插件,就应该是某个特有的功能为业务系统服务,如果插件之间存在彼此依赖,徒增复杂性,不建议提供这种功能(当然如果确实有需要,也可以定义一下插件之间优先级,用于控制加载顺序,然后所有的插件类加载器注册到一个插件公共类加载器即可)
Package说明
容器相关
package:com.lzc.alioo.container
pom:com.lzc.alioo.container:alioo-container
插件相关
com.lzc.alioo.container.plugin
pom:com.lzc.alioo.container.plugin:alioo-plugin-xx
测试工程
package:com.cc.demo.v3.Application
pom:com.cc.demo:classloadertest
创建业务插件
下面正式进入插件开发工作
目标:
java项目中开发插件,与业务代码作好类隔离,避免相互影响,插件中实现的能力最终可以被业务代码调用,插件中返回的类对象可以在业务代码中正常访问与后续操作
要求:
方法1,名字:sayHello,出参是jdk自带的类String
方法2,名字:queryHelloModel,出参是插件中的自定义类HelloOneModel
代码结构及核心代码
.
├── pom.xml
└── src
├── main
│ └── java
│ └── com
│ └── lzc
│ └── alioo
│ └── container
│ └── plugin
│ └── x1
│ ├── HelloOneModel.java
│ └── HelloOneService.java
└── test
package com.lzc.alioo.container.plugin.x1;
public class HelloOneService {
public String sayHello() {
return "Hello from custom class loader:" + this.getClass().getClassLoader();
}
public HelloOneModel queryHelloModel(String name) {
HelloOneModel model = new HelloOneModel();
model.setName(name);
model.setAge(18);
return model;
}
public Object queryObject(String name) {
HelloOneModel model = new HelloOneModel();
model.setName(name);
model.setAge(20);
return model;
}
}
package com.lzc.alioo.container.plugin.x1;
public class HelloOneModel {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "HelloModel{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
为了体现多插件的概念,我将这个插件代码复制了一个,改了一个包结构用于区分
.
├── pom.xml
└── src
├── main
│ └── java
│ ├── com
│ │ └── lzc
│ │ └── alioo
│ │ └── container
│ │ └── plugin
│ │ └── x2
│ │ ├── HelloTwoModel.java
│ │ └── HelloTwoService.java
创建classLoader
classLoader说明:
AliooClassLoader 业务类加载器
PluginClassLoader 插件类加载器
PluginSharableClassLoader 插件统一管理类加载器(所有的PluginClassLoader都会注册进来)
创建业务容器
所谓业务容器,是指构造一个轻量级的隔离容器,用于实现App和插件之间、插件和插件之间的依赖隔离。
本示例中的AliooApplication类负责构造业务容器,为了配合自定义的AliooClassLoader,参考springboot的reLaunch方式,即创建一个新线程并指定AliooClassLoader,让其重新执行main方法,从而达到所有类交由AliooClassLoader进行加载的目的。这部分代码有几个关键部分
○ AliooApplication类变量inited,在为其赋值成true,必须通过markField方法来处理,因为AliooApplication第二次执行时是通过AliooClassLoader加载的
○ 重新执行main方法必须通过新创建线程的方式,因为可以通过指定新创建线程的类加载器是AliooClassLoader,从而达到重新执行main方法后所有类加载器都是AliooClassLoader
○ 创建线程launchThread并且start方法之后,必须立即调用join方法,用于将程序阻塞到此处
○ join方法之后,必须加上一行代码System.exit(0) 如果没有下面这行代码,Application类中main方法里的业务代码会再执行一次(BizTest3.main(args);
package com.lzc.alioo.container;
import java.io.File;
import java.lang.reflect.Field;
public class AliooApplication {
private static boolean inited;
public static void run(String[] args) {
try {
if (inited) {
return;
}
//插件集合类加载器
PluginSharableClassLoader sharableClassLoader = PluginSharableClassLoader.init();
//加载业务类加载器
AliooClassLoader aliooClassLoader = AliooClassLoader.init(AliooApplication.class.getClassLoader(), sharableClassLoader);
reLaunch(aliooClassLoader);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void reLaunch(AliooClassLoader aliooClassLoader) throws Exception {
String mainClassName = fetchMainClassName();
Thread launchThread = new Thread(() -> {
try {
// 这里不能简单如此赋值,否则会出现死循环
// inited = true;
markField(aliooClassLoader, "inited", true);
Class mainClass = aliooClassLoader.loadClass(mainClassName);
mainClass.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
} catch (Exception e) {
System.out.println("reLaunch err:" + e.getMessage());
e.printStackTrace();
}
}, "aliooMainThread");
launchThread.setContextClassLoader(aliooClassLoader);
launchThread.start();
launchThread.join();
// 执行到这里,新启动的main线程已经退出了,可以直接退出进程
// 如果没有下面这行代码,Application类中业务代码会再执行一次(BizTest3.main(args);)
System.exit(0);
}
public static void markField(ClassLoader classLoader, String fieldName, Object value) {
try {
Class<?> sarLauncherClass = classLoader.loadClass(AliooApplication.class.getName());
Field declaredField = sarLauncherClass.getDeclaredField(fieldName);
declaredField.setAccessible(true);
declaredField.set(null, value);
} catch (Throwable t) {
// ignore
}
}
public static String fetchMainClassName() {
for (StackTraceElement stackTraceElement : new RuntimeException().getStackTrace()) {
if (stackTraceElement.getMethodName().equals("main")) {
return stackTraceElement.getClassName();
}
}
throw new RuntimeException("main class not found");
}
}
package com.lzc.alioo.container;
import lombok.extern.slf4j.Slf4j;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class AliooClassLoader extends URLClassLoader {
private static AliooClassLoader instance;
private static PluginSharableClassLoader sharableClassLoader;
private static boolean inited;
private Map<String, Class> cache;
private AliooClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
this.cache = new HashMap<>();
}
public static AliooClassLoader init(ClassLoader classLoader, PluginSharableClassLoader pluginSharableClassLoader) {
if (inited) {
return instance;
}
URL[] urls = ClassLoaderUtils.getUrls(classLoader);
instance = new AliooClassLoader(urls, ClassLoader.getSystemClassLoader().getParent());
sharableClassLoader = pluginSharableClassLoader;
inited = true;
return instance;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
//优先检查是否已经在缓存中
Class clazz = null;
if ((clazz = cache.get(name)) != null) {
return clazz;
}
//插件中查找
if ((clazz = sharableClassLoader.loadClass(name)) != null) {
log.info("Loaded By " + sharableClassLoader + " name: " + name);
return clazz;
}
//项目classpath中查找
if ((clazz = super.loadClass(name)) != null) {
log.info("Loaded By " + this + " name: " + name);
cache.put(name, clazz);
return clazz;
}
throw new ClassNotFoundException(name);
}
}
package com.lzc.alioo.container;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
public class PluginSharableClassLoader extends URLClassLoader {
private static final Map<String, PluginClassLoader> pluginNameMap = new HashMap<>();
private static final Map<String, PluginClassLoader> classNamePluginCache = new HashMap<>();
public static PluginSharableClassLoader init() {
//插件集合类加载器
PluginSharableClassLoader sharableClassLoader = new PluginSharableClassLoader();
// 加载插件类加载器
initPluginClassLoader(sharableClassLoader);
return sharableClassLoader;
}
public PluginSharableClassLoader() {
super(new URL[0], ClassLoader.getSystemClassLoader().getParent());
}
public static void initPluginClassLoader(PluginSharableClassLoader sharableClassLoader) {
String pluginPath = System.getProperty("alioo.plugin.path", System.getProperty("user.home") + File.separator + "alioo-plugin/");
File[] pluginFiles = new File(pluginPath).listFiles();
if (pluginFiles == null) {
System.out.println("not found alioo plugin by pluginPath:" + pluginPath);
return;
}
for (int i = 0; i < pluginFiles.length; i++) {
if (pluginFiles[i].getName().endsWith(".jar")) {
String module = pluginFiles[i].getName().replace(".jar", "");
if (sharableClassLoader.contains(module)) {
continue;
}
PluginClassLoader.init(module, pluginFiles[i]);
}
}
}
public static void register(PluginClassLoader pluginClassLoader) {
pluginNameMap.put(pluginClassLoader.getName(), pluginClassLoader);
//pluginClassLoader提取出所有url中jar中class文件
pluginClassLoader.getExportedClass().stream().forEach(className -> classNamePluginCache.put(className, pluginClassLoader));
}
public boolean contains(String pluginName) {
return pluginNameMap.containsKey(pluginName);
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class clazz = null;
//预判一下当前加载的类是否在某个插件类加载器中
PluginClassLoader pluginClassLoader = classNamePluginCache.get(name.replace('.', '/') + ".class");
if (pluginClassLoader != null) {
clazz = pluginClassLoader.loadClassData(name);
return clazz;
}
return null;
}
}
package com.lzc.alioo.container;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@Slf4j
public class PluginClassLoader extends URLClassLoader {
private PluginClassLoader(String module, URL[] urlPath) {
super(module, urlPath, ClassLoader.getSystemClassLoader().getParent());
PluginSharableClassLoader.register(this);
}
public static PluginClassLoader init(String module, File pluginFile) {
URL[] urlPath = new URL[0];
try {
urlPath = new URL[]{pluginFile.toURI().toURL()};
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
PluginClassLoader instance = new PluginClassLoader(module, urlPath);
System.out.println("found alioo plugin by pluginPath:" + pluginFile.getAbsolutePath());
return instance;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class clazz = null;
//对于import的类优先使用lzcClassLoader进行统一加载
clazz = loadClassData(name);
if (clazz != null) {
log.info("Loaded By " + this.getName() + "(" + this + ") name: " + name);
}
return clazz;
}
public Class<?> loadClassData(String className) {
Class clazz = findLoadedClass(className);
if (clazz != null) {
return clazz;
}
try {
return super.loadClass(className);
} catch (ClassNotFoundException e) {
}
return null;
}
public List<String> getExportedClass() {
//todo:实际情况可能是只需要导出部分类文件
File classFile = new File(this.getURLs()[0].getFile());
List<String> list = new ArrayList<>();
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(classFile))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().endsWith(".class")) {
list.add(entry.getName());
}
}
} catch (IOException e) {
e.printStackTrace();
}
return list;
}
}
完整代码库
业务插件
https://github.com/lzc-alioo/alioo-container-plugin-x1
业务容器
https://github.com/lzc-alioo/alioo-container
测试工程
https://github.com/lzc-alioo/classloadertest
TODO
● 观察日志会发现类java.lang.Object,分别被AliooClassLoader与PluginClassLoader加载过,当插件中的方法返回类型是java.lang.Object时会报异常 java.lang.NoSuchMethodError: com.lzc.alioo.container.plugin.x1.HelloOneService.queryObject(Ljava/lang/String;)Ljava/lang/Object;需要想办法让java.lang.Object统一交由AliooClassLoader加载,而不是在PluginClassLoader中自行加载
● 业务插件中会有自己依赖的三方jar包,多个业务插件分别依赖着不同的三方jar包实践