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

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


The following commit(s) were added to refs/heads/10.1.x by this push:
     new 0a7a97ec60 Review "Ignore unusable JULI Log service providers instead 
of failing"
0a7a97ec60 is described below

commit 0a7a97ec6072198dae1eb5e78b64a2d86faf45e7
Author: Mark Thomas <[email protected]>
AuthorDate: Fri Nov 6 15:42:33 2026 +0000

    Review "Ignore unusable JULI Log service providers instead of failing"
    
    Handle some additional cases of unusable service providers
    
    Co-authored by: Codex / GPT-6.1-Sol
---
 java/org/apache/juli/logging/LogFactory.java       |  63 ++++++-
 test/org/apache/juli/logging/TestLogFactory.java   | 194 +++++++++++++++++++--
 .../logging/provider/TesterLogFactoryProvider.java |  44 +++++
 3 files changed, 278 insertions(+), 23 deletions(-)

diff --git a/java/org/apache/juli/logging/LogFactory.java 
b/java/org/apache/juli/logging/LogFactory.java
index 285b5672f0..a299195374 100644
--- a/java/org/apache/juli/logging/LogFactory.java
+++ b/java/org/apache/juli/logging/LogFactory.java
@@ -16,8 +16,13 @@
  */
 package org.apache.juli.logging;
 
+import java.io.IOException;
 import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.net.URL;
 import java.nio.file.FileSystems;
+import java.util.Collections;
+import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.ServiceConfigurationError;
 import java.util.ServiceLoader;
@@ -75,26 +80,70 @@ public class LogFactory {
         // constructor taking the String name. Providers that cannot be
         // loaded or that do not expose the required constructor are
         // ignored, so a malformed provider does not prevent the discovery
-        // of a usable one, nor the fall back to DirectJDKLog. Note that,
-        // as the ServiceLoader requires, a provider also needs a public
-        // no-argument constructor to be considered at all.
-        ServiceLoader<Log> logLoader = ServiceLoader.load(Log.class);
+        // of a usable one, nor the fall back to DirectJDKLog. Class-path
+        // providers need a public no-argument constructor; named-module
+        // providers may instead use a factory method.
+        ServiceLoader<Log> logLoader = ServiceLoader.load(Log.class,
+                new 
ServiceLoaderClassLoader(Thread.currentThread().getContextClassLoader()));
         Constructor<? extends Log> m = null;
         Iterator<ServiceLoader.Provider<Log>> providers = 
logLoader.stream().iterator();
         while (m == null) {
+            ServiceLoader.Provider<Log> provider;
             try {
                 if (!providers.hasNext()) {
                     break;
                 }
-                m = providers.next().type().getConstructor(String.class);
-            } catch (ReflectiveOperationException | SecurityException | 
ServiceConfigurationError e) {
-                // Not a usable provider. Ignore it and look at the next one.
+                provider = providers.next();
+            } catch (ServiceConfigurationError | SecurityException | 
LinkageError e) {
+                // An individual service file or provider may be unusable. The 
resource enumeration has already
+                // advanced past it, so continue with the next file or 
provider.
+                continue;
+            }
+            try {
+                Class<? extends Log> type = provider.type();
+                if (type.getModule().isNamed()) {
+                    // For a factory-method provider, type() is the method's 
declared return type, which may be
+                    // the Log interface or a base class rather than the 
implementation whose constructor we need.
+                    type = provider.get().getClass();
+                }
+                Constructor<? extends Log> candidate = 
type.getConstructor(String.class);
+                // Instances are created later, by reflection from this class. 
A provider that is not accessible
+                // to it would fail every call of getInstance(), so treat it 
as unusable now.
+                if (Modifier.isPublic(type.getModifiers()) &&
+                        type.getModule().isExported(type.getPackageName(), 
LogFactory.class.getModule())) {
+                    m = candidate;
+                }
+            } catch (ReflectiveOperationException | SecurityException | 
ServiceConfigurationError | LinkageError e) {
+                // Not a usable provider, for example because a type it refers 
to is not available. Ignore it and
+                // look at the next one.
             }
         }
         discoveredLogConstructor = m;
     }
 
 
+    /**
+     * Delegate service discovery to the context class loader, but turn a 
failed resource lookup into an empty
+     * enumeration. Unlike a failure reading an individual service file, a 
failure obtaining the enumeration leaves
+     * ServiceLoader unable to advance, so retrying its iterator would repeat 
the same lookup indefinitely.
+     */
+    private static class ServiceLoaderClassLoader extends ClassLoader {
+
+        private ServiceLoaderClassLoader(ClassLoader parent) {
+            super(parent == null ? ClassLoader.getSystemClassLoader() : 
parent);
+        }
+
+        @Override
+        public Enumeration<URL> getResources(String name) {
+            try {
+                return super.getResources(name);
+            } catch (IOException | SecurityException | LinkageError e) {
+                return Collections.emptyEnumeration();
+            }
+        }
+    }
+
+
     // --------------------------------------------------------- Public Methods
 
     // only those 2 methods need to change to use a different direct logger.
diff --git a/test/org/apache/juli/logging/TestLogFactory.java 
b/test/org/apache/juli/logging/TestLogFactory.java
index 69d595ff79..9920f45b4e 100644
--- a/test/org/apache/juli/logging/TestLogFactory.java
+++ b/test/org/apache/juli/logging/TestLogFactory.java
@@ -17,15 +17,28 @@
 package org.apache.juli.logging;
 
 import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.module.Configuration;
+import java.lang.module.ModuleFinder;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Set;
+
+import javax.tools.JavaCompiler;
+import javax.tools.ToolProvider;
 
 import org.junit.Assert;
 import org.junit.Test;
 
+import org.apache.juli.logging.provider.TesterLogFactoryProvider;
+
 public class TestLogFactory {
 
     private static final String NO_STRING_CONSTRUCTOR = 
TesterLogNoStringConstructor.class.getName();
@@ -101,6 +114,150 @@ public class TestLogFactory {
     }
 
 
+    @Test
+    public void testResourceLookupFailureStopsDiscovery() throws Exception {
+        doTestResourceLookupFailureStopsDiscovery(false);
+    }
+
+
+    @Test
+    public void testDeniedResourceLookupStopsDiscovery() throws Exception {
+        doTestResourceLookupFailureStopsDiscovery(true);
+    }
+
+
+    private void doTestResourceLookupFailureStopsDiscovery(boolean denied) 
throws Exception {
+        URL[] urls = { codeSourceOf(LogFactory.class), 
codeSourceOf(TestLogFactory.class) };
+        int[] lookups = new int[1];
+        try (URLClassLoader loader = new URLClassLoader(urls, 
ClassLoader.getPlatformClassLoader()) {
+            @Override
+            public Enumeration<URL> getResources(String name) throws 
IOException {
+                if (name.equals("META-INF/services/" + Log.class.getName())) {
+                    // Fail a repeated lookup immediately rather than letting 
a broken implementation loop forever.
+                    Assert.assertEquals("The failing resource enumeration was 
retried", 1, ++lookups[0]);
+                    if (denied) {
+                        throw new SecurityException("Test resource lookup 
denial");
+                    }
+                    throw new IOException("Test resource lookup failure");
+                }
+                return super.getResources(name);
+            }
+        }) {
+            Assert.assertEquals(DIRECT_JDK_LOG, 
getLogFromFactory(loader).getClass().getName());
+            Assert.assertEquals(1, lookups[0]);
+        }
+    }
+
+
+    @Test
+    public void testUnreadableServiceFilesDoNotHideUsableProvider() throws 
Exception {
+        Path tempDir = 
Files.createTempDirectory("juli-logfactory-unreadable-test");
+        try {
+            Path service = tempDir.resolve("readable.services");
+            Files.writeString(service, STRING_CONSTRUCTOR + 
System.lineSeparator(), StandardCharsets.UTF_8);
+            List<URL> resources = 
List.of(tempDir.resolve("missing.services").toUri().toURL(),
+                    tempDir.resolve("also-missing.services").toUri().toURL(), 
service.toUri().toURL());
+            URL[] urls = { codeSourceOf(LogFactory.class), 
codeSourceOf(TestLogFactory.class) };
+            try (URLClassLoader loader = new URLClassLoader(urls, 
ClassLoader.getPlatformClassLoader()) {
+                @Override
+                public Enumeration<URL> getResources(String name) throws 
IOException {
+                    if (name.equals("META-INF/services/" + 
Log.class.getName())) {
+                        return Collections.enumeration(resources);
+                    }
+                    return super.getResources(name);
+                }
+            }) {
+                Assert.assertEquals(STRING_CONSTRUCTOR, 
getLogFromFactory(loader).getClass().getName());
+            }
+        } finally {
+            deleteRecursively(tempDir.toFile());
+        }
+    }
+
+
+    @Test
+    public void testModuleFactoryProviderIsDiscovered() throws Exception {
+        doTestModuleFactoryProvider(TesterLogFactoryProvider.class, true);
+    }
+
+
+    @Test
+    public void testModuleFactoryProviderReturningBaseClassIsDiscovered() 
throws Exception {
+        
doTestModuleFactoryProvider(TesterLogFactoryProvider.BaseFactory.class, true);
+    }
+
+
+    /*
+     * A provider whose package is not exported to the logging module cannot 
be instantiated by it, so it must be
+     * ignored at discovery rather than make every later call to getLog() fail.
+     */
+    @Test
+    public void testModuleProviderInUnexportedPackageIsIgnored() throws 
Exception {
+        doTestModuleFactoryProvider(TesterLogFactoryProvider.class, false);
+    }
+
+
+    private void doTestModuleFactoryProvider(Class<?> providerClass, boolean 
exported) throws Exception {
+        Path tempDir = 
Files.createTempDirectory("juli-logfactory-module-test");
+        try {
+            Path modules = tempDir.resolve("modules");
+            Path juli = modules.resolve("test.juli");
+            copyClasses(juli, Log.class, LogFactory.class, 
LogConfigurationException.class, DirectJDKLog.class,
+                    TesterLogStringConstructor.class);
+            copyClasses(juli, LogFactory.class.getDeclaredClasses());
+            compileModule(tempDir.resolve("juli-src"), juli, "test.juli",
+                    "module test.juli { requires java.logging; exports 
org.apache.juli.logging; " +
+                            "uses org.apache.juli.logging.Log; }", modules);
+
+            Path provider = modules.resolve("test.provider");
+            copyClasses(provider, TesterLogFactoryProvider.class, 
TesterLogFactoryProvider.NamedLog.class,
+                    TesterLogFactoryProvider.BaseFactory.class);
+            compileModule(tempDir.resolve("provider-src"), provider, 
"test.provider",
+                    "module test.provider { requires test.juli; " +
+                            (exported ? "exports 
org.apache.juli.logging.provider; " : "") +
+                            "provides org.apache.juli.logging.Log with " + 
providerClass.getCanonicalName() +
+                            "; }", modules);
+
+            ModuleFinder finder = ModuleFinder.of(modules);
+            Configuration configuration = 
ModuleLayer.boot().configuration().resolve(finder, ModuleFinder.of(),
+                    Set.of("test.provider"));
+            ModuleLayer layer = 
ModuleLayer.boot().defineModulesWithOneLoader(configuration,
+                    ClassLoader.getPlatformClassLoader());
+            Object log = getLogFromFactory(layer.findLoader("test.provider"));
+            Assert.assertEquals(exported ? 
TesterLogFactoryProvider.NamedLog.class.getName() : DIRECT_JDK_LOG,
+                    log.getClass().getName());
+        } finally {
+            deleteRecursively(tempDir.toFile());
+        }
+    }
+
+
+    private static void copyClasses(Path directory, Class<?>... classes) 
throws IOException {
+        for (Class<?> clazz : classes) {
+            String resource = clazz.getName().replace('.', '/') + ".class";
+            Path target = directory.resolve(resource);
+            Files.createDirectories(target.getParent());
+            try (InputStream input = clazz.getResourceAsStream('/' + 
resource)) {
+                Assert.assertNotNull(resource, input);
+                Files.copy(input, target);
+            }
+        }
+    }
+
+
+    private static void compileModule(Path sources, Path output, String name, 
String descriptor, Path modules)
+            throws IOException {
+        Files.createDirectories(sources);
+        Path source = sources.resolve("module-info.java");
+        Files.writeString(source, descriptor, StandardCharsets.UTF_8);
+        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+        Assert.assertNotNull("A JDK compiler is required", compiler);
+        Assert.assertEquals("Module compilation failed", 0, compiler.run(null, 
null, null,
+                "--module-path", modules.toString(), "--patch-module", name + 
'=' + output,
+                "-d", output.toString(), source.toString()));
+    }
+
+
     /*
      * Loads a fresh copy of the LogFactory in an isolated class loader in 
which the given
      * names are registered as the only providers of the Log service, calls 
getLog() twice
@@ -125,22 +282,7 @@ public class TestLogFactory {
             URL[] urls = { codeSourceOf(LogFactory.class), 
codeSourceOf(TestLogFactory.class),
                     tempDir.toUri().toURL() };
             try (URLClassLoader isolatedLoader = new URLClassLoader(urls, 
ClassLoader.getPlatformClassLoader())) {
-                Thread currentThread = Thread.currentThread();
-                ClassLoader originalLoader = 
currentThread.getContextClassLoader();
-                // The ServiceLoader used by LogFactory looks at the context 
class loader
-                currentThread.setContextClassLoader(isolatedLoader);
-                try {
-                    Class<?> isolatedFactory = 
isolatedLoader.loadClass(LogFactory.class.getName());
-                    Object result = null;
-                    // Call twice, the second call must not fail with 
NoClassDefFoundError
-                    for (int i = 0; i < 2; i++) {
-                        result = isolatedFactory.getMethod("getLog", 
String.class).invoke(null, "tester");
-                        Assert.assertNotNull(result);
-                    }
-                    return result;
-                } finally {
-                    currentThread.setContextClassLoader(originalLoader);
-                }
+                return getLogFromFactory(isolatedLoader);
             }
         } finally {
             deleteRecursively(tempDir.toFile());
@@ -148,6 +290,26 @@ public class TestLogFactory {
     }
 
 
+    private static Object getLogFromFactory(ClassLoader loader) throws 
Exception {
+        Thread currentThread = Thread.currentThread();
+        ClassLoader originalLoader = currentThread.getContextClassLoader();
+        // The ServiceLoader used by LogFactory looks at the context class 
loader.
+        currentThread.setContextClassLoader(loader);
+        try {
+            Class<?> isolatedFactory = 
loader.loadClass(LogFactory.class.getName());
+            Object result = null;
+            // Call twice, the second call must not fail with 
NoClassDefFoundError.
+            for (int i = 0; i < 2; i++) {
+                result = isolatedFactory.getMethod("getLog", 
String.class).invoke(null, "tester");
+                Assert.assertNotNull(result);
+            }
+            return result;
+        } finally {
+            currentThread.setContextClassLoader(originalLoader);
+        }
+    }
+
+
     private static URL codeSourceOf(Class<?> clazz) throws Exception {
         return clazz.getProtectionDomain().getCodeSource().getLocation();
     }
diff --git 
a/test/org/apache/juli/logging/provider/TesterLogFactoryProvider.java 
b/test/org/apache/juli/logging/provider/TesterLogFactoryProvider.java
new file mode 100644
index 0000000000..8574e246e1
--- /dev/null
+++ b/test/org/apache/juli/logging/provider/TesterLogFactoryProvider.java
@@ -0,0 +1,44 @@
+/*
+ * 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.juli.logging.provider;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.TesterLogStringConstructor;
+
+/**
+ * Named-module service provider whose factory declares the service interface 
as its return type.
+ */
+public class TesterLogFactoryProvider {
+
+    public static Log provider() {
+        return new NamedLog("initial");
+    }
+
+    public static class BaseFactory {
+
+        public static TesterLogStringConstructor provider() {
+            return new NamedLog("initial");
+        }
+    }
+
+    public static class NamedLog extends TesterLogStringConstructor {
+
+        public NamedLog(String name) {
+            super(name);
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to