title: Apache NIFI项目结构的类资源隔离机制
date: 2020-07-11
categories:

  • Apache NIFI
    tags:
  • Apache NIFI
    author: 张诚
    location: BeiJing
    publish: true

前言

本文简单的讨论一下Apache NIFI项目结构的类资源隔离机制,适合接触过源码的同学阅读。

NIFI的常见的子Moudle结构

以nifi-flume-bundle为例

  1. nifi-flume-bundle
  2. -- nifi-flume-processors
  3. -- nifi-flume-nar

nifi-flume-bundle 有两个子项目,nifi-flume-processors里是Processor的具体实现,打成jar包。nifi-flume-nar里没有代码实现负责将nifi-flume-processors.jar及其依赖打成nar包。

NAR是什么?

NAR是NiFi Archive的缩写,创建nar的原因是为了实现Java类加载器隔离资源。NIFI的组件实现都来自不同的公司和贡献者,代码里往往会引入不同版本的第三方库(比如apache-commons等)。NAR文件避免了NoClassDefFoundError异常的出现(这些异常是由于在不同处理器的类加载器中已经加载了错误版本的依赖而引发的)。

NAR文件结构

META-INF
    bundled-dependencies    
        async-1.4.0.jar
        ...
        flume-dataset-sink-1.6.0.jar
        ...
        nifi-flume-processors-1.11.4.jar
        nifi-utils-1.11.4.jar
        ...
    DEPENDENCIES
    docs
        ...
    LICENSE
    MANIFEST.MF
    maven
        org.apache.nifi
            nifi-flume-nar
                pom.properties
                pom.xml
    NOTICE

NAR文件实际上跟WAR和NAR差不多,但有一些区别。 NAR的根目录是META-INF(与JAR中一样,而WAR是WEB-INF)。 META-INF根目录下是描述性文件,例如LICENSE,DEPENDENCIES(列出捆绑的依赖项的许可证信息)和NOTICE(包含处理器本身的许可证)。

此外,META-INF下还有3个关键文件和目录。首先是MANIFEST.MF文件,它跟jar里的文件基本相同,但是,其中多包含一些NAR信息,比如Nar-Id用来识别nar,Nar-Version是NAR里处理器的版本,Nar-Dependency-Id是当前NAR所依赖的NAR的ID(nar不能依赖多个其他nar)等等,还包括有关用于构建NAR的Java和Maven版本以及其来源的有用元数据。

Manifest-Version: 1.0
Build-Branch: develop
Build-Timestamp: 2020-07-09T11:08:58Z
Archiver-Version: Plexus Archiver
Nar-Dependency-Group: org.apache.nifi
Built-By: zhangcheng
Nar-Id: nifi-flume-nar
Clone-During-Instance-Class-Loading: false
Nar-Dependency-Version: 1.11.4
Nar-Version: 1.11.4
Build-Tag: nifi-1.11.4-RC1
Build-Revision: 7c28976
Nar-Group: org.apache.nifi
Nar-Dependency-Id: nifi-hadoop-libraries-nar
Created-By: Apache Maven 3.6.1
Build-Jdk: 1.8.0_181

maven目录包含用于构建NAR的POM文件(Maven构建描述文件),以及一个pom.properties文件,其中包含NAR的maven兼容详细信息(maven用于依赖性识别的3个关键元素,groupId,artifiactId,version)

#Generated by Maven
#Thu Jul 09 11:08:58 CST 2020
version=1.11.4
groupId=org.apache.nifi
artifactId=nifi-flume-nar

bundled-dependencies目录下是组件的jar包及其依赖的其他jar包,这些jar会被特定的类加载器加载。

以nar为基础的类资源隔离

在NIFI启动时,会把lib目录下的nar文件都解压到work/nar目录下。其中nifi-framework-nar-1.11.4.nar会单独解压到work/nar/framework/下,其他的nar文件都解压到work/nar/extensions/下,对应的目录类似于nifi-flume-nar-1.11.4.ar-unpacked

NIFI启动源码解读NiFi.java 源码解读NIFI Nar包加载机制源码解读中我们说过每一个nar包对应创建一个类加载器,使用不同的类加载器去加载这个nar资源。

NarClassLoader

遵循JAVA的类加载器委托加载机制,由NarClassLoaders的createNarClassLoader创建

private static ClassLoader createNarClassLoader(final File narDirectory, final ClassLoader parentClassLoader) throws IOException, ClassNotFoundException {
    logger.debug("Loading NAR file: " + narDirectory.getAbsolutePath());
    final ClassLoader narClassLoader = new NarClassLoader(narDirectory, parentClassLoader);
    logger.info("Loaded NAR file: " + narDirectory.getAbsolutePath() + " as class loader " + narClassLoader);
    return narClassLoader;
}

每一个nar包对应创建一个类加载器,源码里JettyServer.start()里启动jetty前调用extensionManager.discoverExtensions(systemBundle, bundles);(bundles里记录了nar信息和对应的类加载器),

@Override
public void discoverExtensions(final Set<Bundle> narBundles) {
    // get the current context class loader
    ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader();

    // consider each nar class loader
    for (final Bundle bundle : narBundles) {
        // Must set the context class loader to the nar classloader itself
        // so that static initialization techniques that depend on the context class loader will work properly
        final ClassLoader ncl = bundle.getClassLoader();
        Thread.currentThread().setContextClassLoader(ncl);
        loadExtensions(bundle);

        // Create a look-up from coordinate to bundle
        bundleCoordinateBundleLookup.put(bundle.getBundleDetails().getCoordinate(), bundle);
    }

    // restore the current context class loader if appropriate
    if (currentContextClassLoader != null) {
        Thread.currentThread().setContextClassLoader(currentContextClassLoader);
    }
}

在loadExtensions(bundle)里使用SPI机制ServiceLoader去加载各个组件的class信息,而组件代码所涉及的其他类的class也会隐式的由当前组件的Class对象中引用的类加载器去加载,这样就完成了整个项目架构以nar为基础的类资源隔离。

private void loadExtensions(final Bundle bundle) {
        for (final Map.Entry<Class, Set<Class>> entry : definitionMap.entrySet()) {
            final boolean isControllerService = ControllerService.class.equals(entry.getKey());
            final boolean isProcessor = Processor.class.equals(entry.getKey());
            final boolean isReportingTask = ReportingTask.class.equals(entry.getKey());
            //SPI机制
            final ServiceLoader<?> serviceLoader = ServiceLoader.load(entry.getKey(), bundle.getClassLoader());
            for (final Object o : serviceLoader) {
                try {
                    loadExtension(o, entry.getKey(), bundle);
                } catch (Exception e) {
                    logger.warn("Failed to register extension {} due to: {}" , new Object[]{o.getClass().getCanonicalName(), e.getMessage()});
                    if (logger.isDebugEnabled()) {
                        logger.debug("", e);
                    }
                }
            }

            classLoaderBundleLookup.put(bundle.getClassLoader(), bundle);
        }
    }

公众号

关注公众号 得到第一手文章/文档更新推送。

Apache NIFI项目结构的类资源隔离机制 - 图1