你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

插件化技术

2021/11/15 23:43:16

转载于插件化技术 发表于 2020-03-29 | 分类于 Java

插件化技术

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

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();

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

WMRouter中对ServiceLoader的改进与使用