This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 3b16fb8c1444e7ce76fb33bcd38378a4bb987c23 Author: Mark Thomas <ma...@apache.org> AuthorDate: Fri Oct 25 21:54:12 2024 +0100 Cache class loader non-found resources Some components (e.g. DriverManager) will attempt to load the same class over and over again even if it isn't found the first time. If the web application is large, this search can take a noticeable amount of time. This cache avoids those issues. It can be disabled if not wanted / required. --- java/org/apache/catalina/core/StandardContext.java | 13 ++ .../catalina/loader/WebappClassLoaderBase.java | 139 +++++++++++++-------- .../util/collections/ConcurrentLruCache.java | 119 ++++++++++++++++++ .../util/collections/TestConcurrentLruCache.java | 96 ++++++++++++++ webapps/docs/config/context.xml | 11 ++ 5 files changed, 327 insertions(+), 51 deletions(-) diff --git a/java/org/apache/catalina/core/StandardContext.java b/java/org/apache/catalina/core/StandardContext.java index f4fb6f8bf0..2994082059 100644 --- a/java/org/apache/catalina/core/StandardContext.java +++ b/java/org/apache/catalina/core/StandardContext.java @@ -798,9 +798,21 @@ public class StandardContext extends ContainerBase implements Context, Notificat private boolean parallelAnnotationScanning = false; + private int notFoundClassResourceCacheSize = 1000; + // ----------------------------------------------------- Context Properties + public int getNotFoundClassResourceCacheSize() { + return notFoundClassResourceCacheSize; + } + + + public void setNotFoundClassResourceCacheSize(int notFoundClassResourceCacheSize) { + this.notFoundClassResourceCacheSize = notFoundClassResourceCacheSize; + } + + @Override public void setCreateUploadTargets(boolean createUploadTargets) { this.createUploadTargets = createUploadTargets; @@ -4258,6 +4270,7 @@ public class StandardContext extends ContainerBase implements Context, Notificat cl.setClearReferencesHttpClientKeepAliveThread(getClearReferencesHttpClientKeepAliveThread()); cl.setClearReferencesThreadLocals(getClearReferencesThreadLocals()); cl.setSkipMemoryLeakChecksOnJvmShutdown(getSkipMemoryLeakChecksOnJvmShutdown()); + cl.setNotFoundClassResourceCacheSize(getNotFoundClassResourceCacheSize()); } // By calling unbindThread and bindThread in a row, we setup the diff --git a/java/org/apache/catalina/loader/WebappClassLoaderBase.java b/java/org/apache/catalina/loader/WebappClassLoaderBase.java index eb41edfeeb..2164378b47 100644 --- a/java/org/apache/catalina/loader/WebappClassLoaderBase.java +++ b/java/org/apache/catalina/loader/WebappClassLoaderBase.java @@ -67,6 +67,7 @@ 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.collections.ConcurrentLruCache; import org.apache.tomcat.util.compat.JreCompat; import org.apache.tomcat.util.res.StringManager; import org.apache.tomcat.util.threads.ThreadPoolExecutor; @@ -311,9 +312,26 @@ public abstract class WebappClassLoaderBase extends URLClassLoader 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. * @@ -684,14 +702,20 @@ public abstract class WebappClassLoaderBase extends URLClassLoader String path = nameToPath(name); - WebResource resource = resources.getClassLoaderResource(path); - if (resource.exists()) { - url = resource.getURL(); - trackLastModified(path, resource); - } + 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 && hasExternalRepositories) { - url = super.findResource(name); + if (url == null) { + notFoundClassResources.add(path); + } } if (log.isTraceEnabled()) { @@ -878,60 +902,64 @@ public abstract class WebappClassLoaderBase extends URLClassLoader log.trace(" Searching local repositories"); } String path = nameToPath(name); - 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.size() > 0) { - // 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 e) { - log.error(sm.getString("webappClassLoader.transformError", name), e); - return null; - } finally { - try { - stream.close(); - } catch (IOException e) { - } - } - byte[] binaryContent = baos.toByteArray(); - String internalName = path.substring(1, path.length() - CLASS_FILE_SUFFIX.length()); - for (ClassFileTransformer transformer : this.transformers) { + 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.size() > 0) { + // If the resource is a class, decorate it with any attached transformers + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int numRead; try { - byte[] transformed = transformer.transform(this, internalName, null, null, binaryContent); - if (transformed != null) { - binaryContent = transformed; + while ((numRead = stream.read(buf)) >= 0) { + baos.write(buf, 0, numRead); } - } catch (IllegalClassFormatException e) { + } catch (IOException e) { log.error(sm.getString("webappClassLoader.transformError", name), e); return null; + } finally { + try { + stream.close(); + } catch (IOException e) { + } + } + 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); } - stream = new ByteArrayInputStream(binaryContent); + trackLastModified(path, resource); } - trackLastModified(path, resource); - } - try { - if (hasExternalRepositories && stream == null) { - URL url = super.findResource(name); - if (url != null) { - stream = url.openStream(); + try { + if (hasExternalRepositories && stream == null) { + URL url = super.findResource(name); + if (url != null) { + stream = url.openStream(); + } } + } catch (IOException e) { + // Ignore } - } catch (IOException e) { - // Ignore - } - if (stream != null) { - if (log.isTraceEnabled()) { - log.trace(" --> Returning stream from local"); + if (stream != null) { + if (log.isTraceEnabled()) { + log.trace(" --> Returning stream from local"); + } + return stream; } - return stream; + + notFoundClassResources.add(path); } // (3) Delegate to parent unconditionally @@ -1263,6 +1291,7 @@ public abstract class WebappClassLoaderBase extends URLClassLoader state = LifecycleState.STOPPING; resourceEntries.clear(); + notFoundClassResources.clear(); jarModificationTimes.clear(); resources = null; @@ -1964,9 +1993,13 @@ public abstract class WebappClassLoaderBase extends URLClassLoader WebResource resource = null; if (entry == null) { + if (notFoundClassResources.contains(path)) { + return null; + } resource = resources.getClassLoaderResource(path); if (!resource.exists()) { + notFoundClassResources.add(path); return null; } @@ -1999,10 +2032,14 @@ public abstract class WebappClassLoaderBase extends URLClassLoader } if (resource == null) { + if (notFoundClassResources.contains(path)) { + return null; + } resource = resources.getClassLoaderResource(path); } if (!resource.exists()) { + notFoundClassResources.add(path); return null; } diff --git a/java/org/apache/tomcat/util/collections/ConcurrentLruCache.java b/java/org/apache/tomcat/util/collections/ConcurrentLruCache.java new file mode 100644 index 0000000000..51e29b969c --- /dev/null +++ b/java/org/apache/tomcat/util/collections/ConcurrentLruCache.java @@ -0,0 +1,119 @@ +/* + * 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.tomcat.util.collections; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class ConcurrentLruCache<T> { + + private volatile LimitedLinkedHashMap<T,T> map; + private final Object lock = new Object(); + + public ConcurrentLruCache(int limit) { + setLimit(limit); + } + + + public void add(T entry) { + if (map == null) { + return; + } + synchronized (lock) { + if (map == null) { + return; + } + map.put(entry, entry); + } + } + + + public boolean contains(T entry) { + if (map == null) { + return false; + } + synchronized (lock) { + if (map == null) { + return false; + } + return map.get(entry) != null; + } + } + + + public void clear() { + if (map == null) { + return; + } + synchronized (lock) { + if (map == null) { + return; + } + map.clear(); + } + } + + + public void setLimit(int limit) { + synchronized (lock) { + if (limit > 0) { + Map<T,T> oldMap = map; + map = new LimitedLinkedHashMap<>(limit); + if (oldMap != null) { + map.putAll(oldMap); + } + } else { + map = null; + } + } + } + + + public int getLimit() { + synchronized (lock) { + if (map == null) { + return -1; + } else { + return map.getLimit(); + } + } + } + + + private static class LimitedLinkedHashMap<K,V> extends LinkedHashMap<K,V> { + private static final long serialVersionUID = 1L; + + private volatile int limit; + + LimitedLinkedHashMap(int limit) { + super(16, 0.75F, true); + this.limit = limit; + } + + @Override + protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { + if (size() > limit) { + return true; + } + return false; + } + + private int getLimit() { + return limit; + } + } +} diff --git a/test/org/apache/tomcat/util/collections/TestConcurrentLruCache.java b/test/org/apache/tomcat/util/collections/TestConcurrentLruCache.java new file mode 100644 index 0000000000..94fa447903 --- /dev/null +++ b/test/org/apache/tomcat/util/collections/TestConcurrentLruCache.java @@ -0,0 +1,96 @@ +/* + * 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.tomcat.util.collections; + +import org.junit.Assert; +import org.junit.Test; + +public class TestConcurrentLruCache { + + @Test + public void testLimit() { + ConcurrentLruCache<String> cache = getStartingCache(); + + cache.add("D"); + + Assert.assertFalse(cache.contains("A")); + Assert.assertTrue(cache.contains("B")); + Assert.assertTrue(cache.contains("C")); + Assert.assertTrue(cache.contains("D")); + } + + + @Test + public void testLRU() { + ConcurrentLruCache<String> cache = getStartingCache(); + + cache.contains("A"); + cache.add("D"); + + Assert.assertTrue(cache.contains("A")); + Assert.assertFalse(cache.contains("B")); + Assert.assertTrue(cache.contains("C")); + Assert.assertTrue(cache.contains("D")); + } + + + @Test + public void testIncreaseLimit() { + ConcurrentLruCache<String> cache = getStartingCache(); + + cache.setLimit(4); + + cache.add("D"); + + Assert.assertTrue(cache.contains("A")); + Assert.assertTrue(cache.contains("B")); + Assert.assertTrue(cache.contains("C")); + Assert.assertTrue(cache.contains("D")); + + cache.add("E"); + + Assert.assertFalse(cache.contains("A")); + Assert.assertTrue(cache.contains("B")); + Assert.assertTrue(cache.contains("C")); + Assert.assertTrue(cache.contains("D")); + Assert.assertTrue(cache.contains("E")); + } + + + @Test + public void testReduceLimit() { + ConcurrentLruCache<String> cache = getStartingCache(); + + cache.contains("A"); + cache.setLimit(2); + + Assert.assertTrue(cache.contains("A")); + Assert.assertFalse(cache.contains("B")); + Assert.assertTrue(cache.contains("C")); + } + + + private ConcurrentLruCache<String> getStartingCache() { + ConcurrentLruCache<String> cache = new ConcurrentLruCache<>(3); + + cache.add("A"); + cache.add("B"); + cache.add("C"); + + return cache; + } +} diff --git a/webapps/docs/config/context.xml b/webapps/docs/config/context.xml index 9e59e39bb9..b141edc8dc 100644 --- a/webapps/docs/config/context.xml +++ b/webapps/docs/config/context.xml @@ -870,6 +870,17 @@ of <code>true</code> will be used.</p> </attribute> + <attribute name="notFoundClassResourceCacheSize" required="false"> + <p>Class resources are not cached by the standard resources + implementation since they are loaded on first use and then 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 + search will take. An LRU cache is used and this attribute controls the + size of that cache. A value of less than 1 disables the cache. If not + specified, the default value of 1000 will be used.</p> + </attribute> + <attribute name="renewThreadsWhenStoppingContext" required="false"> <p>If <code>true</code>, when this context is stopped, Tomcat renews all the threads from the thread pool that was used to serve this context. --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org