转载于插件化技术 发表于 2020-03-29 | 分类于 Java
插件化技术
本文将介绍代码设计中的插件化实现。涉及到的关键技术点 自定义ClassLoader
和 ServiceLoader
。
接着,会说下插件化技术的典型应用场景。
ClassLoader
类加载的过程
参考:JVM 中关于3.2 类的生命周期
介绍。
显式与隐式加载
显式:在代码中通过调用 ClassLoader 加载 class 对象,如直接使用 Class.forName(name) 或 this.getClass().getClassLoader().loadClass() 加载 class 对象
隐式:通过虚拟机自动加载到内存中,如在加载某个类的 class 文件时,该类的 class 文件中引用了另外一个类的对象,此时额外引用的类将通过 JVM 自动加载到内存中
一段源程序代码:
public class Demo {
static int hello() {
int a = 1;
int b = 2;
int c = a + b;
return c;
}
public static void main(String[] args) {
System.out.println(hello());
}
}
生成字节码文件:
javac demo.java
对class文件反汇编:
javap -v -l -c demo.class > Demo.txt
-v: 不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息
-l: 输出行号和本地变量表信息
-c: 会对当前 class 字节码进行反编译生成汇编代码
通过文件编译工具来查看demo.txt的内容:
Classfile /private/tmp/Demo.class
Last modified 2020-4-4; size 464 bytes
MD5 checksum 2b2ee02c5a47ef7f4ed5388443f76800
Compiled from "Demo.java"
public class Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #5.#20 // Demo.hello:()I
#4 = Methodref #21.#22 // java/io/PrintStream.println:(I)V
#5 = Class #23 // Demo
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 hello
#12 = Utf8 ()I
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 SourceFile
#16 = Utf8 Demo.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = NameAndType #11:#12 // hello:()I
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(I)V
#23 = Utf8 Demo
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (I)V
{
public Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
static int hello();
descriptor: ()I
flags: ACC_STATIC
Code:
stack=2, locals=3, args_size=0
0: iconst_1
1: istore_0
2: iconst_2
3: istore_1
4: iload_0
5: iload_1
6: iadd
7: istore_2
8: iload_2
9: ireturn
LineNumberTable:
line 3: 0
line 4: 2
line 5: 4
line 6: 8
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: invokestatic #3 // Method hello:()I
6: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 10: 0
line 11: 9
}
解释下 #1 = Methodref #6.#17 // java/lang/Object."<init>":()V
:
执行类的构造方法时,首先会执行父类的构造方法,java.lang.Object是任何类的父类,
所以这边会首先执行 Object 类的构造方法,#1 会引用 #6、#17 对应的符号常量。
在JVM中表示两个class对象是否为同一个类对象存在两个必要条件:
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
Launcher启动类
Launcher启动类图:
注:Launcher类里面,有几个内部static类,分别是
- static class AppClassLoader extends URLClassLoader
- private static class BootClassPathHolder
- static class ExtClassLoader extends URLClassLoader
- private static class Factory implements URLStreamHandlerFactory
加载器类型
- 启动类加载器,由C++实现,没有父类。
- 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
- 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
- 自定义类加载器,父类加载器肯定为AppClassLoader。
加载器之间的类图关系:
loadClass(String)
将类加载请求到来时,先从缓存中查找该类对象,如果不存在就走双亲委派模式。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先判断这个 class 是否已经加载成功,只判断全限定名是否相同
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 先通过父类加载器查找,递归下去,直到 BootstrapClassLoader
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果父加载器为null,则用 BootstrapClassLoader 去加载
// 这也解释了 ExtClassLoader 的parent为null,但仍然说 BootstrapClassLoader 是它的父加载器
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();
// 如果向上委托父加载没有加载成功,则通过 findClass(String) 查找
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) {
// 生成最终的Class对象,对应着验证、准备、解析的过程
resolveClass(c);
}
return c;
}
}
findClass(String)
不建议直接覆盖 loadClass() 去打破双亲委派模式,建议把自定义逻辑写在 findClass() 中,findClass() 方法通常是和 defineClass() 方法一起使用的。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
defineClass(byte[] b,int off,int len)
将byte字节流解析成JVM 能够识别的Class对象。
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
resolveClass(Class<?> c)
对应链接阶段,它是native方法,主要对字节码进行验证,为类变量分配内存并设置初始值,将字节码文件中的符号引用转换为直接引用。
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
自定义ClassLoader
为什么要自定义ClassLoader呢?
- 当 class 文件不在 classpath 路径下,默认系统类加载无法找到该 class 文件,此时需要实现一个自定义的 ClassLoader 来加载特定路径下的 class 文件生成 Class 对象
- 当一个 class 文件是通过网络传输并且可能会进行相应的加密操作时,需要先对 class 文件进行相应的解密后再加载到 JVM 内存中
- 当需要实现热部署功能时,一个 class 文件通过不同的类加载器产生不同 class 对象从而实现热部署功能
自定义FileClassLoader:
/**
* Description:
*
* @author mwt
* @version 1.0
* @date 2020-04-03
*/
public class FileClassLoader extends ClassLoader {
private static final String CLASS_FILE_SUFFIX = ".class";
private String mLibpath;
public FileClassLoader(String mLibpath) {
this.mLibpath = mLibpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classFileName = getClassFileName(name);
File file = new File(mLibpath, classFileName);
try {
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
try {
while ((len = is.read()) != -1) {
bos.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
/**
* 读取java类对应的class文件
*
* @param name
* @return
*/
private String getClassFileName(String name) {
int index = name.lastIndexOf(".");
if (index == -1) {
return name + CLASS_FILE_SUFFIX;
} else {
return name.substring(index + 1) + CLASS_FILE_SUFFIX;
}
}
}
这里可以参考java自定义ClassLoader加载指定的class文件 这篇文章,基本一样的方法。
SPI
在Java应用中存在着很多服务提供者接口,Service Provider Interface,这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等。这些SPI的接口属于Java核心库,一般存在于rt.jar中,
由 Bootstrap 类加载器加载,而 SPI 的第三方实现代码则是作为 Java 应用所依赖的jar包被存放在 classpath 路径下。SPI 接口中的代码经常需要加载第三方实现类并调用其相关方法,但 SPI 的核心接口类是由 Bootstrap 类加载器加载,由于双亲委派模式的存在,Bootstrap 类加载器也无法反向委托 AppClassLoader 加载 SPI 的实现类。
此时,就需要一种特殊的类加载来加载第三方的类库,而线程上下文加载器就是很好的选择,可以破坏双亲委派模型。
如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器。
如在Launcher类中,会将AppClassLoader设置到当前线程上下文:
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
// Also set the context class loader for the primordial thread.
// 设置AppClassLoader为线程上下文类加载器
Thread.currentThread().setContextClassLoader(loader);
ServiceLoader
首先看下 ServiceLoader 的成员:
public final class ServiceLoader<S>
implements Iterable<S> {
// 指明了路径是在"META-INF/services“下
private static final String PREFIX = "META-INF/services/";
// 表示正在加载的服务的类或接口
private final Class<S> service;
// 使用的类加载器
private final ClassLoader loader;
// 创建ServiceLoader时获取的访问控制上下文
private final AccessControlContext acc;
// 缓存的服务提供者集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 内部使用的迭代器,用于类的懒加载,只有在迭代时才加载
// ServiceLoader 的实际加载过程是交给 LazyIterator 来做的
private LazyIterator lookupIterator;
......
}
调用其静态的load方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
关注下LazyIterator中的nextService方法:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 在迭代器的next中才会进行真正的类加载
c = Class.forName(cn, false, loader);
}
catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
}
catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error();
// This cannot happen
}
JDBC
DriverManager类的static块中会加载所用的Driver实现类:
//DriverManager是Java核心包rt.jar的类
public class DriverManager {
//省略不必要的代码
static {
loadInitialDrivers();//执行该方法
println("JDBC DriverManager initialized");
}
//loadInitialDrivers方法
private static void loadInitialDrivers() {
sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//加载外部的Driver的实现类
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
//省略不必要的代码......
}
});
}
ServiceLoader中的load方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 通过线程上下文类加载器加载,默认情况下就是AppClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
在不同的数据库驱动包中的 META-INF/services 目录下都会有一个名为 java.sql.Driver
的文件,记录Driver的实现类。
Mysql驱动包中:
Oracle驱动包中:
最佳实践
Flink中的插件化应用
DataX插件加载原理
插件的加载都是使用ClassLoader动态加载。 为了避免类的冲突,对于每个插件的加载,对应着独立的加载器。加载器由JarLoader实现,插件的加载接口由LoadUtil类负责。当要加载一个插件时,需要实例化一个JarLoader,然后切换thread class loader之后,才加载插件。
-
自定义JarLoader
JarLoader 继承 URLClassLoader,扩充了可以加载目录的功能。可以从指定的目录下,把传入的路径,及其子路径、以及路径中的jar文件加入到classpath。public class JarLoader extends URLClassLoader { public JarLoader(String[] paths) { this(paths, JarLoader.class.getClassLoader()); } public JarLoader(String[] paths, ClassLoader parent) { // 调用getURLS,获取所有的jar包路径 super(getURLs(paths), parent); } // 获取所有的jar包 private static URL[] getURLs(String[] paths) { // 获取包括子目录的所有目录路径 List<String> dirs = new ArrayList<String>(); for (String path : paths) { dirs.add(path); // 获取path目录和其子目录的所有目录路径 JarLoader.collectDirs(path, dirs); } // 遍历目录,获取jar包的路径 List<URL> urls = new ArrayList<URL>(); for (String path : dirs) { urls.addAll(doGetURLs(path)); } return urls.toArray(new URL[0]); } // 递归的方式,获取所有目录 private static void collectDirs(String path, List<String> collector) { // path为空,终止 if (null == path || StringUtils.isBlank(path)) { return; } // path不为目录,终止 File current = new File(path); if (!current.exists() || !current.isDirectory()) { return; } // 遍历完子文件,终止 for (File child : current.listFiles()) { if (!child.isDirectory()) { continue; } collector.add(child.getAbsolutePath()); collectDirs(child.getAbsolutePath(), collector); } } private static List<URL> doGetURLs(final String path) { File jarPath = new File(path); // 只寻找文件以.jar结尾的文件 FileFilter jarFilter = new FileFilter() { @Override public boolean accept(File pathname) { return pathname.getName().endsWith(".jar"); } }; File[] allJars = new File(path).listFiles(jarFilter); List<URL> jarURLs = new ArrayList<URL>(allJars.length); for (int i = 0; i < allJars.length; i++) { try { jarURLs.add(allJars[i].toURI().toURL()); } catch (Exception e) { throw DataXException.asDataXException( FrameworkErrorCode.PLUGIN_INIT_ERROR, "系统加载jar包出错", e); } } return jarURLs; } }
-
LoadUtil类
LoadUtil管理着插件的加载器,调用getJarLoader返回插件对应的加载器。
public class LoadUtil {
// 加载器的HashMap, Key由插件类型和名称决定, 格式为plugin.{pulginType}.{pluginName}
private static Map<String, JarLoader> jarLoaderCenter = new HashMap<String, JarLoader>();
public static synchronized JarLoader getJarLoader(PluginType pluginType, String pluginName) {
Configuration pluginConf = getPluginConf(pluginType, pluginName);
JarLoader jarLoader = jarLoaderCenter.get(generatePluginKey(pluginType,
pluginName));
if (null == jarLoader) {
// 构建加载器JarLoader
// 获取jar所在的目录
String pluginPath = pluginConf.getString("path");
jarLoader = new JarLoader(new String[]{pluginPath});
//添加到HashMap中
jarLoaderCenter.put(generatePluginKey(pluginType, pluginName),
jarLoader);
}
return jarLoader;
}
private static final String pluginTypeNameFormat = "plugin.%s.%s";
// 生成HashMpa的key值
private static String generatePluginKey(PluginType pluginType,
String pluginName) {
return String.format(pluginTypeNameFormat, pluginType.toString(),
pluginName);
}
当获取类加载器,就可以调用 LoadUtil 来加载插件。
// 加载插件类
// pluginType 代表插件类型
// pluginName 代表插件名称
// pluginRunType 代表着运行类型,Job或者Task
private static synchronized Class<? extends AbstractPlugin> loadPluginClass(
PluginType pluginType, String pluginName,
ContainerType pluginRunType) {
// 获取插件配置
Configuration pluginConf = getPluginConf(pluginType, pluginName);
// 获取插件对应的ClassLoader
JarLoader jarLoader = LoadUtil.getJarLoader(pluginType, pluginName);
try {
// 加载插件的class
return (Class<? extends AbstractPlugin>) jarLoader
.loadClass(pluginConf.getString("class") + "$"
+ pluginRunType.value());
} catch (Exception e) {
throw DataXException.asDataXException(FrameworkErrorCode.RUNTIME_ERROR, e);
}
}
- ClassLoaderSwapper类
public final class ClassLoaderSwapper {
// 保存切换之前的加载器
private ClassLoader storeClassLoader = null;
public ClassLoader setCurrentThreadClassLoader(ClassLoader classLoader) {
// 保存切换前的加载器
this.storeClassLoader = Thread.currentThread().getContextClassLoader();
// 切换加载器到classLoader
Thread.currentThread().setContextClassLoader(classLoader);
return this.storeClassLoader;
}
public ClassLoader restoreCurrentThreadClassLoader() {
ClassLoader classLoader = Thread.currentThread()
.getContextClassLoader();
// 切换到原来的加载器
Thread.currentThread().setContextClassLoader(this.storeClassLoader);
// 返回切换之前的类加载器
return classLoader;
}
}
切换类加载器:
// 实例化
ClassLoaderSwapper classLoaderSwapper = ClassLoaderSwapper.newCurrentThreadClassLoaderSwapper();
ClassLoader classLoader1 = new URLClassLoader();
// 切换加载器classLoader1
classLoaderSwapper.setCurrentThreadClassLoader(classLoader1);
Class<? extends MyClass> myClass = classLoader1.loadClass("MyClass");
// 切回加载器
classLoaderSwapper.restoreCurrentThreadClassLoader();