blob: 6a1b95146723e77842ff758ddc78b219d2d3bcc7 [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.openejb;
import org.apache.openejb.classloader.ClassLoaderConfigurer;
import org.apache.openejb.classloader.CompositeClassLoaderConfigurer;
import org.apache.openejb.core.TempClassLoader;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
import org.apache.openejb.util.UrlCache;
import org.apache.openejb.util.classloader.URLClassLoaderFirst;
import org.apache.xbean.recipe.ObjectRecipe;
import java.beans.Introspector;
import java.io.File;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.zip.ZipFile;
/**
* @version $Revision: 1423017 $ $Date: 2012-12-17 16:52:12 +0000 (Mon, 17 Dec 2012) $
*/
public class ClassLoaderUtil {
private static final Logger logger = Logger.getInstance(LogCategory.OPENEJB, ClassLoaderUtil.class);
private static final Map<String, List<ClassLoader>> classLoadersByApp = new HashMap<String, List<ClassLoader>>();
private static final Map<ClassLoader, Set<String>> appsByClassLoader = new HashMap<ClassLoader, Set<String>>();
private static final UrlCache localUrlCache = new UrlCache();
public static ClassLoader getContextClassLoader() {
return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
@Override
public ClassLoader run() {
return Thread.currentThread().getContextClassLoader();
}
});
}
public static File getUrlCachedName(final String appId, final URL url) {
return localUrlCache.getUrlCachedName(appId, url);
}
public static boolean isUrlCached(final String appId, final URL url) {
return localUrlCache.isUrlCached(appId, url);
}
public static URL getUrlKeyCached(final String appId, final File file) {
return localUrlCache.getUrlKeyCached(appId, file);
}
public static URLClassLoader createClassLoaderFirst(final String appId, final URL[] urls, final ClassLoader parent) {
return cacheClassLoader(appId, new URLClassLoaderFirst(localUrlCache.cacheUrls(appId, urls), parent));
}
public static URLClassLoader createClassLoader(final String appId, final URL[] urls, final ClassLoader parent) {
return cacheClassLoader(appId, new URLClassLoader(localUrlCache.cacheUrls(appId, urls), parent));
}
private static URLClassLoader cacheClassLoader(final String appId, final URLClassLoader classLoader) {
List<ClassLoader> classLoaders = classLoadersByApp.get(appId);
if (classLoaders == null) {
classLoaders = new ArrayList<ClassLoader>(2);
classLoadersByApp.put(appId, classLoaders);
}
classLoaders.add(classLoader);
Set<String> apps = appsByClassLoader.get(classLoader);
if (apps == null) {
apps = new LinkedHashSet<String>(1);
appsByClassLoader.put(classLoader, apps);
}
apps.add(appId);
return classLoader;
}
/**
* Destroy a classloader as forcefully as possible.
*
* @param classLoader ClassLoader to destroy.
*/
public static void destroyClassLoader(final ClassLoader classLoader) {
logger.debug("Destroying classLoader " + toString(classLoader));
// remove from the indexes
final Set<String> apps = appsByClassLoader.remove(classLoader);
if (apps != null) {
List<ClassLoader> classLoaders;
for (final String appId : apps) {
classLoaders = classLoadersByApp.get(appId);
if (classLoaders != null) {
classLoaders.remove(classLoader);
}
//If this is the last class loader in the app, clean up the app
if (null == classLoaders || classLoaders.isEmpty()) {
destroyClassLoader(appId);
}
}
}
// Clear OpenJPA caches
cleanOpenJPACache(classLoader);
//Clear open jar files belonging to this ClassLoader
for (final String jar : getClosedJarFiles(classLoader)) {
clearSunJarFileFactoryCache(jar);
}
}
/**
* Dirty hack to force closure of file handles in the Oracle VM URLClassLoader
* Any URLClassLoader passed into this method will be unusable after the method completes.
*
* @param cl ClassLoader of expected type URLClassLoader (Silent failure)
*/
private static List<String> getClosedJarFiles(final ClassLoader cl) {
final List<String> files = new ArrayList<String>();
if (null != cl && cl instanceof URLClassLoader) {
final URLClassLoader ucl = (URLClassLoader) cl;
final Class clazz = java.net.URLClassLoader.class;
try {
final java.lang.reflect.Field ucp = clazz.getDeclaredField("ucp");
ucp.setAccessible(true);
final Object cp = ucp.get(ucl);
final java.lang.reflect.Field loaders = cp.getClass().getDeclaredField("loaders");
loaders.setAccessible(true);
final java.util.Collection c = (java.util.Collection) loaders.get(cp);
java.lang.reflect.Field loader;
java.util.jar.JarFile jf;
for (final Object jl : c.toArray()) {
try {
loader = jl.getClass().getDeclaredField("jar");
loader.setAccessible(true);
jf = (java.util.jar.JarFile) loader.get(jl);
files.add(jf.getName());
jf.close();
} catch (Throwable t) {
//If we got this far, this is probably not a JAR loader so skip it
}
}
} catch (Throwable t) {
//Not an Oracle VM
}
}
return files;
}
public boolean finalizeNativeLibs(final ClassLoader cl) {
boolean res = false;
final Class classClassLoader = ClassLoader.class;
java.lang.reflect.Field nativeLibraries = null;
try {
nativeLibraries = classClassLoader.getDeclaredField("nativeLibraries");
} catch (NoSuchFieldException e1) {
//Ignore
}
if (nativeLibraries == null) {
return res;
}
nativeLibraries.setAccessible(true);
Object obj = null;
try {
obj = nativeLibraries.get(cl);
} catch (IllegalAccessException e1) {
//Ignore
}
if (!(obj instanceof Vector)) {
return res;
}
res = true;
final Vector java_lang_ClassLoader_NativeLibrary = (Vector) obj;
java.lang.reflect.Method finalize;
for (final Object lib : java_lang_ClassLoader_NativeLibrary) {
try {
finalize = lib.getClass().getDeclaredMethod("finalize", new Class[0]);
if (finalize != null) {
finalize.setAccessible(true);
try {
finalize.invoke(lib, new Object[0]);
} catch (Throwable e) {
//Ignore
}
}
} catch (Throwable e) {
//Ignore
}
}
return res;
}
public static void destroyClassLoader(final String appId) {
logger.debug("Destroying classLoaders for application " + appId);
final List<ClassLoader> classLoaders = classLoadersByApp.remove(appId);
if (classLoaders != null) {
final Iterator<ClassLoader> it = classLoaders.iterator();
Set<String> apps;
ClassLoader cl;
while (it.hasNext()) {
cl = it.next();
apps = appsByClassLoader.get(cl);
if (null != apps) {
//This app is no longer using the class loader
apps.remove(appId);
}
//If no apps are using the class loader, destroy it
if (null == apps || apps.isEmpty()) {
it.remove();
appsByClassLoader.remove(cl);
destroyClassLoader(cl);
System.gc();
} else {
logger.debug("ClassLoader " + toString(cl) + " held open by the applications: " + apps);
}
}
}
localUrlCache.releaseUrls(appId);
clearSunJarFileFactoryCache(appId);
}
public static URLClassLoader createTempClassLoader(final ClassLoader parent) {
return new TempClassLoader(parent);
}
public static URLClassLoader createTempClassLoader(final String appId, final URL[] rawUrls, final ClassLoader parent) {
String updatedAppId = appId;
if (appId != null) { // here we often get the full path of the app as id where later it is simply the name of the file/dir
final File file = new File(appId);
if (file.exists()) {
updatedAppId = file.getName();
if (updatedAppId.endsWith(".war") || updatedAppId.endsWith(".ear")) {
updatedAppId = updatedAppId.substring(0, updatedAppId.length() - ".war".length());
}
}
}
final URL[] urls;
ClassLoaderConfigurer configurer = ClassLoaderUtil.configurer(updatedAppId);
if (configurer == null) { // try the complete path
configurer = ClassLoaderUtil.configurer(appId);
}
if (configurer != null) {
final Collection<URL> urlList = new ArrayList<URL>();
for (final URL rawUrl : rawUrls) {
if (configurer.accept(rawUrl)) {
urlList.add(rawUrl);
}
}
urlList.addAll(Arrays.asList(configurer.additionalURLs()));
urls = urlList.toArray(new URL[urlList.size()]);
} else {
urls = rawUrls;
}
return new TempClassLoader(createClassLoader(appId, urls, parent));
}
/**
* Cleans well known class loader leaks in VMs and libraries. There is a lot of bad code out there and this method
* will clear up the know problems. This method should only be called when the class loader will no longer be used.
* It this method is called two often it can have a serious impact on preformance.
*/
public static void clearClassLoaderCaches() {
clearSunSoftCache(ObjectInputStream.class, "subclassAudits");
clearSunSoftCache(ObjectOutputStream.class, "subclassAudits");
clearSunSoftCache(ObjectStreamClass.class, "localDescs");
clearSunSoftCache(ObjectStreamClass.class, "reflectors");
Introspector.flushCaches();
}
public static void clearSunJarFileFactoryCache(final String jarLocation) {
clearSunJarFileFactoryCacheImpl(jarLocation, 5);
}
/**
* Due to several different implementation changes in various JDK releases the code here is not as
* straight forward as reflecting debug items in your current runtime. There have even been breaking changes
* between 1.6 runtime builds, let alone 1.5.
* <p/>
* If you discover a new issue here please be careful to ensure the existing functionality is 'extended' and not
* just replaced to match your runtime observations.
* <p/>
* If you want to look at the mess that leads up to this then follow the source code changes made to
* the class sun.net.www.protocol.jar.JarFileFactory over several years.
*
* @param jarLocation String
* @param attempt int
*/
@SuppressWarnings({"unchecked"})
private static synchronized void clearSunJarFileFactoryCacheImpl(final String jarLocation, final int attempt) {
logger.debug("Clearing Sun JarFileFactory cache for directory " + jarLocation);
try {
final Class jarFileFactory = Class.forName("sun.net.www.protocol.jar.JarFileFactory");
//Do not generify these maps as their contents are NOT stable across runtimes.
final Field fileCacheField = jarFileFactory.getDeclaredField("fileCache");
fileCacheField.setAccessible(true);
final Map fileCache = (Map) fileCacheField.get(null);
final Map fileCacheCopy = new HashMap(fileCache);
final Field urlCacheField = jarFileFactory.getDeclaredField("urlCache");
urlCacheField.setAccessible(true);
final Map urlCache = (Map) urlCacheField.get(null);
final Map urlCacheCopy = new HashMap(urlCache);
//The only stable item we have here is the JarFile/ZipFile in this map
Iterator iterator = urlCacheCopy.entrySet().iterator();
final List urlCacheRemoveKeys = new ArrayList();
while (iterator.hasNext()) {
final Map.Entry entry = (Map.Entry) iterator.next();
final Object key = entry.getKey();
if (key instanceof ZipFile) {
final ZipFile zf = (ZipFile) key;
final File file = new File(zf.getName()); //getName returns File.getPath()
if (isParent(jarLocation, file)) {
//Flag for removal
urlCacheRemoveKeys.add(key);
}
} else {
logger.warning("Unexpected key type: " + key);
}
}
iterator = fileCacheCopy.entrySet().iterator();
final List fileCacheRemoveKeys = new ArrayList();
while (iterator.hasNext()) {
final Map.Entry entry = (Map.Entry) iterator.next();
final Object value = entry.getValue();
if (urlCacheRemoveKeys.contains(value)) {
fileCacheRemoveKeys.add(entry.getKey());
}
}
//Use these unstable values as the keys for the fileCache values.
iterator = fileCacheRemoveKeys.iterator();
while (iterator.hasNext()) {
final Object next = iterator.next();
try {
final Object remove = fileCache.remove(next);
if (null != remove) {
logger.debug("Removed item from fileCache: " + remove);
}
} catch (Throwable e) {
logger.warning("Failed to remove item from fileCache: " + next);
}
}
iterator = urlCacheRemoveKeys.iterator();
while (iterator.hasNext()) {
final Object next = iterator.next();
try {
final Object remove = urlCache.remove(next);
try {
((ZipFile) next).close();
} catch (Throwable e) {
//Ignore
}
if (null != remove) {
logger.debug("Removed item from urlCache: " + remove);
}
} catch (Throwable e) {
logger.warning("Failed to remove item from urlCache: " + next);
}
}
} catch (ConcurrentModificationException e) {
if (attempt > 0) {
clearSunJarFileFactoryCacheImpl(jarLocation, (attempt - 1));
} else {
logger.error("Unable to clear Sun JarFileFactory cache after 5 attempts", e);
}
} catch (ClassNotFoundException e) {
// not a sun vm
} catch (NoSuchFieldException e) {
// different version of sun vm?
} catch (Throwable e) {
logger.error("Unable to clear Sun JarFileFactory cache", e);
}
}
private static boolean isParent(final String jarLocation, File file) {
final File dir = new File(jarLocation);
while (file != null) {
if (file.equals(dir)) {
return true;
}
file = file.getParentFile();
}
return false;
}
/**
* Clears the caches maintained by the SunVM object stream implementation.
* This method uses reflection and setAccessable to obtain access to the Sun cache.
* The cache Class synchronizes upon itself for access to the cache Map.
* This method completely clears the class loader cache which will impact preformance of object serialization.
*
* @param clazz the name of the class containing the cache field
* @param fieldName the name of the cache field
*/
public static void clearSunSoftCache(final Class clazz, final String fieldName) {
synchronized (clazz) {
try {
final Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
final Map cache = (Map) field.get(null);
cache.clear();
} catch (Throwable ignored) {
// there is nothing a user could do about this anyway
}
}
}
public static void cleanOpenJPACache(final ClassLoader classLoader) {
try {
final Class<?> pcRegistryClass = ClassLoaderUtil.class.getClassLoader().loadClass("org.apache.openjpa.enhance.PCRegistry");
final Method deRegisterMethod = pcRegistryClass.getMethod("deRegister", ClassLoader.class);
deRegisterMethod.invoke(null, classLoader);
} catch (Throwable ignored) {
// there is nothing a user could do about this anyway
}
}
private static String toString(final ClassLoader classLoader) {
if (classLoader == null) {
return "null";
} else {
return classLoader.getClass().getSimpleName() + "@" + System.identityHashCode(classLoader);
}
}
public static String resourceName(final String s) {
return s.replace(".", "/") + ".class";
}
public static ClassLoaderConfigurer configurer(final String rawId) {
String id = rawId;
if (id != null && (id.startsWith("/") || id.startsWith("\\")) && !new File(id).exists() && id.length() > 1) {
id = id.substring(1);
}
// TODO: see how to manage tomee/openejb prefix
String key = "tomee.classloader.configurer." + id + ".clazz";
String impl = SystemInstance.get().getProperty(key);
if (impl == null) {
key = "tomee.classloader.configurer.clazz";
impl = SystemInstance.get().getProperty(key);
if (impl == null) {
key = "openejb.classloader.configurer." + id + ".clazz";
impl = SystemInstance.get().getProperty(key);
if (impl == null) {
key = "openejb.classloader.configurer.clazz";
impl = SystemInstance.get().getProperty(key);
}
}
}
if (impl != null) {
key = key.substring(0, key.length() - "clazz".length());
boolean list = false;
try {
ClassLoaderUtil.class.getClassLoader().loadClass(impl);
} catch (ClassNotFoundException e) {
list = true;
}
if (!list) {
return createConfigurer(key, impl);
} else {
final String[] names = impl.split(",");
final ClassLoaderConfigurer[] configurers = new ClassLoaderConfigurer[names.length];
for (int i = 0; i < names.length; i++) {
configurers[i] = createConfigurer(names[i], SystemInstance.get().getProperty(names[i] + ".clazz"));
}
return new CompositeClassLoaderConfigurer(configurers);
}
}
return null;
}
private static ClassLoaderConfigurer createConfigurer(final String key, final String impl) {
try {
final ObjectRecipe recipe = new ObjectRecipe(impl);
for (final Map.Entry<Object, Object> entry : SystemInstance.get().getProperties().entrySet()) {
final String entryKey = entry.getKey().toString();
if (entryKey.startsWith(key)) {
final String newKey = entryKey.substring(key.length());
if (!"clazz".equals(newKey)) {
recipe.setProperty(newKey, entry.getValue());
}
}
}
final Object instance = recipe.create();
if (instance instanceof ClassLoaderConfigurer) {
return (ClassLoaderConfigurer) instance;
} else {
logger.error(impl + " is not a classlaoder configurer, using default behavior");
}
} catch (Exception e) {
logger.error("Can't create classloader configurer " + impl + ", using default behavior");
}
return null;
}
}