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