/** * Copied from YARN * * Copyright 2015 StreamSets Inc. * * Licensed under 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 com.streamsets.pipeline; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; /** * A {@link URLClassLoader} for application isolation. There are two * configuration types supported by StageClassLoader. The first * is System classes which are always delegated to the parent * and the second is application classes which are never delegated * to the parent. */ public class SDCClassLoader extends BlackListURLClassLoader { /* * Note: * if you update this, you must also update api-children-classloader.properties */ private static final String[] PACKAGES_BLACKLIST_FOR_STAGE_LIBRARIES = { "com.streamsets.pipeline.api.", "com.streamsets.pipeline.container.", "com.codehale.metrics.", "org.slf4j.", "org.apache.log4j." }; /** * Default value of the system classes if the user did not override them. * JDK classes, hadoop classes and resources, and some select third-party * classes are considered system classes, and are not loaded by the * application classloader. */ static final List<String> SYSTEM_API_CLASSES; static final List<String> SYSTEM_API_CHILDREN_CLASSES; private static String API = "api"; private static String API_CHILDREN = "api-children"; private static final String[] CLASSLOADER_TYPES = new String[] { API, API_CHILDREN }; private static final String SYSTEM_CLASSES_DEFAULT_KEY = "system.classes.default"; private static boolean debug = false; public static void setDebug(boolean debug) { SDCClassLoader.debug = debug; } public static boolean isDebug() { return debug; } static { Map<String, String> systemClassesDefaultsMap = new HashMap<>(); for (String classLoaderType : CLASSLOADER_TYPES) { String propertiesFile = classLoaderType + "-classloader.properties"; try (InputStream is = SDCClassLoader.class.getClassLoader() .getResourceAsStream(propertiesFile);) { if (is == null) { throw new ExceptionInInitializerError("properties file " + propertiesFile + " is not found"); } Properties props = new Properties(); props.load(is); // get the system classes default String systemClassesDefault = props.getProperty(SYSTEM_CLASSES_DEFAULT_KEY); if (systemClassesDefault == null) { throw new ExceptionInInitializerError("property " + SYSTEM_CLASSES_DEFAULT_KEY + " is not found"); } systemClassesDefaultsMap.put(classLoaderType, systemClassesDefault); } catch (IOException e) { throw new ExceptionInInitializerError(e); } } SYSTEM_API_CLASSES = Collections.unmodifiableList(Arrays.asList(ClassLoaderUtil.getTrimmedStrings( ClassLoaderUtil.checkNotNull(systemClassesDefaultsMap.get(API), API)))); List<String> apiChildren = new ArrayList<>(Arrays.asList(ClassLoaderUtil.getTrimmedStrings( ClassLoaderUtil.checkNotNull(systemClassesDefaultsMap.get(API_CHILDREN), API_CHILDREN)))); apiChildren.addAll(SYSTEM_API_CLASSES); SYSTEM_API_CHILDREN_CLASSES = Collections.unmodifiableList(apiChildren); } private final ClassLoader parent; private final boolean parentIsAPIClassLoader; private final SystemPackage systemPackage; private final boolean isPrivate; private final ApplicationPackage applicationPackage; public SDCClassLoader(String type, String name, List<URL> urls, ClassLoader parent, String[] blacklistedPackages, SystemPackage systemPackage, ApplicationPackage applicationPackage, boolean isPrivate, boolean parentIsAPIClassLoader, boolean isStageLibClassLoader) { super(type, name, getOrderedURLsForClassLoader(urls, isStageLibClassLoader, name), parent, blacklistedPackages); if (debug) { System.err.println(getClass().getSimpleName() + " " + getName() + ": urls: " + Arrays.toString(urls.toArray())); System.err.println(getClass().getSimpleName() + " " + getName() + ": system classes: " + systemPackage); } this.parent = parent; this.parentIsAPIClassLoader = parentIsAPIClassLoader; if (parent == null) { throw new IllegalArgumentException("No parent classloader!"); } if (debug) { System.err.println(getClass().getSimpleName() + " " + getName() + ": parent classloader: " + parent); } if (systemPackage == null) { throw new IllegalArgumentException("System classes cannot be null"); } // if the caller-specified system classes are null or empty, use the default this.systemPackage = systemPackage; if(debug) { System.err.println(getClass().getSimpleName() + " " + getName() + ": system classes: " + this.systemPackage); } this.applicationPackage = applicationPackage; this.isPrivate = isPrivate; if(debug) { System.err.println(getClass().getSimpleName() + " " + getName() + ": application packages: " + this.applicationPackage); } } /** * Arranges the urls in the following order: * <ul> * <li>stage lib jars</li> * <li>protolib jars</li> * <li>non protolib jars</li> * </ul> * * @param stageLibName * @param urls * @return */ static List<URL> bringStageAndProtoLibsToFront(String stageLibName, List<URL> urls) { List<URL> otherJars = new ArrayList<>(); List<URL> protolibJars = new ArrayList<>(); List<URL> stageLibjars = new ArrayList<>(); for (URL url : urls) { String str = url.toExternalForm(); if (str.endsWith(".jar")) { int nameIdx = str.lastIndexOf("/"); if (nameIdx > -1) { String jarName = str.substring(nameIdx + 1); if (jarName.contains("-protolib-")) { // adding only protolib jars protolibJars.add(url); } else if (jarName.contains(stageLibName)) { stageLibjars.add(url); } else { otherJars.add(url); } } else { otherJars.add(url); } } else { otherJars.add(url); } } List<URL> allJars = new ArrayList<>(); if (stageLibjars.size() != 1) { throw new ExceptionInInitializerError("Expected exactly 1 stage lib jar but found " + stageLibjars.size() + " with name " + stageLibName); } allJars.addAll(stageLibjars); allJars.addAll(protolibJars); allJars.addAll(otherJars); return allJars; } @Override public URL getResource(String name) { URL url = null; boolean isSystemPackage = systemPackage.isSystem(name); if (!isSystemPackage) { url = findResource(name); if (url == null && name.startsWith("/")) { if (debug) { System.err.println(getClass().getSimpleName() + " " + getName() + ": Remove leading / off " + name); } url = findResource(name.substring(1)); } } if (url == null && (isSystemPackage || !applicationPackage.isApplication(name))) { url = parent.getResource(name); } if (url != null) { if (debug) { System.err.println(getClass().getSimpleName() + " " + getName() + ": getResource(" + name + ")=" + url); } } return url; } @Override public Enumeration<URL> getResources(String name) throws IOException { if (debug) { System.err.println("getResources(" + name + ")"); } Enumeration<URL> result = null; if (!systemPackage.isSystem(name)) { // Search local repositories if (debug) { System.err.println(" Searching local repositories"); } result = findResources(name); if (result != null && result.hasMoreElements()) { if (debug) { System.err.println(" --> Returning result from local"); } return result; } if (applicationPackage.isApplication(name)) { if (debug) { System.err.println(" --> application class, returning empty enumeration"); } return Collections.emptyEnumeration(); } } // Delegate to parent unconditionally if (debug) { System.err.println(" Delegating to parent classloader unconditionally " + parent); } result = parent.getResources(name); if (result != null && result.hasMoreElements()) { if (debug) { List<URL> resultList = Collections.list(result); result = Collections.enumeration(resultList); System.err.println(" --> Returning result from parent: " + resultList); } return result; } // (4) Resource was not found if (debug) { System.err.println(" --> Resource not found, returning empty enumeration"); } return Collections.emptyEnumeration(); } @Override public InputStream getResourceAsStream(String name) { if (debug) { System.err.println("getResourceAsStream(" + name + ")"); } InputStream stream = null; if (!systemPackage.isSystem(name)) { // Search local repositories if (debug) { System.err.println(" Searching local repositories"); } URL url = findResource(name); if (url != null) { if (debug) { System.err.println(" --> Returning stream from local"); } try { return url.openStream(); } catch (IOException e) { // Ignore } } if (applicationPackage.isApplication(name)) { if (debug) { System.err.println(" --> application class, returning null"); } return null; } } // Delegate to parent unconditionally if (debug) { System.err.println(" Delegating to parent classloader unconditionally " + parent); } stream = parent.getResourceAsStream(name); if (stream != null) { if (debug) { System.err.println(" --> Returning stream from parent"); } return stream; } // (4) Resource was not found if (debug) { System.err.println(" --> Resource not found, returning null"); } return null; } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { return this.loadClass(name, false); } @Override protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { if (debug) { System.err.println(getClass().getSimpleName() + " " + getName() + ": Loading class: " + name); } Class<?> c = findLoadedClass(name); ClassNotFoundException ex = null; boolean isSystemPackage = systemPackage.isSystem(name); if (c == null && !isSystemPackage) { // Try to load class from this classloader's URLs. Note that this is like // the servlet spec, not the usual Java 2 behaviour where we ask the // parent to attempt to load first. try { c = findClass(name); if (debug && c != null) { System.err.println(getClass().getSimpleName() + " " + getName() + ": Loaded class: " + name + " "); } } catch (ClassNotFoundException e) { if (debug) { System.err.println(getClass().getSimpleName() + " " + getName() + ": " + e); } ex = e; } } // try parent classloader in the following situations: // 1. Package has been marked system // 2. parent is the API classloader // under most circumstances we do not want to try the parent classloader // for application classes, however this is not true if the parent is the api // classloader since we load the api and codahale/dropwizard metrics from there // 3. Class is not an application class if (c == null && (isSystemPackage || parentIsAPIClassLoader || !applicationPackage.isApplication(name))) { c = parent.loadClass(name); if (debug && c != null) { System.err.println(getClass().getSimpleName() + " " + getName() + ": Loaded class from parent: " + name + " "); } } if (c == null) { throw ex != null ? ex : new ClassNotFoundException(name); } if (resolve) { resolveClass(c); } return c; } public static SDCClassLoader getAPIClassLoader(List<URL> apiURLs, ClassLoader parent) { return new SDCClassLoader("api-lib", "API", apiURLs, parent, null, new SystemPackage(SYSTEM_API_CLASSES), ApplicationPackage.get(parent), false, false, false); } public static SDCClassLoader getContainerCLassLoader(List<URL> containerURLs, ClassLoader apiCL) { return new SDCClassLoader("container-lib", "Container", containerURLs, apiCL, null, new SystemPackage(SYSTEM_API_CHILDREN_CLASSES), ApplicationPackage.get(apiCL.getParent()), false, true, false); } public static SDCClassLoader getStageClassLoader(String type, String name, List<URL> libURLs, ClassLoader apiCL) { return getStageClassLoader(type, name, libURLs, apiCL, false); } public static SDCClassLoader getStageClassLoader(String type, String name, List<URL> libURLs, ClassLoader apiCL, boolean isPrivate) { return new SDCClassLoader(type, name, libURLs, apiCL, PACKAGES_BLACKLIST_FOR_STAGE_LIBRARIES, new SystemPackage(SYSTEM_API_CHILDREN_CLASSES), ApplicationPackage.get(apiCL.getParent()), isPrivate, true, true); } public SDCClassLoader duplicateStageClassLoader() { return getStageClassLoader(getType(), getName(), urls, parent, true); } private static List<URL> getOrderedURLsForClassLoader( List<URL> urls, boolean isStageLibClassLoader, String stageLibName ) { // only for stagelib classloaders, we force stagelib and protolib JARs to be first in the classpath, so they can // override classes from its dependencies. Usecase: Hadoop native compression codecs replacement return (isStageLibClassLoader) ? bringStageAndProtoLibsToFront(stageLibName, urls) : urls; } public boolean isPrivate() { return isPrivate; } public String toString() { return String.format("SDCClassLoader[type=%s name=%s private=%b]", getType(), getName(), isPrivate); } }