You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2020/04/12 16:11:29 UTC

[tomcat] branch 9.0.x updated: Rework the fix for BZ 64021 for better custom class loader support

This is an automated email from the ASF dual-hosted git repository.

markt pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/9.0.x by this push:
     new 47edccf  Rework the fix for BZ 64021 for better custom class loader support
47edccf is described below

commit 47edccfc0f4d3713c9fb8843e316a08823cb2cbb
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Sun Apr 12 17:05:35 2020 +0100

    Rework the fix for BZ 64021 for better custom class loader support
    
    https://bz.apache.org/bugzilla/show_bug.cgi?id=64021
    Better support custom class loaders that load resources from
    non-standard locations and do not utilise the WebResources
    implementation.
---
 .../catalina/startup/WebappServiceLoader.java      | 119 +++++++++++++--------
 .../catalina/startup/TestWebappServiceLoader.java  |  10 +-
 webapps/docs/changelog.xml                         |   5 +
 3 files changed, 86 insertions(+), 48 deletions(-)

diff --git a/java/org/apache/catalina/startup/WebappServiceLoader.java b/java/org/apache/catalina/startup/WebappServiceLoader.java
index 0d0bc7a..73394ac 100644
--- a/java/org/apache/catalina/startup/WebappServiceLoader.java
+++ b/java/org/apache/catalina/startup/WebappServiceLoader.java
@@ -26,15 +26,16 @@ import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Enumeration;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.regex.Pattern;
 
 import javax.servlet.ServletContext;
 
 import org.apache.catalina.Context;
-import org.apache.catalina.WebResource;
 import org.apache.tomcat.util.scan.JarFactory;
 
 /**
@@ -67,6 +68,7 @@ public class WebappServiceLoader<T> {
     private final ServletContext servletContext;
     private final Pattern containerSciFilterPattern;
 
+
     /**
      * Construct a loader to load services from a ServletContext.
      *
@@ -83,8 +85,14 @@ public class WebappServiceLoader<T> {
         }
     }
 
+
     /**
-     * Load the providers for a service type.
+     * Load the providers for a service type. Container defined services will be
+     * loaded before application defined services in case the application
+     * depends on a Container provided service. Note that services are always
+     * loaded via the Context (web application) class loader so it is possible
+     * for an application to provide an alternative implementation of what would
+     * normally be a Container provided service.
      *
      * @param serviceType the type of service to load
      * @return an unmodifiable collection of service providers
@@ -93,21 +101,64 @@ public class WebappServiceLoader<T> {
     public List<T> load(Class<T> serviceType) throws IOException {
         String configFile = SERVICES + serviceType.getName();
 
-        LinkedHashSet<String> applicationServicesFound = new LinkedHashSet<>();
-        LinkedHashSet<String> containerServicesFound = new LinkedHashSet<>();
+        // Obtain the Container provided service configuration files.
+        ClassLoader loader = context.getParentClassLoader();
+        Enumeration<URL> containerResources;
+        if (loader == null) {
+            containerResources = ClassLoader.getSystemResources(configFile);
+        } else {
+            containerResources = loader.getResources(configFile);
+        }
+
+        // Extract the Container provided service class names. Each
+        // configuration file may list more than one service class name. This
+        // uses a LinkedHashSet so if a service class name appears more than
+        // once in the configuration files, only the first one found is used.
+        LinkedHashSet<String> containerServiceClassNames = new LinkedHashSet<>();
+        Set<URL> containerServiceConfigFiles = new HashSet<>();
+        while (containerResources.hasMoreElements()) {
+            URL containerServiceConfigFile = containerResources.nextElement();
+            containerServiceConfigFiles.add(containerServiceConfigFile);
+            parseConfigFile(containerServiceClassNames, containerServiceConfigFile);
+        }
+
+        // Filter the discovered container SCIs if required
+        if (containerSciFilterPattern != null) {
+            Iterator<String> iter = containerServiceClassNames.iterator();
+            while (iter.hasNext()) {
+                if (containerSciFilterPattern.matcher(iter.next()).find()) {
+                    iter.remove();
+                }
+            }
+        }
 
-        // if the ServletContext has ORDERED_LIBS, then use that to specify the
-        // set of JARs from WEB-INF/lib that should be used for loading services
+        // Obtaining the application provided configuration files is a little
+        // more difficult for two reasons:
+        // - The web application may employ a custom class loader. Ideally, we
+        //   would use ClassLoader.findResources() but that method is protected.
+        //   We could force custom class loaders to override that method and
+        //   make it public but that would be a new requirement and break
+        //   backwards compatibility for what is an often customised component.
+        // - If the application web.xml file has defined an order for fragments
+        //   then only those JAR files represented by fragments in that order
+        //   (and arguably WEB-INF/classes) should be scanned for services.
+        LinkedHashSet<String> applicationServiceClassNames = new LinkedHashSet<>();
+
+        // Check to see if the ServletContext has ORDERED_LIBS defined
         @SuppressWarnings("unchecked")
         List<String> orderedLibs = (List<String>) servletContext.getAttribute(ServletContext.ORDERED_LIBS);
 
-        // Handle application SCIs directly...
+        // Obtain the application provided service configuration files
         if (orderedLibs == null) {
-            // No ordered libs, so use every service definition we can find
-            WebResource[] resources = context.getResources().getClassLoaderResources("/" + configFile);
-            for (WebResource resource : resources) {
-                if (resource.isFile()) {
-                    parseConfigFile(applicationServicesFound, resource.getURL());
+            // Because a custom class loader may be being used, we have to use
+            // getResources() which will return application and Container files.
+            Enumeration<URL> allResources = servletContext.getClassLoader().getResources(configFile);
+            while (allResources.hasMoreElements()) {
+                URL serviceConfigFile = allResources.nextElement();
+                // Only process the service configuration file if it is not a
+                // Container level file that has already been processed
+                if (!containerServiceConfigFiles.contains(serviceConfigFile)) {
+                    parseConfigFile(applicationServiceClassNames, serviceConfigFile);
                 }
             }
         } else {
@@ -115,7 +166,7 @@ public class WebappServiceLoader<T> {
             // in WEB-INF/classes
             URL unpacked = servletContext.getResource(CLASSES + configFile);
             if (unpacked != null) {
-                parseConfigFile(applicationServicesFound, unpacked);
+                parseConfigFile(applicationServiceClassNames, unpacked);
             }
 
             for (String lib : orderedLibs) {
@@ -133,49 +184,27 @@ public class WebappServiceLoader<T> {
                     url = JarFactory.getJarEntryURL(jarUrl, configFile);
                 }
                 try {
-                    parseConfigFile(applicationServicesFound, url);
+                    parseConfigFile(applicationServiceClassNames, url);
                 } catch (FileNotFoundException e) {
                     // no provider file found, this is OK
                 }
             }
         }
 
-        // and use the parent ClassLoader for all other SCIs
-        ClassLoader loader = context.getParentClassLoader();
-
-        Enumeration<URL> resources;
-        if (loader == null) {
-            resources = ClassLoader.getSystemResources(configFile);
-        } else {
-            resources = loader.getResources(configFile);
-        }
-        while (resources.hasMoreElements()) {
-            parseConfigFile(containerServicesFound, resources.nextElement());
-        }
-
-        // Filter the discovered container SCIs if required
-        if (containerSciFilterPattern != null) {
-            Iterator<String> iter = containerServicesFound.iterator();
-            while (iter.hasNext()) {
-                if (containerSciFilterPattern.matcher(iter.next()).find()) {
-                    iter.remove();
-                }
-            }
-        }
-
         // Add the application services after the container services to ensure
         // that the container services are loaded first
-        containerServicesFound.addAll(applicationServicesFound);
+        containerServiceClassNames.addAll(applicationServiceClassNames);
 
-        // load the discovered services
-        if (containerServicesFound.isEmpty()) {
+        // Short-cut if no services have been found
+        if (containerServiceClassNames.isEmpty()) {
             return Collections.emptyList();
         }
-        return loadServices(serviceType, containerServicesFound);
+        // Load the discovered services
+        return loadServices(serviceType, containerServiceClassNames);
     }
 
-    void parseConfigFile(LinkedHashSet<String> servicesFound, URL url)
-            throws IOException {
+
+    void parseConfigFile(LinkedHashSet<String> servicesFound, URL url) throws IOException {
         try (InputStream is = url.openStream();
             InputStreamReader in = new InputStreamReader(is, StandardCharsets.UTF_8);
             BufferedReader reader = new BufferedReader(in)) {
@@ -194,8 +223,8 @@ public class WebappServiceLoader<T> {
         }
     }
 
-    List<T> loadServices(Class<T> serviceType, LinkedHashSet<String> servicesFound)
-            throws IOException {
+
+    List<T> loadServices(Class<T> serviceType, LinkedHashSet<String> servicesFound) throws IOException {
         ClassLoader loader = servletContext.getClassLoader();
         List<T> services = new ArrayList<>(servicesFound.size());
         for (String serviceClass : servicesFound) {
diff --git a/test/org/apache/catalina/startup/TestWebappServiceLoader.java b/test/org/apache/catalina/startup/TestWebappServiceLoader.java
index 6a0f18d..9a5d788 100644
--- a/test/org/apache/catalina/startup/TestWebappServiceLoader.java
+++ b/test/org/apache/catalina/startup/TestWebappServiceLoader.java
@@ -65,6 +65,8 @@ public class TestWebappServiceLoader {
     @Test
     public void testNoInitializersFound() throws IOException {
         loader = new WebappServiceLoader<>(context);
+        EasyMock.expect(cl.getResources(CONFIG_FILE))
+                .andReturn(Collections.<URL>emptyEnumeration());
         EasyMock.expect(servletContext.getAttribute(ServletContext.ORDERED_LIBS))
                 .andReturn(null);
         EasyMock.expect(cl.getResources(CONFIG_FILE))
@@ -81,11 +83,13 @@ public class TestWebappServiceLoader {
         loader = EasyMock.createMockBuilder(WebappServiceLoader.class)
                 .addMockedMethod("parseConfigFile", LinkedHashSet.class, URL.class)
                 .withConstructor(context).createMock(control);
+        EasyMock.expect(cl.getResources(CONFIG_FILE))
+                .andReturn(Collections.enumeration(Collections.singleton(url)));
+        loader.parseConfigFile(EasyMock.isA(LinkedHashSet.class), EasyMock.same(url));
         EasyMock.expect(servletContext.getAttribute(ServletContext.ORDERED_LIBS))
                 .andReturn(null);
         EasyMock.expect(cl.getResources(CONFIG_FILE))
                 .andReturn(Collections.enumeration(Collections.singleton(url)));
-        loader.parseConfigFile(EasyMock.isA(LinkedHashSet.class), EasyMock.same(url));
         control.replay();
         Assert.assertTrue(loader.load(ServletContainerInitializer.class).isEmpty());
         control.verify();
@@ -102,6 +106,8 @@ public class TestWebappServiceLoader {
                 .addMockedMethod("parseConfigFile", LinkedHashSet.class, URL.class)
                 .withConstructor(context).createMock(control);
         List<String> jars = Arrays.asList("jar1.jar", "dir/");
+        EasyMock.expect(parent.getResources(CONFIG_FILE))
+                .andReturn(Collections.<URL>emptyEnumeration());
         EasyMock.expect(servletContext.getAttribute(ServletContext.ORDERED_LIBS))
                 .andReturn(jars);
         EasyMock.expect(servletContext.getResource("/WEB-INF/classes/" + CONFIG_FILE))
@@ -112,8 +118,6 @@ public class TestWebappServiceLoader {
         EasyMock.expect(servletContext.getResource("/WEB-INF/lib/dir/"))
                 .andReturn(url2);
         loader.parseConfigFile(EasyMock.isA(LinkedHashSet.class), EasyMock.eq(sci2));
-        EasyMock.expect(parent.getResources(CONFIG_FILE))
-                .andReturn(Collections.<URL>emptyEnumeration());
 
         control.replay();
         Assert.assertTrue(loader.load(ServletContainerInitializer.class).isEmpty());
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index d84ab31..b9f8891 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -51,6 +51,11 @@
         Reduce reflection use and remove AJP specific code in the Connector.
         (remm/markt/fhanik)
       </fix>
+      <fix>
+        Rework the fix for <bug>64021</bug> to better support web applications
+        that use a custom class loader that loads resources from non-standard
+        locations. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org