blob: 9fd9e66bc033d4c54c6e04530ea22a5efcadf3a9 [file] [log] [blame]
/*
* 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.
*/
package org.apache.nifi.nar;
import org.apache.nifi.annotation.behavior.RequiresInstanceClassLoading;
import org.apache.nifi.authentication.LoginIdentityProvider;
import org.apache.nifi.authorization.Authorizer;
import org.apache.nifi.controller.ControllerService;
import org.apache.nifi.controller.repository.ContentRepository;
import org.apache.nifi.controller.repository.FlowFileRepository;
import org.apache.nifi.controller.repository.FlowFileSwapManager;
import org.apache.nifi.controller.status.history.ComponentStatusRepository;
import org.apache.nifi.flowfile.FlowFilePrioritizer;
import org.apache.nifi.processor.Processor;
import org.apache.nifi.provenance.ProvenanceRepository;
import org.apache.nifi.reporting.ReportingTask;
import org.apache.nifi.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Scans through the classpath to load all FlowFileProcessors, FlowFileComparators, and ReportingTasks using the service provider API and running through all classloaders (root, NARs).
*
* @ThreadSafe - is immutable
*/
@SuppressWarnings("rawtypes")
public class ExtensionManager {
private static final Logger logger = LoggerFactory.getLogger(ExtensionManager.class);
// Maps a service definition (interface) to those classes that implement the interface
private static final Map<Class, Set<Class>> definitionMap = new HashMap<>();
private static final Map<String, ClassLoader> extensionClassloaderLookup = new HashMap<>();
private static final Set<String> requiresInstanceClassLoading = new HashSet<>();
private static final Map<String, ClassLoader> instanceClassloaderLookup = new ConcurrentHashMap<>();
static {
definitionMap.put(Processor.class, new HashSet<>());
definitionMap.put(FlowFilePrioritizer.class, new HashSet<>());
definitionMap.put(ReportingTask.class, new HashSet<>());
definitionMap.put(ControllerService.class, new HashSet<>());
definitionMap.put(Authorizer.class, new HashSet<>());
definitionMap.put(LoginIdentityProvider.class, new HashSet<>());
definitionMap.put(ProvenanceRepository.class, new HashSet<>());
definitionMap.put(ComponentStatusRepository.class, new HashSet<>());
definitionMap.put(FlowFileRepository.class, new HashSet<>());
definitionMap.put(FlowFileSwapManager.class, new HashSet<>());
definitionMap.put(ContentRepository.class, new HashSet<>());
}
/**
* Loads all FlowFileProcessor, FlowFileComparator, ReportingTask class types that can be found on the bootstrap classloader and by creating classloaders for all NARs found within the classpath.
* @param extensionLoaders the loaders to scan through in search of extensions
*/
public static void discoverExtensions(final Set<ClassLoader> extensionLoaders) {
final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
// get the current context class loader
ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader();
// consider the system class loader
loadExtensions(systemClassLoader);
// consider each nar class loader
for (final ClassLoader ncl : extensionLoaders) {
// 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
Thread.currentThread().setContextClassLoader(ncl);
loadExtensions(ncl);
}
// restore the current context class loader if appropriate
if (currentContextClassLoader != null) {
Thread.currentThread().setContextClassLoader(currentContextClassLoader);
}
}
/**
* Loads extensions from the specified class loader.
*
* @param classLoader from which to load extensions
*/
@SuppressWarnings("unchecked")
private static void loadExtensions(final ClassLoader classLoader) {
for (final Map.Entry<Class, Set<Class>> entry : definitionMap.entrySet()) {
final ServiceLoader<?> serviceLoader = ServiceLoader.load(entry.getKey(), classLoader);
for (final Object o : serviceLoader) {
registerServiceClass(o.getClass(), extensionClassloaderLookup, classLoader, entry.getValue());
}
}
}
/**
* Registers extension for the specified type from the specified ClassLoader.
*
* @param type the extension type
* @param classloaderMap mapping of classname to classloader
* @param classLoader the classloader being mapped to
* @param classes to map to this classloader but which come from its ancestors
*/
private static void registerServiceClass(final Class<?> type, final Map<String, ClassLoader> classloaderMap, final ClassLoader classLoader, final Set<Class> classes) {
final String className = type.getName();
final ClassLoader registeredClassLoader = classloaderMap.get(className);
// see if this class is already registered (this should happen when the class is loaded by an ancestor of the specified classloader)
if (registeredClassLoader == null) {
classloaderMap.put(className, classLoader);
classes.add(type);
// keep track of which classes require a class loader per component instance
if (type.isAnnotationPresent(RequiresInstanceClassLoading.class)) {
requiresInstanceClassLoading.add(className);
}
} else {
boolean loadedFromAncestor = false;
// determine if this class was loaded from an ancestor
ClassLoader ancestorClassLoader = classLoader.getParent();
while (ancestorClassLoader != null) {
if (ancestorClassLoader == registeredClassLoader) {
loadedFromAncestor = true;
break;
}
ancestorClassLoader = ancestorClassLoader.getParent();
}
// if this class was loaded from a non ancestor class loader, report potential unexpected behavior
if (!loadedFromAncestor) {
logger.warn("Attempt was made to load " + className + " from " + classLoader
+ " but that class name is already loaded/registered from " + registeredClassLoader
+ ". This may cause unpredictable behavior. Order of NARs is not guaranteed.");
}
}
}
/**
* Determines the effective classloader for classes of the given type. If returns null it indicates the given type is not known or was not detected.
*
* @param classType to lookup the classloader of
* @return String of fully qualified class name; null if not a detected type
*/
public static ClassLoader getClassLoader(final String classType) {
return extensionClassloaderLookup.get(classType);
}
/**
* Determines the effective ClassLoader for the instance of the given type.
*
* @param classType the type of class to lookup the ClassLoader for
* @param instanceIdentifier the identifier of the specific instance of the classType to look up the ClassLoader for
* @return the ClassLoader for the given instance of the given type, or null if the type is not a detected extension type
*/
public static ClassLoader getClassLoader(final String classType, final String instanceIdentifier) {
if (StringUtils.isEmpty(classType) || StringUtils.isEmpty(instanceIdentifier)) {
throw new IllegalArgumentException("Class Type and Instance Identifier must be provided");
}
// Check if we already have a ClassLoader for this instance
ClassLoader instanceClassLoader = instanceClassloaderLookup.get(instanceIdentifier);
// If we don't then we'll create a new ClassLoader for this instance and add it to the map for future lookups
if (instanceClassLoader == null) {
final ClassLoader registeredClassLoader = getClassLoader(classType);
if (registeredClassLoader == null) {
return null;
}
// If the class is annotated with @RequiresInstanceClassLoading and the registered ClassLoader is a URLClassLoader
// then make a new InstanceClassLoader that is a full copy of the NAR Class Loader, otherwise create an empty
// InstanceClassLoader that has the NAR ClassLoader as a parent
if (requiresInstanceClassLoading.contains(classType) && (registeredClassLoader instanceof URLClassLoader)) {
final URLClassLoader registeredUrlClassLoader = (URLClassLoader) registeredClassLoader;
instanceClassLoader = new InstanceClassLoader(instanceIdentifier, classType, registeredUrlClassLoader.getURLs(), registeredUrlClassLoader.getParent());
} else {
instanceClassLoader = new InstanceClassLoader(instanceIdentifier, classType, new URL[0], registeredClassLoader);
}
instanceClassloaderLookup.put(instanceIdentifier, instanceClassLoader);
}
return instanceClassLoader;
}
/**
* Removes the ClassLoader for the given instance and closes it if necessary.
*
* @param instanceIdentifier the identifier of a component to remove the ClassLoader for
* @return the removed ClassLoader for the given instance, or null if not found
*/
public static ClassLoader removeInstanceClassLoaderIfExists(final String instanceIdentifier) {
if (instanceIdentifier == null) {
return null;
}
final ClassLoader classLoader = instanceClassloaderLookup.remove(instanceIdentifier);
if (classLoader != null && (classLoader instanceof URLClassLoader)) {
final URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
try {
urlClassLoader.close();
} catch (IOException e) {
logger.warn("Unable to class URLClassLoader for " + instanceIdentifier);
}
}
return classLoader;
}
/**
* Checks if the given class type requires per-instance class loading (i.e. contains the @RequiresInstanceClassLoading annotation)
*
* @param classType the class to check
* @return true if the class is found in the set of classes requiring instance level class loading, false otherwise
*/
public static boolean requiresInstanceClassLoading(final String classType) {
return requiresInstanceClassLoading.contains(classType);
}
public static Set<Class> getExtensions(final Class<?> definition) {
final Set<Class> extensions = definitionMap.get(definition);
return (extensions == null) ? Collections.<Class>emptySet() : extensions;
}
public static void logClassLoaderMapping() {
final StringBuilder builder = new StringBuilder();
builder.append("Extension Type Mapping to Classloader:");
for (final Map.Entry<Class, Set<Class>> entry : definitionMap.entrySet()) {
builder.append("\n\t=== ").append(entry.getKey().getSimpleName()).append(" type || Classloader ===");
for (final Class type : entry.getValue()) {
builder.append("\n\t").append(type.getName()).append(" || ").append(getClassLoader(type.getName()));
}
builder.append("\n\t=== End ").append(entry.getKey().getSimpleName()).append(" types ===");
}
logger.info(builder.toString());
}
}