插件化技术

本文将介绍代码设计中的插件化实现。涉及到的关键技术点 自定义ClassLoaderServiceLoader
接着,会说下插件化技术的典型应用场景。

ClassLoader

类加载的过程

参考:JVM 中关于3.2 类的生命周期 介绍。

显式与隐式加载

显式:在代码中通过调用 ClassLoader 加载 class 对象,如直接使用 Class.forName(name) 或 this.getClass().getClassLoader().loadClass() 加载 class 对象
隐式:通过虚拟机自动加载到内存中,如在加载某个类的 class 文件时,该类的 class 文件中引用了另外一个类的对象,此时额外引用的类将通过 JVM 自动加载到内存中

一段源程序代码:

1
2
3
4
5
6
7
8
9
10
11
12
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());
}
}

生成字节码文件:

1
javac demo.java

对class文件反汇编:

1
2
3
4
5
javap -v -l -c demo.class > Demo.txt

-v: 不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息
-l: 输出行号和本地变量表信息
-c: 会对当前 class 字节码进行反编译生成汇编代码

通过文件编译工具来查看demo.txt的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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启动类图:

加载器类型

  • 启动类加载器,由C++实现,没有父类。
  • 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
  • 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
  • 自定义类加载器,父类加载器肯定为AppClassLoader。

加载器之间的类图关系:

loadClass(String)

将类加载请求到来时,先从缓存中查找该类对象,如果不存在就走双亲委派模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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() 方法一起使用的。

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

defineClass(byte[] b,int off,int len)

将byte字节流解析成JVM 能够识别的Class对象。

1
2
3
4
5
6
7
8
9
10
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方法,主要对字节码进行验证,为类变量分配内存并设置初始值,将字节码文件中的符号引用转换为直接引用。

1
2
3
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}

自定义ClassLoader

为什么要自定义ClassLoader呢?

  • 当 class 文件不在 classpath 路径下,默认系统类加载无法找到该 class 文件,此时需要实现一个自定义的 ClassLoader 来加载特定路径下的 class 文件生成 Class 对象
  • 当一个 class 文件是通过网络传输并且可能会进行相应的加密操作时,需要先对 class 文件进行相应的解密后再加载到 JVM 内存中
  • 当需要实现热部署功能时,一个 class 文件通过不同的类加载器产生不同 class 对象从而实现热部署功能

自定义FileClassLoader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* 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;
}
}

}

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设置到当前线程上下文:

1
2
3
4
5
6
7
8
9
10
11
// 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 的成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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方法:

1
2
3
4
5
6
7
8
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方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//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方法:

1
2
3
4
5
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。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    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返回插件对应的加载器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 来加载插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 加载插件类
// 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类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}
}

切换类加载器:

1
2
3
4
5
6
7
8
9
// 实例化
ClassLoaderSwapper classLoaderSwapper = ClassLoaderSwapper.newCurrentThreadClassLoaderSwapper();

ClassLoader classLoader1 = new URLClassLoader();
// 切换加载器classLoader1
classLoaderSwapper.setCurrentThreadClassLoader(classLoader1);
Class<? extends MyClass> myClass = classLoader1.loadClass("MyClass");
// 切回加载器
classLoaderSwapper.restoreCurrentThreadClassLoader();

解决大数据引擎及版本众多问题

WMRouter中对ServiceLoader的改进与使用