On Fri, Oct 25, 2024 at 10:55 PM <ma...@apache.org> wrote: > > 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.
I know that, this existed a long time ago: https://github.com/apache/tomcat/blob/7.0.x/java/org/apache/catalina/loader/WebappClassLoaderBase.java#L341 It was quite significant then since everything was much slower. Rémy > --- > 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 > --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org