| /* |
| * 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.catalina.loader; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.instrument.ClassFileTransformer; |
| import java.lang.instrument.IllegalClassFormatException; |
| import java.lang.ref.Reference; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.InaccessibleObjectException; |
| import java.lang.reflect.Method; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.security.CodeSource; |
| import java.security.Permission; |
| import java.security.PermissionCollection; |
| import java.security.ProtectionDomain; |
| import java.security.cert.Certificate; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.ConcurrentModificationException; |
| import java.util.Date; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.NoSuchElementException; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| import java.util.jar.Attributes; |
| import java.util.jar.Attributes.Name; |
| import java.util.jar.Manifest; |
| |
| import org.apache.catalina.Container; |
| import org.apache.catalina.Lifecycle; |
| import org.apache.catalina.LifecycleException; |
| import org.apache.catalina.LifecycleListener; |
| import org.apache.catalina.LifecycleState; |
| import org.apache.catalina.WebResource; |
| import org.apache.catalina.WebResourceRoot; |
| import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; |
| import org.apache.juli.WebappProperties; |
| import org.apache.juli.logging.Log; |
| import org.apache.juli.logging.LogFactory; |
| import org.apache.tomcat.InstrumentableClassLoader; |
| import org.apache.tomcat.util.ExceptionUtils; |
| import org.apache.tomcat.util.IntrospectionUtils; |
| import org.apache.tomcat.util.buf.ToStringUtil; |
| import org.apache.tomcat.util.collections.ConcurrentLruCache; |
| import org.apache.tomcat.util.compat.JreCompat; |
| import org.apache.tomcat.util.res.StringManager; |
| import org.apache.tomcat.util.threads.ThreadPoolExecutor; |
| |
| /** |
| * Specialized web application class loader. |
| * <p> |
| * This class loader is a full reimplementation of the <code>URLClassLoader</code> from the JDK. It is designed to be |
| * fully compatible with a normal <code>URLClassLoader</code>, although its internal behavior may be completely |
| * different. |
| * <p> |
| * <strong>IMPLEMENTATION NOTE</strong> - By default, this class loader follows the delegation model required by the |
| * specification. The bootstrap class loader will be queried first, then the local repositories, and only then |
| * delegation to the parent class loader will occur. This allows the web application to override any shared class except |
| * the classes from J2SE. Special handling is provided from the JAXP XML parser interfaces, the JNDI interfaces, and the |
| * classes from the servlet API, which are never loaded from the webapp repositories. The <code>delegate</code> property |
| * allows an application to modify this behavior to move the parent class loader ahead of the local repositories. |
| * <p> |
| * <strong>IMPLEMENTATION NOTE</strong> - Due to limitations in Jasper compilation technology, any repository which |
| * contains classes from the servlet API will be ignored by the class loader. |
| * <p> |
| * <strong>IMPLEMENTATION NOTE</strong> - The class loader generates source URLs which include the full JAR URL when a |
| * class is loaded from a JAR file, which allows setting security permission at the class level, even when a class is |
| * contained inside a JAR. |
| * <p> |
| * <strong>IMPLEMENTATION NOTE</strong> - Local repositories are searched in the order they are added via the initial |
| * constructor. |
| * <p> |
| * <strong>IMPLEMENTATION NOTE</strong> - As of 8.0, this class loader implements {@link InstrumentableClassLoader}, |
| * permitting web application classes to instrument other classes in the same web application. It does not permit |
| * instrumentation of system or container classes or classes in other web apps. |
| * |
| * @author Remy Maucherat |
| */ |
| public abstract class WebappClassLoaderBase extends URLClassLoader |
| implements Lifecycle, InstrumentableClassLoader, WebappProperties { |
| |
| private static final Log log = LogFactory.getLog(WebappClassLoaderBase.class); |
| |
| /** |
| * List of ThreadGroup names to ignore when scanning for web application started threads that need to be shut down. |
| */ |
| private static final List<String> JVM_THREAD_GROUP_NAMES = new ArrayList<>(); |
| |
| private static final String JVM_THREAD_GROUP_SYSTEM = "system"; |
| |
| private static final String CLASS_FILE_SUFFIX = ".class"; |
| |
| static { |
| if (!JreCompat.isGraalAvailable()) { |
| registerAsParallelCapable(); |
| } |
| JVM_THREAD_GROUP_NAMES.add(JVM_THREAD_GROUP_SYSTEM); |
| JVM_THREAD_GROUP_NAMES.add("RMI Runtime"); |
| } |
| |
| |
| // ------------------------------------------------------- Static Variables |
| |
| /** |
| * The string manager for this package. |
| */ |
| protected static final StringManager sm = StringManager.getManager(WebappClassLoaderBase.class); |
| |
| |
| // ----------------------------------------------------------- Constructors |
| |
| /** |
| * Construct a new ClassLoader with no defined repositories and no parent ClassLoader. |
| */ |
| protected WebappClassLoaderBase() { |
| |
| super(new URL[0]); |
| |
| ClassLoader p = getParent(); |
| if (p == null) { |
| p = getSystemClassLoader(); |
| } |
| this.parent = p; |
| |
| ClassLoader j = String.class.getClassLoader(); |
| if (j == null) { |
| j = getSystemClassLoader(); |
| while (j.getParent() != null) { |
| j = j.getParent(); |
| } |
| } |
| this.javaseClassLoader = j; |
| } |
| |
| |
| /** |
| * Construct a new ClassLoader with no defined repositories and the given parent ClassLoader. |
| * <p> |
| * Method is used via reflection - see {@link WebappLoader#createClassLoader()} |
| * |
| * @param parent Our parent class loader |
| */ |
| protected WebappClassLoaderBase(ClassLoader parent) { |
| |
| super(new URL[0], parent); |
| |
| ClassLoader p = getParent(); |
| if (p == null) { |
| p = getSystemClassLoader(); |
| } |
| this.parent = p; |
| |
| ClassLoader j = String.class.getClassLoader(); |
| if (j == null) { |
| j = getSystemClassLoader(); |
| while (j.getParent() != null) { |
| j = j.getParent(); |
| } |
| } |
| this.javaseClassLoader = j; |
| } |
| |
| |
| // ----------------------------------------------------- Instance Variables |
| |
| /** |
| * Associated web resources for this webapp. |
| */ |
| protected WebResourceRoot resources = null; |
| |
| |
| /** |
| * The cache of ResourceEntry for classes and resources we have loaded, keyed by resource path, not binary name. |
| * Path is used as the key since resources may be requested by binary name (classes) or path (other resources such |
| * as property files) and the mapping from binary name to path is unambiguous but the reverse mapping is ambiguous. |
| */ |
| protected final Map<String,ResourceEntry> resourceEntries = new ConcurrentHashMap<>(); |
| |
| |
| /** |
| * Should this class loader delegate to the parent class loader <strong>before</strong> searching its own |
| * repositories (i.e. the usual Java2 delegation model)? If set to <code>false</code>, this class loader will search |
| * its own repositories first, and delegate to the parent only if the class or resource is not found locally. Note |
| * that the default, <code>false</code>, is the behavior called for by the servlet specification. |
| */ |
| protected boolean delegate = false; |
| |
| |
| private final Map<String,Long> jarModificationTimes = new HashMap<>(); |
| |
| |
| /** |
| * A list of read File Permission's required if this loader is for a web application context. |
| */ |
| protected final ArrayList<Permission> permissionList = new ArrayList<>(); |
| |
| |
| /** |
| * The PermissionCollection for each CodeSource for a web application context. |
| */ |
| protected final HashMap<String,PermissionCollection> loaderPC = new HashMap<>(); |
| |
| |
| /** |
| * The parent class loader. |
| */ |
| protected final ClassLoader parent; |
| |
| |
| /** |
| * The bootstrap class loader used to load the JavaSE classes. In some implementations this class loader is always |
| * <code>null</code> and in those cases {@link ClassLoader#getParent()} will be called recursively on the system |
| * class loader and the last non-null result used. |
| */ |
| private ClassLoader javaseClassLoader; |
| |
| |
| /** |
| * Enables the RMI Target memory leak detection to be controlled. This is necessary since the detection can only |
| * work if some of the modularity checks are disabled. |
| */ |
| private boolean clearReferencesRmiTargets = true; |
| |
| /** |
| * Should Tomcat attempt to terminate threads that have been started by the web application? Stopping threads is |
| * performed via the deprecated (for good reason) <code>Thread.stop()</code> method and is likely to result in |
| * instability. As such, enabling this should be viewed as an option of last resort in a development environment and |
| * is not recommended in a production environment. If not specified, the default value of <code>false</code> will be |
| * used. |
| */ |
| private boolean clearReferencesStopThreads = false; |
| |
| /** |
| * Should Tomcat attempt to terminate any {@link java.util.TimerThread}s that have been started by the web |
| * application? If not specified, the default value of <code>false</code> will be used. |
| */ |
| private boolean clearReferencesStopTimerThreads = false; |
| |
| /** |
| * Should Tomcat call {@link org.apache.juli.logging.LogFactory#release(ClassLoader)} when the class loader is |
| * stopped? If not specified, the default value of <code>true</code> is used. Changing the default setting is likely |
| * to lead to memory leaks and other issues. |
| */ |
| private boolean clearReferencesLogFactoryRelease = true; |
| |
| /** |
| * If an HttpClient keep-alive timer thread has been started by this web application and is still running, should |
| * Tomcat change the context class loader from the current {@link ClassLoader} to {@link ClassLoader#getParent()} to |
| * prevent a memory leak? Note that the keep-alive timer thread will stop on its own once the keep-alives all expire |
| * however, on a busy system that might not happen for some time. |
| */ |
| private boolean clearReferencesHttpClientKeepAliveThread = true; |
| |
| /** |
| * Should Tomcat attempt to clear references to classes loaded by this class loader from ThreadLocals? |
| */ |
| private boolean clearReferencesThreadLocals = true; |
| |
| /** |
| * Should Tomcat skip the memory leak checks when the web application is stopped as part of the process of shutting |
| * down the JVM? |
| */ |
| private boolean skipMemoryLeakChecksOnJvmShutdown = false; |
| |
| /** |
| * Holds the class file transformers decorating this class loader. The CopyOnWriteArrayList is thread safe. It is |
| * expensive on writes, but those should be rare. It is very fast on reads, since synchronization is not actually |
| * used. Importantly, the ClassLoader will never block iterating over the transformers while loading a class. |
| */ |
| private final List<ClassFileTransformer> transformers = new CopyOnWriteArrayList<>(); |
| |
| |
| /** |
| * Flag that indicates that {@link #addURL(URL)} has been called which creates a requirement to check the super |
| * class when searching for resources. |
| */ |
| private boolean hasExternalRepositories = false; |
| |
| |
| /** |
| * Repositories managed by this class rather than the super class. |
| */ |
| private final List<URL> localRepositories = new ArrayList<>(); |
| |
| |
| private volatile LifecycleState state = LifecycleState.NEW; |
| |
| /* |
| * Class resources are not cached since they are loaded on first use and the resource is then no longer required. It |
| * does help, however, to cache classes that are not found as in some scenarios the same class will be searched for |
| * many times and the greater the number of JARs/classes, the longer that lookup will take. |
| */ |
| private final ConcurrentLruCache<String> notFoundClassResources = new ConcurrentLruCache<>(1000); |
| |
| |
| // ------------------------------------------------------------- Properties |
| |
| public void setNotFoundClassResourceCacheSize(int notFoundClassResourceCacheSize) { |
| notFoundClassResources.setLimit(notFoundClassResourceCacheSize); |
| } |
| |
| |
| public int getNotFoundClassResourceCacheSize() { |
| return notFoundClassResources.getLimit(); |
| } |
| |
| |
| /** |
| * Set associated resources. |
| * |
| * @param resources the resources from which the classloader will load the classes |
| */ |
| public void setResources(WebResourceRoot resources) { |
| this.resources = resources; |
| } |
| |
| |
| /** |
| * @return the context name for this class loader. |
| */ |
| public String getContextName() { |
| if (resources == null) { |
| return "Unknown"; |
| } else { |
| return resources.getContext().getBaseName(); |
| } |
| } |
| |
| |
| /** |
| * Return the "delegate first" flag for this class loader. |
| * |
| * @return <code>true</code> if the class lookup will delegate to the parent first. The default in Tomcat is |
| * <code>false</code>. |
| */ |
| public boolean getDelegate() { |
| return this.delegate; |
| } |
| |
| |
| /** |
| * Set the "delegate first" flag for this class loader. If this flag is true, this class loader delegates to the |
| * parent class loader <strong>before</strong> searching its own repositories, as in an ordinary (non-servlet) chain |
| * of Java class loaders. If set to <code>false</code> (the default), this class loader will search its own |
| * repositories first, and delegate to the parent only if the class or resource is not found locally, as per the |
| * servlet specification. |
| * |
| * @param delegate The new "delegate first" flag |
| */ |
| public void setDelegate(boolean delegate) { |
| this.delegate = delegate; |
| } |
| |
| |
| public boolean getClearReferencesRmiTargets() { |
| return this.clearReferencesRmiTargets; |
| } |
| |
| |
| public void setClearReferencesRmiTargets(boolean clearReferencesRmiTargets) { |
| this.clearReferencesRmiTargets = clearReferencesRmiTargets; |
| } |
| |
| |
| /** |
| * @return the clearReferencesStopThreads flag for this Context. |
| */ |
| public boolean getClearReferencesStopThreads() { |
| return this.clearReferencesStopThreads; |
| } |
| |
| |
| /** |
| * Set the clearReferencesStopThreads feature for this Context. |
| * |
| * @param clearReferencesStopThreads The new flag value |
| */ |
| public void setClearReferencesStopThreads(boolean clearReferencesStopThreads) { |
| this.clearReferencesStopThreads = clearReferencesStopThreads; |
| } |
| |
| |
| /** |
| * @return the clearReferencesStopTimerThreads flag for this Context. |
| */ |
| public boolean getClearReferencesStopTimerThreads() { |
| return this.clearReferencesStopTimerThreads; |
| } |
| |
| |
| /** |
| * Set the clearReferencesStopTimerThreads feature for this Context. |
| * |
| * @param clearReferencesStopTimerThreads The new flag value |
| */ |
| public void setClearReferencesStopTimerThreads(boolean clearReferencesStopTimerThreads) { |
| this.clearReferencesStopTimerThreads = clearReferencesStopTimerThreads; |
| } |
| |
| |
| /** |
| * @return the clearReferencesLogFactoryRelease flag for this Context. |
| */ |
| public boolean getClearReferencesLogFactoryRelease() { |
| return this.clearReferencesLogFactoryRelease; |
| } |
| |
| |
| /** |
| * Set the clearReferencesLogFactoryRelease feature for this Context. |
| * |
| * @param clearReferencesLogFactoryRelease The new flag value |
| */ |
| public void setClearReferencesLogFactoryRelease(boolean clearReferencesLogFactoryRelease) { |
| this.clearReferencesLogFactoryRelease = clearReferencesLogFactoryRelease; |
| } |
| |
| |
| /** |
| * @return the clearReferencesHttpClientKeepAliveThread flag for this Context. |
| */ |
| public boolean getClearReferencesHttpClientKeepAliveThread() { |
| return this.clearReferencesHttpClientKeepAliveThread; |
| } |
| |
| |
| /** |
| * Set the clearReferencesHttpClientKeepAliveThread feature for this Context. |
| * |
| * @param clearReferencesHttpClientKeepAliveThread The new flag value |
| */ |
| public void setClearReferencesHttpClientKeepAliveThread(boolean clearReferencesHttpClientKeepAliveThread) { |
| this.clearReferencesHttpClientKeepAliveThread = clearReferencesHttpClientKeepAliveThread; |
| } |
| |
| |
| public boolean getClearReferencesThreadLocals() { |
| return clearReferencesThreadLocals; |
| } |
| |
| |
| public void setClearReferencesThreadLocals(boolean clearReferencesThreadLocals) { |
| this.clearReferencesThreadLocals = clearReferencesThreadLocals; |
| } |
| |
| |
| public boolean getSkipMemoryLeakChecksOnJvmShutdown() { |
| return skipMemoryLeakChecksOnJvmShutdown; |
| } |
| |
| |
| public void setSkipMemoryLeakChecksOnJvmShutdown(boolean skipMemoryLeakChecksOnJvmShutdown) { |
| this.skipMemoryLeakChecksOnJvmShutdown = skipMemoryLeakChecksOnJvmShutdown; |
| } |
| |
| |
| // ------------------------------------------------------- Reloader Methods |
| |
| @Override |
| public void addTransformer(ClassFileTransformer transformer) { |
| |
| if (transformer == null) { |
| throw new IllegalArgumentException( |
| sm.getString("webappClassLoader.addTransformer.illegalArgument", getContextName())); |
| } |
| |
| if (this.transformers.contains(transformer)) { |
| // if the same instance of this transformer was already added, bail out |
| log.warn(sm.getString("webappClassLoader.addTransformer.duplicate", transformer, getContextName())); |
| return; |
| } |
| this.transformers.add(transformer); |
| |
| log.info(sm.getString("webappClassLoader.addTransformer", transformer, getContextName())); |
| } |
| |
| @Override |
| public void removeTransformer(ClassFileTransformer transformer) { |
| |
| if (transformer == null) { |
| return; |
| } |
| |
| if (this.transformers.remove(transformer)) { |
| log.info(sm.getString("webappClassLoader.removeTransformer", transformer, getContextName())); |
| } |
| } |
| |
| protected void copyStateWithoutTransformers(WebappClassLoaderBase base) { |
| base.resources = this.resources; |
| base.delegate = this.delegate; |
| base.state = LifecycleState.NEW; |
| base.clearReferencesStopThreads = this.clearReferencesStopThreads; |
| base.clearReferencesStopTimerThreads = this.clearReferencesStopTimerThreads; |
| base.clearReferencesLogFactoryRelease = this.clearReferencesLogFactoryRelease; |
| base.clearReferencesHttpClientKeepAliveThread = this.clearReferencesHttpClientKeepAliveThread; |
| base.jarModificationTimes.putAll(this.jarModificationTimes); |
| base.permissionList.addAll(this.permissionList); |
| base.loaderPC.putAll(this.loaderPC); |
| } |
| |
| /** |
| * Have one or more classes or resources been modified so that a reload is appropriate? |
| * |
| * @return <code>true</code> if there's been a modification |
| */ |
| public boolean modified() { |
| |
| if (log.isTraceEnabled()) { |
| log.trace("modified()"); |
| } |
| |
| for (Entry<String,ResourceEntry> entry : resourceEntries.entrySet()) { |
| long cachedLastModified = entry.getValue().lastModified; |
| long lastModified = resources.getClassLoaderResource(entry.getKey()).getLastModified(); |
| if (lastModified != cachedLastModified) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("webappClassLoader.resourceModified", entry.getKey(), |
| new Date(cachedLastModified), new Date(lastModified))); |
| } |
| return true; |
| } |
| } |
| |
| // Check if JARs have been added or removed |
| WebResource[] jars = resources.listResources("/WEB-INF/lib"); |
| // Filter out non-JAR resources |
| |
| int jarCount = 0; |
| for (WebResource jar : jars) { |
| if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) { |
| jarCount++; |
| Long recordedLastModified = jarModificationTimes.get(jar.getName()); |
| if (recordedLastModified == null) { |
| // Jar has been added |
| log.info(sm.getString("webappClassLoader.jarsAdded", resources.getContext().getName())); |
| return true; |
| } |
| if (recordedLastModified.longValue() != jar.getLastModified()) { |
| // Jar has been changed |
| log.info(sm.getString("webappClassLoader.jarsModified", resources.getContext().getName())); |
| return true; |
| } |
| } |
| } |
| |
| if (jarCount < jarModificationTimes.size()) { |
| log.info(sm.getString("webappClassLoader.jarsRemoved", resources.getContext().getName())); |
| return true; |
| } |
| |
| |
| // No classes have been modified |
| return false; |
| } |
| |
| |
| @Override |
| public String toString() { |
| |
| StringBuilder sb = new StringBuilder(this.getClass().getSimpleName()); |
| sb.append("\r\n context: "); |
| sb.append(getContextName()); |
| sb.append("\r\n delegate: "); |
| sb.append(delegate); |
| sb.append("\r\n"); |
| if (this.parent != null) { |
| sb.append("----------> Parent Classloader:\r\n"); |
| sb.append(this.parent.toString()); |
| sb.append("\r\n"); |
| } |
| if (!this.transformers.isEmpty()) { |
| sb.append("----------> Class file transformers:\r\n"); |
| for (ClassFileTransformer transformer : this.transformers) { |
| sb.append(transformer).append("\r\n"); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| |
| // ---------------------------------------------------- ClassLoader Methods |
| |
| |
| // Note: exposed for use by tests |
| protected final Class<?> doDefineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) { |
| return super.defineClass(name, b, off, len, protectionDomain); |
| } |
| |
| /** |
| * Find the specified class in our local repositories, if possible. If not found, throw |
| * <code>ClassNotFoundException</code>. |
| * |
| * @param name The binary name of the class to be loaded |
| * |
| * @exception ClassNotFoundException if the class was not found |
| */ |
| @Override |
| public Class<?> findClass(String name) throws ClassNotFoundException { |
| |
| if (log.isTraceEnabled()) { |
| log.trace(" findClass(" + name + ")"); |
| } |
| |
| checkStateForClassLoading(name); |
| |
| if (name == null) { |
| throw new ClassNotFoundException("null"); |
| } |
| |
| String path = binaryNameToPath(name, true); |
| |
| // Ask our superclass to locate this class, if possible |
| // (throws ClassNotFoundException if it is not found) |
| Class<?> clazz = null; |
| try { |
| if (log.isTraceEnabled()) { |
| log.trace(" findClassInternal(" + name + ")"); |
| } |
| if (!notFoundClassResources.contains(path)) { |
| try { |
| clazz = findClassInternal(name, path); |
| } catch (RuntimeException e) { |
| if (log.isTraceEnabled()) { |
| log.trace(" -->RuntimeException Rethrown", e); |
| } |
| throw e; |
| } |
| if (clazz == null && hasExternalRepositories) { |
| try { |
| clazz = super.findClass(name); |
| } catch (RuntimeException e) { |
| if (log.isTraceEnabled()) { |
| log.trace(" -->RuntimeException Rethrown", e); |
| } |
| throw e; |
| } |
| } |
| } |
| } catch (ClassNotFoundException e) { |
| if (log.isTraceEnabled()) { |
| log.trace(" --> Passing on ClassNotFoundException"); |
| } |
| notFoundClassResources.add(path); |
| throw e; |
| } |
| if (clazz == null) { |
| if (log.isTraceEnabled()) { |
| log.trace(" --> Returning ClassNotFoundException"); |
| } |
| notFoundClassResources.add(path); |
| throw new ClassNotFoundException(name); |
| } |
| |
| // Return the class we have located |
| if (log.isTraceEnabled()) { |
| log.trace(" Returning class " + clazz); |
| } |
| |
| if (log.isTraceEnabled()) { |
| ClassLoader cl = clazz.getClassLoader(); |
| log.trace(" Loaded by " + cl.toString()); |
| } |
| return clazz; |
| |
| } |
| |
| |
| /** |
| * Find the specified resource in our local repository, and return a <code>URL</code> referring to it, or |
| * <code>null</code> if this resource cannot be found. |
| * |
| * @param name Name of the resource to be found |
| */ |
| @Override |
| public URL findResource(final String name) { |
| |
| if (log.isTraceEnabled()) { |
| log.trace(" findResource(" + name + ")"); |
| } |
| |
| checkStateForResourceLoading(name); |
| |
| URL url = null; |
| |
| if (name == null || name.startsWith("/")) { |
| return null; |
| } |
| |
| String path = nameToPath(name); |
| |
| if (!notFoundClassResources.contains(path)) { |
| WebResource resource = resources.getClassLoaderResource(path); |
| if (resource.exists()) { |
| url = resource.getURL(); |
| trackLastModified(path, resource); |
| } |
| |
| if (url == null && hasExternalRepositories) { |
| url = super.findResource(name); |
| } |
| |
| if (url == null) { |
| notFoundClassResources.add(path); |
| } |
| } |
| |
| if (log.isTraceEnabled()) { |
| if (url != null) { |
| log.trace(" --> Returning '" + url.toString() + "'"); |
| } else { |
| log.trace(" --> Resource not found, returning null"); |
| } |
| } |
| return url; |
| } |
| |
| |
| private void trackLastModified(String path, WebResource resource) { |
| if (resourceEntries.containsKey(path)) { |
| return; |
| } |
| ResourceEntry entry = new ResourceEntry(); |
| entry.lastModified = resource.getLastModified(); |
| synchronized (resourceEntries) { |
| resourceEntries.putIfAbsent(path, entry); |
| } |
| } |
| |
| |
| @Override |
| public Enumeration<URL> findResources(String name) throws IOException { |
| |
| if (log.isTraceEnabled()) { |
| log.trace(" findResources(" + name + ")"); |
| } |
| |
| checkStateForResourceLoading(name); |
| |
| LinkedHashSet<URL> result = new LinkedHashSet<>(); |
| |
| if (name == null || name.startsWith("/")) { |
| return null; |
| } |
| |
| String path = nameToPath(name); |
| |
| WebResource[] webResources = resources.getClassLoaderResources(path); |
| for (WebResource webResource : webResources) { |
| if (webResource.exists()) { |
| result.add(webResource.getURL()); |
| } |
| } |
| |
| // Adding the results of a call to the superclass |
| if (hasExternalRepositories) { |
| Enumeration<URL> otherResourcePaths = super.findResources(name); |
| while (otherResourcePaths.hasMoreElements()) { |
| result.add(otherResourcePaths.nextElement()); |
| } |
| } |
| |
| return Collections.enumeration(result); |
| } |
| |
| |
| /** |
| * Find the resource with the given name. A resource is some data (images, audio, text, etc.) that can be accessed |
| * by class code in a way that is independent of the location of the code. The name of a resource is a "/"-separated |
| * path name that identifies the resource. If the resource cannot be found, return <code>null</code>. |
| * <p> |
| * This method searches according to the following algorithm, returning as soon as it finds the appropriate URL. If |
| * the resource cannot be found, returns <code>null</code>. |
| * <ul> |
| * <li>If the <code>delegate</code> property is set to <code>true</code>, call the <code>getResource()</code> method |
| * of the parent class loader, if any.</li> |
| * <li>Call <code>findResource()</code> to find this resource in our locally defined repositories.</li> |
| * <li>Call the <code>getResource()</code> method of the parent class loader, if any.</li> |
| * </ul> |
| * |
| * @param name Name of the resource to return a URL for |
| */ |
| @Override |
| public URL getResource(String name) { |
| |
| if (log.isTraceEnabled()) { |
| log.trace("getResource(" + name + ")"); |
| } |
| |
| checkStateForResourceLoading(name); |
| |
| URL url; |
| |
| boolean delegateFirst = delegate || filter(name, false); |
| |
| // (1) Delegate to parent if requested |
| if (delegateFirst) { |
| if (log.isTraceEnabled()) { |
| log.trace(" Delegating to parent classloader " + parent); |
| } |
| url = parent.getResource(name); |
| if (url != null) { |
| if (log.isTraceEnabled()) { |
| log.trace(" --> Returning '" + url.toString() + "'"); |
| } |
| return url; |
| } |
| } |
| |
| // (2) Search local repositories |
| url = findResource(name); |
| if (url != null) { |
| if (log.isTraceEnabled()) { |
| log.trace(" --> Returning '" + url.toString() + "'"); |
| } |
| return url; |
| } |
| |
| // (3) Delegate to parent unconditionally if not already attempted |
| if (!delegateFirst) { |
| url = parent.getResource(name); |
| if (url != null) { |
| if (log.isTraceEnabled()) { |
| log.trace(" --> Returning '" + url.toString() + "'"); |
| } |
| return url; |
| } |
| } |
| |
| // (4) Resource was not found |
| if (log.isTraceEnabled()) { |
| log.trace(" --> Resource not found, returning null"); |
| } |
| return null; |
| |
| } |
| |
| |
| @Override |
| public Enumeration<URL> getResources(String name) throws IOException { |
| |
| Enumeration<URL> parentResources = parent.getResources(name); |
| Enumeration<URL> localResources = findResources(name); |
| |
| // Need to combine these enumerations. The order in which the |
| // Enumerations are combined depends on how delegation is configured |
| boolean delegateFirst = delegate || filter(name, false); |
| |
| if (delegateFirst) { |
| return new CombinedEnumeration(parentResources, localResources); |
| } else { |
| return new CombinedEnumeration(localResources, parentResources); |
| } |
| } |
| |
| |
| /** |
| * Find the resource with the given name, and return an input stream that can be used for reading it. The search |
| * order is as described for <code>getResource()</code>, after checking to see if the resource data has been |
| * previously cached. If the resource cannot be found, return <code>null</code>. |
| * |
| * @param name Name of the resource to return an input stream for |
| */ |
| @Override |
| public InputStream getResourceAsStream(String name) { |
| |
| if (log.isTraceEnabled()) { |
| log.trace("getResourceAsStream(" + name + ")"); |
| } |
| |
| checkStateForResourceLoading(name); |
| |
| InputStream stream = null; |
| |
| boolean delegateFirst = delegate || filter(name, false); |
| |
| // (1) Delegate to parent if requested |
| if (delegateFirst) { |
| if (log.isTraceEnabled()) { |
| log.trace(" Delegating to parent classloader " + parent); |
| } |
| stream = parent.getResourceAsStream(name); |
| if (stream != null) { |
| if (log.isTraceEnabled()) { |
| log.trace(" --> Returning stream from parent"); |
| } |
| return stream; |
| } |
| } |
| |
| // (2) Search local repositories |
| if (log.isTraceEnabled()) { |
| log.trace(" Searching local repositories"); |
| } |
| if (name.startsWith("/")) { |
| return null; |
| } |
| String path = nameToPath(name); |
| if (!notFoundClassResources.contains(path)) { |
| WebResource resource = resources.getClassLoaderResource(path); |
| if (resource.exists()) { |
| stream = resource.getInputStream(); |
| // Filter out .class resources through the ClassFileTranformer |
| if (name.endsWith(CLASS_FILE_SUFFIX) && !transformers.isEmpty()) { |
| // If the resource is a class, decorate it with any attached transformers |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| byte[] buf = new byte[8192]; |
| int numRead; |
| try { |
| while ((numRead = stream.read(buf)) >= 0) { |
| baos.write(buf, 0, numRead); |
| } |
| } catch (IOException ioe) { |
| log.error(sm.getString("webappClassLoader.transformError", name), ioe); |
| return null; |
| } finally { |
| try { |
| stream.close(); |
| } catch (IOException ignore) { |
| // Ignore |
| } |
| } |
| byte[] binaryContent = baos.toByteArray(); |
| String internalName = path.substring(1, path.length() - CLASS_FILE_SUFFIX.length()); |
| for (ClassFileTransformer transformer : this.transformers) { |
| try { |
| byte[] transformed = transformer.transform(this, internalName, null, null, binaryContent); |
| if (transformed != null) { |
| binaryContent = transformed; |
| } |
| } catch (IllegalClassFormatException e) { |
| log.error(sm.getString("webappClassLoader.transformError", name), e); |
| return null; |
| } |
| } |
| stream = new ByteArrayInputStream(binaryContent); |
| } |
| trackLastModified(path, resource); |
| } |
| try { |
| if (hasExternalRepositories && stream == null) { |
| URL url = super.findResource(name); |
| if (url != null) { |
| stream = url.openStream(); |
| } |
| } |
| } catch (IOException ioe) { |
| // Ignore |
| } |
| if (stream != null) { |
| if (log.isTraceEnabled()) { |
| log.trace(" --> Returning stream from local"); |
| } |
| return stream; |
| } |
| |
| notFoundClassResources.add(path); |
| } |
| |
| // (3) Delegate to parent unconditionally |
| if (!delegateFirst) { |
| if (log.isTraceEnabled()) { |
| log.trace(" Delegating to parent classloader unconditionally " + parent); |
| } |
| stream = parent.getResourceAsStream(name); |
| if (stream != null) { |
| if (log.isTraceEnabled()) { |
| log.trace(" --> Returning stream from parent"); |
| } |
| return stream; |
| } |
| } |
| |
| // (4) Resource was not found |
| if (log.isTraceEnabled()) { |
| log.trace(" --> Resource not found, returning null"); |
| } |
| return null; |
| } |
| |
| |
| /** |
| * Load the class with the specified name. This method searches for classes in the same manner as |
| * <code>loadClass(String, boolean)</code> with <code>false</code> as the second argument. |
| * |
| * @param name The binary name of the class to be loaded |
| * |
| * @exception ClassNotFoundException if the class was not found |
| */ |
| @Override |
| public Class<?> loadClass(String name) throws ClassNotFoundException { |
| return loadClass(name, false); |
| } |
| |
| |
| /** |
| * Load the class with the specified name, searching using the following algorithm until it finds and returns the |
| * class. If the class cannot be found, returns <code>ClassNotFoundException</code>. |
| * <ul> |
| * <li>Call <code>findLoadedClass(String)</code> to check if the class has already been loaded. If it has, the same |
| * <code>Class</code> object is returned.</li> |
| * <li>If the <code>delegate</code> property is set to <code>true</code>, call the <code>loadClass()</code> method |
| * of the parent class loader, if any.</li> |
| * <li>Call <code>findClass()</code> to find this class in our locally defined repositories.</li> |
| * <li>Call the <code>loadClass()</code> method of our parent class loader, if any.</li> |
| * </ul> |
| * If the class was found using the above steps, and the <code>resolve</code> flag is <code>true</code>, this method |
| * will then call <code>resolveClass(Class)</code> on the resulting Class object. |
| * |
| * @param name The binary name of the class to be loaded |
| * @param resolve If <code>true</code> then resolve the class |
| * |
| * @exception ClassNotFoundException if the class was not found |
| */ |
| @Override |
| public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { |
| |
| synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) { |
| if (log.isTraceEnabled()) { |
| log.trace("loadClass(" + name + ", " + resolve + ")"); |
| } |
| Class<?> clazz; |
| |
| // Log access to stopped class loader |
| checkStateForClassLoading(name); |
| |
| // (0) Check our previously loaded local class cache |
| clazz = findLoadedClass0(name); |
| if (clazz != null) { |
| if (log.isTraceEnabled()) { |
| log.trace(" Returning class from cache"); |
| } |
| if (resolve) { |
| resolveClass(clazz); |
| } |
| return clazz; |
| } |
| |
| // (0.1) Check our previously loaded class cache |
| clazz = JreCompat.isGraalAvailable() ? null : findLoadedClass(name); |
| if (clazz != null) { |
| if (log.isTraceEnabled()) { |
| log.trace(" Returning class from cache"); |
| } |
| if (resolve) { |
| resolveClass(clazz); |
| } |
| return clazz; |
| } |
| |
| /* |
| * (0.2) Try loading the class with the bootstrap class loader, to prevent the webapp from overriding Java |
| * SE classes. This implements SRV.10.7.2 |
| */ |
| String resourceName = binaryNameToPath(name, false); |
| |
| ClassLoader javaseLoader = getJavaseClassLoader(); |
| boolean tryLoadingFromJavaseLoader; |
| try { |
| /* |
| * Use getResource as it won't trigger an expensive ClassNotFoundException if the resource is not |
| * available from the Java SE class loader. |
| * |
| * See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for details of how this may trigger a |
| * StackOverflowError. |
| * |
| * Given these reported errors, catch Throwable to ensure all edge cases are caught. |
| */ |
| URL url = javaseLoader.getResource(resourceName); |
| tryLoadingFromJavaseLoader = url != null; |
| } catch (Throwable t) { |
| // Swallow all exceptions apart from those that must be re-thrown |
| ExceptionUtils.handleThrowable(t); |
| // The getResource() trick won't work for this class. We have to |
| // try loading it directly and accept that we might get a |
| // ClassNotFoundException. |
| tryLoadingFromJavaseLoader = true; |
| } |
| |
| if (tryLoadingFromJavaseLoader) { |
| try { |
| clazz = javaseLoader.loadClass(name); |
| if (clazz != null) { |
| if (resolve) { |
| resolveClass(clazz); |
| } |
| return clazz; |
| } |
| } catch (ClassNotFoundException e) { |
| // Ignore |
| } |
| } |
| |
| boolean delegateLoad = delegate || filter(name, true); |
| |
| // (1) Delegate to our parent if requested |
| if (delegateLoad) { |
| if (log.isTraceEnabled()) { |
| log.trace(" Delegating to parent classloader1 " + parent); |
| } |
| try { |
| clazz = Class.forName(name, false, parent); |
| if (clazz != null) { |
| if (log.isTraceEnabled()) { |
| log.trace(" Loading class from parent"); |
| } |
| if (resolve) { |
| resolveClass(clazz); |
| } |
| return clazz; |
| } |
| } catch (ClassNotFoundException e) { |
| // Ignore |
| } |
| } |
| |
| // (2) Search local repositories |
| if (log.isTraceEnabled()) { |
| log.trace(" Searching local repositories"); |
| } |
| try { |
| clazz = findClass(name); |
| if (clazz != null) { |
| if (log.isTraceEnabled()) { |
| log.trace(" Loading class from local repository"); |
| } |
| if (resolve) { |
| resolveClass(clazz); |
| } |
| return clazz; |
| } |
| } catch (ClassNotFoundException e) { |
| // Ignore |
| } |
| |
| // (3) Delegate to parent unconditionally |
| if (!delegateLoad) { |
| if (log.isTraceEnabled()) { |
| log.trace(" Delegating to parent classloader at end: " + parent); |
| } |
| try { |
| clazz = Class.forName(name, false, parent); |
| if (clazz != null) { |
| if (log.isTraceEnabled()) { |
| log.trace(" Loading class from parent"); |
| } |
| if (resolve) { |
| resolveClass(clazz); |
| } |
| return clazz; |
| } |
| } catch (ClassNotFoundException e) { |
| // Ignore |
| } |
| } |
| } |
| |
| if (log.isDebugEnabled()) { |
| log.debug(ToStringUtil.classPathForCNFE(this)); |
| } |
| throw new ClassNotFoundException(name); |
| } |
| |
| |
| protected void checkStateForClassLoading(String className) throws ClassNotFoundException { |
| // It is not permitted to load new classes once the web application has |
| // been stopped. |
| try { |
| checkStateForResourceLoading(className); |
| } catch (IllegalStateException ise) { |
| throw new ClassNotFoundException(ise.getMessage(), ise); |
| } |
| } |
| |
| |
| protected void checkStateForResourceLoading(String resource) throws IllegalStateException { |
| // It is not permitted to load resources once the web application has |
| // been stopped. |
| if (!state.isAvailable()) { |
| IllegalStateException ise = new IllegalStateException(sm.getString("webappClassLoader.stopped", resource)); |
| log.info(ise.getMessage(), ise); |
| throw ise; |
| } |
| } |
| |
| /** |
| * Get the Permissions for a CodeSource. If this instance of WebappClassLoaderBase is for a web application context, |
| * add read FilePermission for the appropriate resources. |
| * |
| * @param codeSource where the code was loaded from |
| * |
| * @return PermissionCollection for CodeSource |
| */ |
| @Override |
| protected PermissionCollection getPermissions(CodeSource codeSource) { |
| return null; |
| } |
| |
| |
| /** |
| * {@inheritDoc} |
| * <p> |
| * Note that list of URLs returned by this method may not be complete. The web application class loader accesses |
| * class loader resources via the {@link WebResourceRoot} which supports the arbitrary mapping of additional files, |
| * directories and contents of JAR files under WEB-INF/classes. Any such resources will not be included in the URLs |
| * returned here. |
| */ |
| @Override |
| public URL[] getURLs() { |
| ArrayList<URL> result = new ArrayList<>(); |
| result.addAll(localRepositories); |
| result.addAll(Arrays.asList(super.getURLs())); |
| return result.toArray(new URL[0]); |
| } |
| |
| |
| // ------------------------------------------------------ Lifecycle Methods |
| |
| |
| @Override |
| public void addLifecycleListener(LifecycleListener listener) { |
| // NOOP |
| } |
| |
| |
| @Override |
| public LifecycleListener[] findLifecycleListeners() { |
| return new LifecycleListener[0]; |
| } |
| |
| |
| @Override |
| public void removeLifecycleListener(LifecycleListener listener) { |
| // NOOP |
| } |
| |
| |
| @Override |
| public LifecycleState getState() { |
| return state; |
| } |
| |
| |
| @Override |
| public String getStateName() { |
| return getState().toString(); |
| } |
| |
| |
| @Override |
| public void init() { |
| state = LifecycleState.INITIALIZED; |
| } |
| |
| |
| @Override |
| public void start() throws LifecycleException { |
| |
| state = LifecycleState.STARTING_PREP; |
| |
| WebResource[] classesResources = resources.getResources("/WEB-INF/classes"); |
| for (WebResource classes : classesResources) { |
| if (classes.isDirectory() && classes.canRead()) { |
| localRepositories.add(classes.getURL()); |
| } |
| } |
| WebResource[] jars = resources.listResources("/WEB-INF/lib"); |
| for (WebResource jar : jars) { |
| if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) { |
| localRepositories.add(jar.getURL()); |
| jarModificationTimes.put(jar.getName(), Long.valueOf(jar.getLastModified())); |
| } |
| } |
| |
| state = LifecycleState.STARTED; |
| } |
| |
| |
| @Override |
| public void stop() throws LifecycleException { |
| |
| state = LifecycleState.STOPPING_PREP; |
| |
| // Clearing references should be done before setting started to |
| // false, due to possible side effects |
| clearReferences(); |
| |
| state = LifecycleState.STOPPING; |
| |
| resourceEntries.clear(); |
| notFoundClassResources.clear(); |
| jarModificationTimes.clear(); |
| resources = null; |
| |
| permissionList.clear(); |
| loaderPC.clear(); |
| |
| state = LifecycleState.STOPPED; |
| } |
| |
| |
| @Override |
| public void destroy() { |
| state = LifecycleState.DESTROYING; |
| |
| try { |
| super.close(); |
| } catch (IOException ioe) { |
| log.warn(sm.getString("webappClassLoader.superCloseFail"), ioe); |
| } |
| state = LifecycleState.DESTROYED; |
| } |
| |
| |
| // ------------------------------------------------------ Protected Methods |
| |
| protected ClassLoader getJavaseClassLoader() { |
| return javaseClassLoader; |
| } |
| |
| protected void setJavaseClassLoader(ClassLoader classLoader) { |
| if (classLoader == null) { |
| throw new IllegalArgumentException(sm.getString("webappClassLoader.javaseClassLoaderNull")); |
| } |
| javaseClassLoader = classLoader; |
| } |
| |
| /** |
| * Clear references. |
| */ |
| protected void clearReferences() { |
| |
| // If the JVM is shutting down, skip the memory leak checks |
| if (skipMemoryLeakChecksOnJvmShutdown && !resources.getContext().getParent().getState().isAvailable()) { |
| // During reloading / redeployment the parent is expected to be |
| // available. Parent is not available so this might be a JVM |
| // shutdown. |
| try { |
| Thread dummyHook = new Thread(); |
| Runtime.getRuntime().addShutdownHook(dummyHook); |
| Runtime.getRuntime().removeShutdownHook(dummyHook); |
| } catch (IllegalStateException ise) { |
| return; |
| } |
| } |
| |
| if (!JreCompat.isGraalAvailable()) { |
| // De-register any remaining JDBC drivers |
| clearReferencesJdbc(); |
| } |
| |
| // Stop any threads the web application started |
| clearReferencesThreads(); |
| |
| // Check for leaks triggered by ThreadLocals loaded by this class loader |
| if (clearReferencesThreadLocals && !JreCompat.isGraalAvailable()) { |
| checkThreadLocalsForLeaks(); |
| } |
| |
| // Clear RMI Targets loaded by this class loader |
| if (clearReferencesRmiTargets) { |
| clearReferencesRmiTargets(); |
| } |
| |
| // Clear the IntrospectionUtils cache. |
| IntrospectionUtils.clear(); |
| |
| // Clear the classloader reference in common-logging |
| if (clearReferencesLogFactoryRelease) { |
| LogFactory.release(this); |
| } |
| |
| // Clear the classloader reference in the VM's bean introspector |
| java.beans.Introspector.flushCaches(); |
| |
| // Clear any custom URLStreamHandlers |
| TomcatURLStreamHandlerFactory.release(this); |
| } |
| |
| |
| /** |
| * Deregister any JDBC drivers registered by the webapp that the webapp forgot. This is made unnecessary complex |
| * because a) DriverManager checks the class loader of the calling class (it would be much easier if it checked the |
| * context class loader) b) using reflection would create a dependency on the DriverManager implementation which |
| * can, and has, changed. |
| * <p> |
| * We can't just create an instance of JdbcLeakPrevention as it will be loaded by the common class loader (since |
| * it's .class file is in the $CATALINA_HOME/lib directory). This would fail DriverManager's check on the class |
| * loader of the calling class. So, we load the bytes via our parent class loader but define the class with this |
| * class loader so the JdbcLeakPrevention looks like a webapp class to the DriverManager. |
| * <p> |
| * If only apps cleaned up after themselves... |
| */ |
| private void clearReferencesJdbc() { |
| // We know roughly how big the class will be (~ 1K) so allow 2k as a |
| // starting point |
| byte[] classBytes = new byte[2048]; |
| int offset = 0; |
| try (InputStream is = getResourceAsStream("org/apache/catalina/loader/JdbcLeakPrevention.class")) { |
| if (is == null) { |
| throw new FileNotFoundException("org/apache/catalina/loader/JdbcLeakPrevention.class"); |
| } |
| int read = is.read(classBytes, offset, classBytes.length - offset); |
| while (read > -1) { |
| offset += read; |
| if (offset == classBytes.length) { |
| // Buffer full - double size |
| byte[] tmp = new byte[classBytes.length * 2]; |
| System.arraycopy(classBytes, 0, tmp, 0, classBytes.length); |
| classBytes = tmp; |
| } |
| read = is.read(classBytes, offset, classBytes.length - offset); |
| } |
| Class<?> lpClass = defineClass("org.apache.catalina.loader.JdbcLeakPrevention", classBytes, 0, offset, |
| this.getClass().getProtectionDomain()); |
| Object obj = lpClass.getConstructor().newInstance(); |
| @SuppressWarnings("unchecked") |
| List<String> driverNames = |
| (List<String>) obj.getClass().getMethod("clearJdbcDriverRegistrations").invoke(obj); |
| for (String name : driverNames) { |
| log.warn(sm.getString("webappClassLoader.clearJdbc", getContextName(), name)); |
| } |
| } catch (Exception e) { |
| // So many things to go wrong above... |
| Throwable t = ExceptionUtils.unwrapInvocationTargetException(e); |
| ExceptionUtils.handleThrowable(t); |
| log.warn(sm.getString("webappClassLoader.jdbcRemoveFailed", getContextName()), t); |
| } |
| } |
| |
| |
| private void clearReferencesThreads() { |
| Thread[] threads = getThreads(); |
| List<Thread> threadsToStop = new ArrayList<>(); |
| |
| // Iterate over the set of threads |
| for (Thread thread : threads) { |
| if (thread != null) { |
| ClassLoader ccl = thread.getContextClassLoader(); |
| if (ccl == this) { |
| // Don't warn about this thread |
| if (thread == Thread.currentThread()) { |
| continue; |
| } |
| |
| final String threadName = thread.getName(); |
| |
| // JVM controlled threads |
| ThreadGroup tg = thread.getThreadGroup(); |
| if (tg != null && JVM_THREAD_GROUP_NAMES.contains(tg.getName())) { |
| // HttpClient keep-alive threads |
| if (clearReferencesHttpClientKeepAliveThread && threadName.equals("Keep-Alive-Timer")) { |
| thread.setContextClassLoader(parent); |
| log.debug(sm.getString("webappClassLoader.checkThreadsHttpClient")); |
| } |
| |
| // Don't warn about remaining JVM controlled threads |
| continue; |
| } |
| |
| // Skip threads that have already died |
| if (!thread.isAlive()) { |
| continue; |
| } |
| |
| // TimerThread can be stopped safely so treat separately |
| // "java.util.TimerThread" in Sun/Oracle JDK |
| // "java.util.Timer$TimerImpl" in Apache Harmony and in IBM JDK |
| if (thread.getClass().getName().startsWith("java.util.Timer") && clearReferencesStopTimerThreads) { |
| clearReferencesStopTimerThread(thread); |
| continue; |
| } |
| |
| if (isRequestThread(thread)) { |
| log.warn(sm.getString("webappClassLoader.stackTraceRequestThread", getContextName(), threadName, |
| getStackTrace(thread))); |
| } else { |
| log.warn(sm.getString("webappClassLoader.stackTrace", getContextName(), threadName, |
| getStackTrace(thread))); |
| } |
| |
| // Don't try and stop the threads unless explicitly |
| // configured to do so |
| if (!clearReferencesStopThreads) { |
| continue; |
| } |
| |
| // If the thread has been started via an executor, try |
| // shutting down the executor |
| boolean usingExecutor = false; |
| try { |
| Object executor = getExecutor(thread); |
| if (executor instanceof ThreadPoolExecutor) { |
| ((ThreadPoolExecutor) executor).shutdownNow(); |
| usingExecutor = true; |
| } else if (executor instanceof java.util.concurrent.ThreadPoolExecutor) { |
| ((java.util.concurrent.ThreadPoolExecutor) executor).shutdownNow(); |
| usingExecutor = true; |
| } |
| } catch (SecurityException | NoSuchFieldException | IllegalArgumentException | |
| IllegalAccessException | InaccessibleObjectException e) { |
| log.warn(sm.getString("webappClassLoader.stopThreadFail", thread.getName(), getContextName()), |
| e); |
| } |
| |
| // Stopping an executor automatically interrupts the |
| // associated threads. For non-executor threads, interrupt |
| // them here. |
| if (!usingExecutor && !thread.isInterrupted()) { |
| thread.interrupt(); |
| } |
| |
| // Threads are expected to take a short time to stop after |
| // being interrupted. Make a note of all threads that are |
| // expected to stop to enable them to be checked at the end |
| // of this method. |
| threadsToStop.add(thread); |
| } |
| } |
| } |
| |
| // If thread stopping is enabled, threads should have been stopped above |
| // when the executor was shut down or the thread was interrupted but |
| // that depends on the thread correctly handling the interrupt. Check |
| // each thread and if any are still running give all threads up to a |
| // total of 2 seconds to shut down. |
| int count = 0; |
| for (Thread t : threadsToStop) { |
| while (t.isAlive() && count < 100) { |
| try { |
| Thread.sleep(20); |
| } catch (InterruptedException e) { |
| // Quit the while loop |
| break; |
| } |
| count++; |
| } |
| if (t.isAlive()) { |
| // Unable to stop the thread. Log an error. |
| log.error(sm.getString("webappClassLoader.stopThreadFail", t.getName(), getContextName())); |
| } |
| } |
| } |
| |
| |
| private Object getExecutor(Thread thread) |
| throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { |
| |
| Object result = null; |
| |
| // Runnable wrapped by Thread |
| // "target" in Sun/Oracle JDK |
| // "runnable" in IBM JDK |
| // "action" in Apache Harmony |
| Object target = null; |
| for (String fieldName : new String[] { "target", "runnable", "action" }) { |
| try { |
| Field targetField = thread.getClass().getDeclaredField(fieldName); |
| targetField.setAccessible(true); |
| target = targetField.get(thread); |
| break; |
| } catch (NoSuchFieldException nfe) { |
| // Ignore |
| } |
| } |
| |
| // "java.util.concurrent" code is in public domain, |
| // so all implementations are similar including our |
| // internal fork. |
| if (target != null && target.getClass().getCanonicalName() != null && (target.getClass().getCanonicalName() |
| .equals("org.apache.tomcat.util.threads.ThreadPoolExecutor.Worker") || |
| target.getClass().getCanonicalName().equals("java.util.concurrent.ThreadPoolExecutor.Worker"))) { |
| Field executorField = target.getClass().getDeclaredField("this$0"); |
| executorField.setAccessible(true); |
| result = executorField.get(target); |
| } |
| |
| if (result == null) { |
| Object holder; |
| Object task; |
| try { |
| Field holderField = thread.getClass().getDeclaredField("holder"); |
| holderField.setAccessible(true); |
| holder = holderField.get(thread); |
| |
| Field taskField = holder.getClass().getDeclaredField("task"); |
| taskField.setAccessible(true); |
| task = taskField.get(holder); |
| } catch (NoSuchFieldException nfe) { |
| return null; |
| } |
| |
| if (task != null && task.getClass().getCanonicalName() != null && (task.getClass().getCanonicalName() |
| .equals("org.apache.tomcat.util.threads.ThreadPoolExecutor.Worker") || |
| task.getClass().getCanonicalName().equals("java.util.concurrent.ThreadPoolExecutor.Worker"))) { |
| Field executorField = task.getClass().getDeclaredField("this$0"); |
| executorField.setAccessible(true); |
| result = executorField.get(task); |
| } |
| } |
| |
| return result; |
| } |
| |
| |
| /* |
| * Look at a threads stack trace to see if it is a request thread or not. It isn't perfect, but it should be |
| * good-enough for most cases. |
| */ |
| private boolean isRequestThread(Thread thread) { |
| |
| StackTraceElement[] elements = thread.getStackTrace(); |
| |
| if (elements.length == 0) { |
| // Must have stopped already. Too late to ignore it. Assume not a |
| // request processing thread. |
| return false; |
| } |
| |
| // Step through the methods in reverse order looking for calls to any |
| // CoyoteAdapter method. All request threads will have this unless |
| // Tomcat has been heavily modified - in which case there isn't much we |
| // can do. |
| for (int i = 0; i < elements.length; i++) { |
| StackTraceElement element = elements[elements.length - (i + 1)]; |
| if ("org.apache.catalina.connector.CoyoteAdapter".equals(element.getClassName())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| |
| private void clearReferencesStopTimerThread(Thread thread) { |
| |
| // Need to get references to: |
| // in Sun/Oracle JDK: |
| // - newTasksMayBeScheduled field (in java.util.TimerThread) |
| // - queue field |
| // - queue.clear() |
| // in IBM JDK, Apache Harmony: |
| // - cancel() method (in java.util.Timer$TimerImpl) |
| |
| try { |
| |
| try { |
| Field newTasksMayBeScheduledField = thread.getClass().getDeclaredField("newTasksMayBeScheduled"); |
| newTasksMayBeScheduledField.setAccessible(true); |
| Field queueField = thread.getClass().getDeclaredField("queue"); |
| queueField.setAccessible(true); |
| |
| Object queue = queueField.get(thread); |
| |
| Method clearMethod = queue.getClass().getDeclaredMethod("clear"); |
| clearMethod.setAccessible(true); |
| |
| synchronized (queue) { |
| newTasksMayBeScheduledField.setBoolean(thread, false); |
| clearMethod.invoke(queue); |
| // In case queue was already empty. Should only be one |
| // thread waiting but use notifyAll() to be safe. |
| queue.notifyAll(); |
| } |
| |
| } catch (NoSuchFieldException nfe) { |
| Method cancelMethod = thread.getClass().getDeclaredMethod("cancel"); |
| synchronized (thread) { |
| cancelMethod.setAccessible(true); |
| cancelMethod.invoke(thread); |
| } |
| } |
| |
| log.warn(sm.getString("webappClassLoader.warnTimerThread", getContextName(), thread.getName())); |
| |
| } catch (Exception e) { |
| // So many things to go wrong above... |
| Throwable t = ExceptionUtils.unwrapInvocationTargetException(e); |
| ExceptionUtils.handleThrowable(t); |
| log.warn(sm.getString("webappClassLoader.stopTimerThreadFail", thread.getName(), getContextName()), t); |
| } |
| } |
| |
| private void checkThreadLocalsForLeaks() { |
| Thread[] threads = getThreads(); |
| |
| try { |
| // Make the fields in the Thread class that store ThreadLocals |
| // accessible |
| Field threadLocalsField = Thread.class.getDeclaredField("threadLocals"); |
| threadLocalsField.setAccessible(true); |
| Field inheritableThreadLocalsField = Thread.class.getDeclaredField("inheritableThreadLocals"); |
| inheritableThreadLocalsField.setAccessible(true); |
| // Make the underlying array of ThreadLoad.ThreadLocalMap.Entry objects |
| // accessible |
| Class<?> tlmClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); |
| Field tableField = tlmClass.getDeclaredField("table"); |
| tableField.setAccessible(true); |
| Method expungeStaleEntriesMethod = tlmClass.getDeclaredMethod("expungeStaleEntries"); |
| expungeStaleEntriesMethod.setAccessible(true); |
| |
| for (Thread thread : threads) { |
| Object threadLocalMap; |
| if (thread != null) { |
| |
| // Clear the first map |
| threadLocalMap = threadLocalsField.get(thread); |
| if (null != threadLocalMap) { |
| expungeStaleEntriesMethod.invoke(threadLocalMap); |
| checkThreadLocalMapForLeaks(threadLocalMap, tableField); |
| } |
| |
| // Clear the second map |
| threadLocalMap = inheritableThreadLocalsField.get(thread); |
| if (null != threadLocalMap) { |
| expungeStaleEntriesMethod.invoke(threadLocalMap); |
| checkThreadLocalMapForLeaks(threadLocalMap, tableField); |
| } |
| } |
| } |
| } catch (InaccessibleObjectException e) { |
| // Must be running on without the necessary command line options. |
| log.warn(sm.getString("webappClassLoader.addExportsThreadLocal", getCurrentModuleName())); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| log.warn(sm.getString("webappClassLoader.checkThreadLocalsForLeaksFail", getContextName()), t); |
| } |
| } |
| |
| |
| /** |
| * Analyzes the given thread local map object. Also pass in the field that points to the internal table to save |
| * re-calculating it on every call to this method. |
| */ |
| private void checkThreadLocalMapForLeaks(Object map, Field internalTableField) |
| throws IllegalAccessException, NoSuchFieldException { |
| if (map != null) { |
| Object[] table = (Object[]) internalTableField.get(map); |
| if (table != null) { |
| for (Object obj : table) { |
| if (obj != null) { |
| boolean keyLoadedByWebapp = false; |
| boolean valueLoadedByWebapp = false; |
| // Check the key |
| Object key = ((Reference<?>) obj).get(); |
| if (this.equals(key) || loadedByThisOrChild(key)) { |
| keyLoadedByWebapp = true; |
| } |
| // Check the value |
| Field valueField = obj.getClass().getDeclaredField("value"); |
| valueField.setAccessible(true); |
| Object value = valueField.get(obj); |
| if (this.equals(value) || loadedByThisOrChild(value)) { |
| valueLoadedByWebapp = true; |
| } |
| if (keyLoadedByWebapp || valueLoadedByWebapp) { |
| Object[] args = new Object[5]; |
| args[0] = getContextName(); |
| if (key != null) { |
| args[1] = getPrettyClassName(key.getClass()); |
| try { |
| args[2] = key.toString(); |
| } catch (Exception e) { |
| log.warn( |
| sm.getString("webappClassLoader.checkThreadLocalsForLeaks.badKey", args[1]), |
| e); |
| args[2] = sm.getString("webappClassLoader.checkThreadLocalsForLeaks.unknown"); |
| } |
| } |
| if (value != null) { |
| args[3] = getPrettyClassName(value.getClass()); |
| try { |
| args[4] = value.toString(); |
| } catch (Exception e) { |
| log.warn(sm.getString("webappClassLoader.checkThreadLocalsForLeaks.badValue", |
| args[3]), e); |
| args[4] = sm.getString("webappClassLoader.checkThreadLocalsForLeaks.unknown"); |
| } |
| } |
| if (valueLoadedByWebapp) { |
| log.error(sm.getString("webappClassLoader.checkThreadLocalsForLeaks", args)); |
| } else if (value == null) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("webappClassLoader.checkThreadLocalsForLeaksNull", args)); |
| } |
| } else { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("webappClassLoader.checkThreadLocalsForLeaksNone", args)); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private String getPrettyClassName(Class<?> clazz) { |
| String name = clazz.getCanonicalName(); |
| if (name == null) { |
| name = clazz.getName(); |
| } |
| return name; |
| } |
| |
| private String getStackTrace(Thread thread) { |
| StringBuilder builder = new StringBuilder(); |
| for (StackTraceElement ste : thread.getStackTrace()) { |
| builder.append("\n ").append(ste); |
| } |
| return builder.toString(); |
| } |
| |
| /** |
| * @param o object to test, may be null |
| * |
| * @return <code>true</code> if o has been loaded by the current classloader or one of its descendants. |
| */ |
| private boolean loadedByThisOrChild(Object o) { |
| if (o == null) { |
| return false; |
| } |
| |
| Class<?> clazz; |
| if (o instanceof Class) { |
| clazz = (Class<?>) o; |
| } else { |
| clazz = o.getClass(); |
| } |
| |
| ClassLoader cl = clazz.getClassLoader(); |
| while (cl != null) { |
| if (cl == this) { |
| return true; |
| } |
| cl = cl.getParent(); |
| } |
| |
| if (o instanceof Collection<?>) { |
| try { |
| for (Object entry : (Collection<?>) o) { |
| if (loadedByThisOrChild(entry)) { |
| return true; |
| } |
| } |
| } catch (ConcurrentModificationException e) { |
| log.warn(sm.getString("webappClassLoader.loadedByThisOrChildFail", clazz.getName(), getContextName()), |
| e); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * @return the current threads as an array. |
| */ |
| private Thread[] getThreads() { |
| // Get the current thread group |
| ThreadGroup tg = Thread.currentThread().getThreadGroup(); |
| // Find the root thread group |
| try { |
| while (tg.getParent() != null) { |
| tg = tg.getParent(); |
| } |
| } catch (SecurityException se) { |
| String msg = sm.getString("webappClassLoader.getThreadGroupError", tg.getName()); |
| if (log.isDebugEnabled()) { |
| log.debug(msg, se); |
| } else { |
| log.warn(msg); |
| } |
| } |
| |
| int threadCountGuess = tg.activeCount() + 50; |
| Thread[] threads = new Thread[threadCountGuess]; |
| int threadCountActual = tg.enumerate(threads); |
| // Make sure we don't miss any threads |
| while (threadCountActual == threadCountGuess) { |
| threadCountGuess *= 2; |
| threads = new Thread[threadCountGuess]; |
| // Note tg.enumerate(Thread[]) silently ignores any threads that |
| // can't fit into the array |
| threadCountActual = tg.enumerate(threads); |
| } |
| |
| return threads; |
| } |
| |
| |
| /** |
| * This depends on the internals of the Sun JVM so it does everything by reflection. |
| */ |
| private void clearReferencesRmiTargets() { |
| try { |
| // Need access to the ccl field of sun.rmi.transport.Target to find |
| // the leaks |
| Class<?> objectTargetClass = Class.forName("sun.rmi.transport.Target"); |
| Field cclField = objectTargetClass.getDeclaredField("ccl"); |
| cclField.setAccessible(true); |
| // Need access to the stub field to report the leaks |
| Field stubField = objectTargetClass.getDeclaredField("stub"); |
| stubField.setAccessible(true); |
| |
| // Clear the objTable map |
| Class<?> objectTableClass = Class.forName("sun.rmi.transport.ObjectTable"); |
| Field objTableField = objectTableClass.getDeclaredField("objTable"); |
| objTableField.setAccessible(true); |
| Object objTable = objTableField.get(null); |
| if (objTable == null) { |
| return; |
| } |
| Field tableLockField = objectTableClass.getDeclaredField("tableLock"); |
| tableLockField.setAccessible(true); |
| Object tableLock = tableLockField.get(null); |
| |
| synchronized (tableLock) { |
| // Iterate over the values in the table |
| if (objTable instanceof Map<?,?>) { |
| Iterator<?> iter = ((Map<?,?>) objTable).values().iterator(); |
| while (iter.hasNext()) { |
| Object obj = iter.next(); |
| Object cclObject = cclField.get(obj); |
| if (this == cclObject) { |
| iter.remove(); |
| Object stubObject = stubField.get(obj); |
| log.error(sm.getString("webappClassLoader.clearRmi", stubObject.getClass().getName(), |
| stubObject)); |
| } |
| } |
| } |
| |
| // Clear the implTable map |
| Field implTableField = objectTableClass.getDeclaredField("implTable"); |
| implTableField.setAccessible(true); |
| Object implTable = implTableField.get(null); |
| if (implTable == null) { |
| return; |
| } |
| |
| // Iterate over the values in the table |
| if (implTable instanceof Map<?,?>) { |
| Iterator<?> iter = ((Map<?,?>) implTable).values().iterator(); |
| while (iter.hasNext()) { |
| Object obj = iter.next(); |
| Object cclObject = cclField.get(obj); |
| if (this == cclObject) { |
| iter.remove(); |
| } |
| } |
| } |
| } |
| } catch (ClassNotFoundException e) { |
| log.info(sm.getString("webappClassLoader.clearRmiInfo", getContextName()), e); |
| } catch (SecurityException | NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) { |
| log.warn(sm.getString("webappClassLoader.clearRmiFail", getContextName()), e); |
| } catch (InaccessibleObjectException e) { |
| // Must be running on without the necessary command line options. |
| log.warn(sm.getString("webappClassLoader.addExportsRmi", getCurrentModuleName())); |
| } |
| } |
| |
| |
| private String getCurrentModuleName() { |
| String moduleName = this.getClass().getModule().getName(); |
| if (moduleName == null) { |
| moduleName = "ALL-UNNAMED"; |
| } |
| return moduleName; |
| } |
| |
| |
| /** |
| * Find specified class in local repositories. |
| * |
| * @param name The binary name of the class to be loaded |
| * |
| * @return the loaded class, or null if the class isn't found |
| */ |
| protected Class<?> findClassInternal(String name) { |
| |
| checkStateForResourceLoading(name); |
| |
| if (name == null) { |
| return null; |
| } |
| String path = binaryNameToPath(name, true); |
| |
| return findClassInternal(name, path); |
| } |
| |
| |
| /* |
| * The use of getPackage() is appropriate given that the code is checking if the package is sealed. Therefore, |
| * parent class loaders need to be checked. |
| */ |
| @SuppressWarnings("deprecation") |
| private Class<?> findClassInternal(String name, String path) { |
| ResourceEntry entry = resourceEntries.get(path); |
| WebResource resource = null; |
| |
| if (entry == null) { |
| resource = resources.getClassLoaderResource(path); |
| |
| if (!resource.exists()) { |
| return null; |
| } |
| |
| entry = new ResourceEntry(); |
| entry.lastModified = resource.getLastModified(); |
| |
| // Add the entry in the local resource repository |
| synchronized (resourceEntries) { |
| // Ensures that all the threads which may be in a race to load |
| // a particular class all end up with the same ResourceEntry |
| // instance |
| ResourceEntry entry2 = resourceEntries.get(path); |
| if (entry2 == null) { |
| resourceEntries.put(path, entry); |
| } else { |
| entry = entry2; |
| } |
| } |
| } |
| |
| Class<?> clazz = entry.loadedClass; |
| if (clazz != null) { |
| return clazz; |
| } |
| |
| synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) { |
| clazz = entry.loadedClass; |
| if (clazz != null) { |
| return clazz; |
| } |
| |
| if (resource == null) { |
| resource = resources.getClassLoaderResource(path); |
| } |
| |
| if (!resource.exists()) { |
| return null; |
| } |
| |
| byte[] binaryContent = resource.getContent(); |
| if (binaryContent == null) { |
| // Something went wrong reading the class bytes (and will have |
| // been logged at debug level). |
| return null; |
| } |
| Manifest manifest = resource.getManifest(); |
| URL codeBase = resource.getCodeBase(); |
| Certificate[] certificates = resource.getCertificates(); |
| |
| if (!transformers.isEmpty()) { |
| // If the resource is a class just being loaded, decorate it |
| // with any attached transformers |
| |
| // Ignore leading '/' and trailing CLASS_FILE_SUFFIX |
| // Should be cheaper than replacing '.' by '/' in class name. |
| String internalName = path.substring(1, path.length() - CLASS_FILE_SUFFIX.length()); |
| |
| for (ClassFileTransformer transformer : this.transformers) { |
| try { |
| byte[] transformed = transformer.transform(this, internalName, null, null, binaryContent); |
| if (transformed != null) { |
| binaryContent = transformed; |
| } |
| } catch (IllegalClassFormatException e) { |
| log.error(sm.getString("webappClassLoader.transformError", name), e); |
| return null; |
| } |
| } |
| } |
| |
| // Looking up the package |
| String packageName = null; |
| int pos = name.lastIndexOf('.'); |
| if (pos != -1) { |
| packageName = name.substring(0, pos); |
| } |
| |
| Package pkg; |
| |
| if (packageName != null) { |
| pkg = getPackage(packageName); |
| |
| // Define the package (if null) |
| if (pkg == null) { |
| try { |
| if (manifest == null) { |
| definePackage(packageName, null, null, null, null, null, null, null); |
| } else { |
| definePackage(packageName, manifest, codeBase); |
| } |
| } catch (IllegalArgumentException e) { |
| // Ignore: normal error due to dual definition of package |
| } |
| pkg = getPackage(packageName); |
| } |
| } |
| |
| try { |
| clazz = defineClass(name, binaryContent, 0, binaryContent.length, |
| new CodeSource(codeBase, certificates)); |
| } catch (UnsupportedClassVersionError ucve) { |
| throw new UnsupportedClassVersionError( |
| ucve.getLocalizedMessage() + " " + sm.getString("webappClassLoader.wrongVersion", name)); |
| } catch (LinkageError e) { |
| // May be caused by the transformation also triggering loading of the class - BZ 68721 |
| try { |
| // Try and load the already defined class |
| clazz = findLoadedClass0(name); |
| } catch (Throwable t) { |
| // Not BZ 68721 |
| ExceptionUtils.handleThrowable(t); |
| // Re-throw the original exception |
| throw e; |
| } |
| if (clazz == null) { |
| // Not BZ 68721 |
| throw e; |
| } |
| } |
| entry.loadedClass = clazz; |
| } |
| |
| return clazz; |
| } |
| |
| |
| private String binaryNameToPath(String binaryName, boolean withLeadingSlash) { |
| // 1 for leading '/', 6 for ".class" |
| StringBuilder path = new StringBuilder(7 + binaryName.length()); |
| if (withLeadingSlash) { |
| path.append('/'); |
| } |
| path.append(binaryName.replace('.', '/')); |
| path.append(CLASS_FILE_SUFFIX); |
| return path.toString(); |
| } |
| |
| |
| private String nameToPath(String name) { |
| StringBuilder path = new StringBuilder(1 + name.length()); |
| path.append('/'); |
| path.append(name); |
| return path.toString(); |
| } |
| |
| |
| /** |
| * Returns true if the specified package name is sealed according to the given manifest. |
| * |
| * @param name Path name to check |
| * @param man Associated manifest |
| * |
| * @return <code>true</code> if the manifest associated says it is sealed |
| */ |
| protected boolean isPackageSealed(String name, Manifest man) { |
| |
| String path = name.replace('.', '/') + '/'; |
| Attributes attr = man.getAttributes(path); |
| String sealed = null; |
| if (attr != null) { |
| sealed = attr.getValue(Name.SEALED); |
| } |
| if (sealed == null) { |
| if ((attr = man.getMainAttributes()) != null) { |
| sealed = attr.getValue(Name.SEALED); |
| } |
| } |
| return "true".equalsIgnoreCase(sealed); |
| |
| } |
| |
| |
| /** |
| * Finds the class with the given name if it has previously been loaded and cached by this class loader, and return |
| * the Class object. If this class has not been cached, return <code>null</code>. |
| * |
| * @param name The binary name of the resource to return |
| * |
| * @return a loaded class |
| */ |
| protected Class<?> findLoadedClass0(String name) { |
| |
| String path = binaryNameToPath(name, true); |
| |
| ResourceEntry entry = resourceEntries.get(path); |
| if (entry != null) { |
| return entry.loadedClass; |
| } |
| return null; |
| } |
| |
| |
| /** |
| * Filter classes. |
| * |
| * @param name class name |
| * @param isClassName <code>true</code> if name is a class name, <code>false</code> if name is a resource name |
| * |
| * @return <code>true</code> if the class should be filtered |
| */ |
| protected boolean filter(String name, boolean isClassName) { |
| |
| if (name == null) { |
| return false; |
| } |
| |
| char ch; |
| if (name.startsWith("jakarta")) { |
| /* 7 == length("jakarta") */ |
| if (name.length() == 7) { |
| return false; |
| } |
| ch = name.charAt(7); |
| if (isClassName && ch == '.') { |
| /* 8 == length("jakarta.") */ |
| if (name.startsWith("servlet.jsp.jstl.", 8)) { |
| return false; |
| } |
| if (name.startsWith("annotation.", 8) || name.startsWith("el.", 8) || name.startsWith("servlet.", 8) || |
| name.startsWith("websocket.", 8) || name.startsWith("security.auth.message.", 8)) { |
| return true; |
| } |
| } else if (!isClassName && ch == '/') { |
| /* 8 == length("jakarta/") */ |
| if (name.startsWith("servlet/jsp/jstl/", 8)) { |
| return false; |
| } |
| if (name.startsWith("annotation/", 8) || name.startsWith("el/", 8) || name.startsWith("servlet/", 8) || |
| name.startsWith("websocket/", 8) || name.startsWith("security/auth/message/", 8)) { |
| return true; |
| } |
| } |
| } else if (name.startsWith("javax")) { |
| /* 5 == length("javax") */ |
| if (name.length() == 5) { |
| return false; |
| } |
| ch = name.charAt(5); |
| if (isClassName && ch == '.') { |
| /* 6 == length("javax.") */ |
| if (name.startsWith("websocket.", 6)) { |
| return true; |
| } |
| } else if (!isClassName && ch == '/') { |
| /* 6 == length("javax/") */ |
| if (name.startsWith("websocket/", 6)) { |
| return true; |
| } |
| } |
| } else if (name.startsWith("org")) { |
| /* 3 == length("org") */ |
| if (name.length() == 3) { |
| return false; |
| } |
| ch = name.charAt(3); |
| if (isClassName && ch == '.') { |
| /* 4 == length("org.") */ |
| if (name.startsWith("apache.", 4)) { |
| /* 11 == length("org.apache.") */ |
| if (name.startsWith("tomcat.jdbc.", 11)) { |
| return false; |
| } |
| if (name.startsWith("el.", 11) || name.startsWith("catalina.", 11) || |
| name.startsWith("jasper.", 11) || name.startsWith("juli.", 11) || |
| name.startsWith("tomcat.", 11) || name.startsWith("naming.", 11) || |
| name.startsWith("coyote.", 11)) { |
| return true; |
| } |
| } |
| } else if (!isClassName && ch == '/') { |
| /* 4 == length("org/") */ |
| if (name.startsWith("apache/", 4)) { |
| /* 11 == length("org/apache/") */ |
| if (name.startsWith("tomcat/jdbc/", 11)) { |
| return false; |
| } |
| if (name.startsWith("el/", 11) || name.startsWith("catalina/", 11) || |
| name.startsWith("jasper/", 11) || name.startsWith("juli/", 11) || |
| name.startsWith("tomcat/", 11) || name.startsWith("naming/", 11) || |
| name.startsWith("coyote/", 11)) { |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| |
| @Override |
| protected void addURL(URL url) { |
| super.addURL(url); |
| hasExternalRepositories = true; |
| // Clear the not found resources as they may now be available at the added URL. |
| notFoundClassResources.clear(); |
| } |
| |
| |
| @Override |
| public String getWebappName() { |
| return getContextName(); |
| } |
| |
| |
| @Override |
| public String getHostName() { |
| if (resources != null) { |
| Container host = resources.getContext().getParent(); |
| if (host != null) { |
| return host.getName(); |
| } |
| } |
| return null; |
| } |
| |
| |
| @Override |
| public String getServiceName() { |
| if (resources != null) { |
| Container host = resources.getContext().getParent(); |
| if (host != null) { |
| Container engine = host.getParent(); |
| if (engine != null) { |
| return engine.getName(); |
| } |
| } |
| } |
| return null; |
| } |
| |
| |
| private static class CombinedEnumeration implements Enumeration<URL> { |
| |
| private final Enumeration<URL>[] sources; |
| private int index = 0; |
| |
| CombinedEnumeration(Enumeration<URL> enum1, Enumeration<URL> enum2) { |
| @SuppressWarnings("unchecked") |
| Enumeration<URL>[] sources = new Enumeration[] { enum1, enum2 }; |
| this.sources = sources; |
| } |
| |
| |
| @Override |
| public boolean hasMoreElements() { |
| return inc(); |
| } |
| |
| |
| @Override |
| public URL nextElement() { |
| if (inc()) { |
| return sources[index].nextElement(); |
| } |
| throw new NoSuchElementException(); |
| } |
| |
| |
| private boolean inc() { |
| while (index < sources.length) { |
| if (sources[index].hasMoreElements()) { |
| return true; |
| } |
| index++; |
| } |
| return false; |
| } |
| } |
| } |