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

Reply via email to