
org.apache.pulsar.common.nar.NarClassLoader Maven / Gradle / Ivy
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* This class was adapted from NiFi NAR Utils
* https://github.com/apache/nifi/tree/master/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils
*/
package org.apache.pulsar.common.nar;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
/**
*
* A ClassLoader for loading NARs (NiFi archives). NARs are designed to allow isolating bundles of code
* (comprising one-or-more NiFi FlowFileProcessors, FlowFileComparators and their dependencies) from
* other such bundles; this allows for dependencies and processors that require conflicting, incompatible versions of
* the same dependency to run in a single instance of NiFi.
*
*
*
* NarClassLoader follows the delegation model described in {@link ClassLoader#findClass(java.lang.String)
* ClassLoader.findClass(...)}; classes are first loaded from the parent ClassLoader, and only if they cannot
* be found there does the NarClassLoader provide a definition. Specifically, this means that resources are
* loaded from NiFi's conf and lib directories first, and if they cannot be found there, are loaded
* from the NAR.
*
*
*
* The packaging of a NAR is such that it is a ZIP file with the following directory structure:
*
*
* +META-INF/
* +-- bundled-dependencies/
* +-- <JAR files>
* +-- MANIFEST.MF
*
*
*
*
* The MANIFEST.MF file contains the same information as a typical JAR file but also includes two additional NiFi
* properties: {@code Nar-Id} and {@code Nar-Dependency-Id}.
*
*
*
* The {@code Nar-Id} provides a unique identifier for this NAR.
*
*
*
* The {@code Nar-Dependency-Id} is optional. If provided, it indicates that this NAR should inherit all of the
* dependencies of the NAR with the provided ID. Often times, the NAR that is depended upon is referred to as the
* Parent. This is because its ClassLoader will be the parent ClassLoader of the dependent NAR.
*
*
*
* If a NAR is built using NiFi's Maven NAR Plugin, the {@code Nar-Id} property will be set to the artifactId of the
* NAR. The {@code Nar-Dependency-Id} will be set to the artifactId of the NAR that is depended upon. For example, if
* NAR A is defined as such:
*
*
* ...
* <artifactId>nar-a</artifactId>
* <packaging>nar</packaging>
* ...
* <dependencies>
* <dependency>
* <groupId>group</groupId>
* <artifactId>nar-z</artifactId>
* <type>nar</type>
* </dependency>
* </dependencies>
*
*
*
*
*
* Then the MANIFEST.MF file that is created for NAR A will have the following properties set:
*
* - {@code Nar-Id: nar-a}
* - {@code Nar-Dependency-Id: nar-z}
*
*
*
*
* Note, above, that the {@code type} of the dependency is set to {@code nar}.
*
*
*
* If the NAR has more than one dependency of {@code type} {@code nar}, then the Maven NAR plugin will fail to build the
* NAR.
*
*/
@Slf4j
public class NarClassLoader extends URLClassLoader {
private static final FileFilter JAR_FILTER = pathname -> {
final String nameToTest = pathname.getName().toLowerCase();
return nameToTest.endsWith(".jar") && pathname.isFile();
};
/**
* The NAR for which this ClassLoader is responsible.
*/
private final File narWorkingDirectory;
private static final String TMP_DIR_PREFIX = "pulsar-nar";
public static final String DEFAULT_NAR_EXTRACTION_DIR = System.getProperty("java.io.tmpdir");
public static NarClassLoader getFromArchive(File narPath, Set additionalJars,
String narExtractionDirectory) throws IOException {
return NarClassLoader.getFromArchive(narPath, additionalJars, NarClassLoader.class.getClassLoader(),
NarClassLoader.DEFAULT_NAR_EXTRACTION_DIR);
}
public static NarClassLoader getFromArchive(File narPath, Set additionalJars) throws IOException {
return NarClassLoader.getFromArchive(narPath, additionalJars, NarClassLoader.DEFAULT_NAR_EXTRACTION_DIR);
}
public static NarClassLoader getFromArchive(File narPath, Set additionalJars, ClassLoader parent,
String narExtractionDirectory)
throws IOException {
File unpacked = NarUnpacker.unpackNar(narPath, getNarExtractionDirectory(narExtractionDirectory));
try {
return new NarClassLoader(unpacked, additionalJars, parent);
} catch (ClassNotFoundException | NoClassDefFoundError e) {
throw new IOException(e);
}
}
private static File getNarExtractionDirectory(String configuredDirectory) {
return new File(configuredDirectory + "/" + TMP_DIR_PREFIX);
}
/**
* Construct a nar class loader.
*
* @param narWorkingDirectory
* directory to explode nar contents to
* @param parent
* @throws IllegalArgumentException
* if the NAR is missing the Java Services API file for FlowFileProcessor implementations.
* @throws ClassNotFoundException
* if any of the FlowFileProcessor implementations defined by the Java Services API cannot be
* loaded.
* @throws IOException
* if an error occurs while loading the NAR.
*/
private NarClassLoader(final File narWorkingDirectory, Set additionalJars, ClassLoader parent)
throws ClassNotFoundException, IOException {
super(new URL[0], parent);
this.narWorkingDirectory = narWorkingDirectory;
// process the classpath
updateClasspath(narWorkingDirectory);
for (String jar : additionalJars) {
if (jar.startsWith("/")) {
addURL(new URL("file://" + jar));
} else {
Path currentRelativePath = Paths.get("");
String cwd = currentRelativePath.toAbsolutePath().toString();
addURL(new URL("file://" + cwd + "/" + jar));
}
}
if (log.isDebugEnabled()) {
log.info("Created class loader with paths: {}", Arrays.toString(getURLs()));
}
}
public File getWorkingDirectory() {
return narWorkingDirectory;
}
/**
* Read a service definition as a String.
*/
public String getServiceDefinition(String serviceName) throws IOException {
String serviceDefPath = narWorkingDirectory + "/META-INF/services/" + serviceName;
return new String(Files.readAllBytes(Paths.get(serviceDefPath)));
}
public List getServiceImplementation(String serviceName) throws IOException {
List impls = new ArrayList<>();
String serviceDefPath = narWorkingDirectory + "/META-INF/services/" + serviceName;
try (BufferedReader reader = new BufferedReader(new FileReader(serviceDefPath))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && !line.startsWith("#")) {
final int indexOfPound = line.indexOf("#");
final String effectiveLine = (indexOfPound > 0) ? line.substring(0, indexOfPound) : line;
impls.add(effectiveLine);
}
}
}
return impls;
}
/**
* Adds URLs for the resources unpacked from this NAR:
*
* - the root: for classes, META-INF, etc.
* - META-INF/dependencies: for config files, .sos, etc.
* - META-INF/dependencies/*.jar: for dependent libraries
*
*
* @param root
* the root directory of the unpacked NAR.
* @throws IOException
* if the URL list could not be updated.
*/
private void updateClasspath(File root) throws IOException {
addURL(root.toURI().toURL()); // for compiled classes, META-INF/, etc.
File dependencies = new File(root, "META-INF/bundled-dependencies");
if (!dependencies.isDirectory()) {
log.warn("{} does not contain META-INF/bundled-dependencies!", narWorkingDirectory);
}
addURL(dependencies.toURI().toURL());
if (dependencies.isDirectory()) {
final File[] jarFiles = dependencies.listFiles(JAR_FILTER);
if (jarFiles != null) {
Arrays.sort(jarFiles, Comparator.comparing(File::getName));
for (File libJar : jarFiles) {
addURL(libJar.toURI().toURL());
}
}
}
}
@Override
protected String findLibrary(final String libname) {
File dependencies = new File(narWorkingDirectory, "META-INF/bundled-dependencies");
if (!dependencies.isDirectory()) {
log.warn("{} does not contain META-INF/bundled-dependencies!", narWorkingDirectory);
}
final File nativeDir = new File(dependencies, "native");
final File libsoFile = new File(nativeDir, "lib" + libname + ".so");
final File dllFile = new File(nativeDir, libname + ".dll");
final File soFile = new File(nativeDir, libname + ".so");
if (libsoFile.exists()) {
return libsoFile.getAbsolutePath();
} else if (dllFile.exists()) {
return dllFile.getAbsolutePath();
} else if (soFile.exists()) {
return soFile.getAbsolutePath();
}
// not found in the nar. try system native dir
return null;
}
@Override
public String toString() {
return NarClassLoader.class.getName() + "[" + narWorkingDirectory.getPath() + "]";
}
}