From e893f4cf8465bf428b49191158c80060932215fc Mon Sep 17 00:00:00 2001 From: Alan Bateman Date: Sat, 11 Apr 2026 04:49:31 +0000 Subject: [PATCH] 8196182: ServiceLoader.iterator().hasNext()/.next() may throw a LinkageError 8350481: ServiceLoader throws NCDEF instead of ServiceConfigurationError Reviewed-by: liach, jpai --- .../classes/java/util/ServiceLoader.java | 10 +- .../util/ServiceLoader/LinkageErrorsTest.java | 214 ++++++++++++++++++ .../MalformedAnnotationProcessorTests.java | 10 +- 3 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 test/jdk/java/util/ServiceLoader/LinkageErrorsTest.java diff --git a/src/java.base/share/classes/java/util/ServiceLoader.java b/src/java.base/share/classes/java/util/ServiceLoader.java index 5137adc1c08..5e4fa4ed2ef 100644 --- a/src/java.base/share/classes/java/util/ServiceLoader.java +++ b/src/java.base/share/classes/java/util/ServiceLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -621,9 +621,9 @@ public final class ServiceLoader Constructor ctor = null; try { ctor = clazz.getConstructor(); - } catch (NoSuchMethodException ex) { + } catch (NoSuchMethodException | LinkageError e) { String cn = clazz.getName(); - fail(service, cn + " Unable to get public no-arg constructor", ex); + fail(service, cn + " Unable to get public no-arg constructor", e); } if (inExplicitModule(clazz)) ctor.setAccessible(true); @@ -1086,8 +1086,8 @@ public final class ServiceLoader String cn = pending.next(); try { return Class.forName(cn, false, loader); - } catch (ClassNotFoundException x) { - fail(service, "Provider " + cn + " not found"); + } catch (ClassNotFoundException | LinkageError e) { + fail(service, "Provider " + cn + " not found", e); return null; } } diff --git a/test/jdk/java/util/ServiceLoader/LinkageErrorsTest.java b/test/jdk/java/util/ServiceLoader/LinkageErrorsTest.java new file mode 100644 index 00000000000..209dce75267 --- /dev/null +++ b/test/jdk/java/util/ServiceLoader/LinkageErrorsTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8196182 8350481 + * @summary Test ServiceLoader iterating over service providers when linkage error is thrown + * @library /test/lib + * @run junit/othervm ${test.main.class} + */ + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Iterator; +import java.util.Map; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import java.util.function.Consumer; +import java.util.function.Predicate; +import jdk.test.lib.compiler.InMemoryJavaCompiler; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class LinkageErrorsTest { + + /** + * Test iteration over service providers when loading a service provider class + * fails with a linkage error. + */ + @Test + void testLoadClassThrows() throws Exception { + Map sources = Map.of( + "Service", + """ + public interface Service {} + """, + "Super", + """ + class Super {} + """, + "Provider1", + """ + public class Provider1 implements Service { + public Provider1() {} + } + """, + "Provider2", + """ + public class Provider2 extends Super implements Service { + public Provider2() {} + } + """ + ); + Path classesDir = compile(sources); + + // delete Provider2's super class to prevent Provider2 from loading + Files.delete(classesDir.resolve("Super.class")); + + // create services configuration file that lists two providers + createServicesConfigFile(classesDir, "Service", "Provider1", "Provider2"); + + // load the service interface + var loader = new URLClassLoader(new URL[] { classesDir.toUri().toURL() }); + Class service = loader.loadClass("Service"); + assertSame(loader, service.getClassLoader()); + + // find and collect all service providers and ServiceConfigurationErrors + var providers = new ArrayList(); + var errors = new ArrayList(); + forEachProvider(loader, service, providers::add, errors::add); + + // Provider1 should be found + assertEquals(1, providers.size()); + assertEquals("Provider1", providers.get(0).getClass().getName()); + + // loading Provider2 expected to fail with LinkageError + assertEquals(1, errors.size()); + assertInstanceOf(LinkageError.class, errors.get(0).getCause()); + } + + /** + * Test iteration over service providers when finding the public no-arg constructor + * of a provider fails with a linkage error. + */ + @Test + void testFindConstructorThrows() throws Exception { + Map sources = Map.of( + "Service", + """ + public interface Service {} + """, + "Param", + """ + class Param {} + """, + "Provider1", + """ + public class Provider1 implements Service { + public Provider1() {} + } + """, + "Provider2", + """ + public class Provider2 implements Service { + public Provider2() {} + public Provider2(Param p) { } + } + """ + ); + Path classesDir = compile(sources); + + // delete the class file for the parameter of Provider's 1-param ctor + Files.delete(classesDir.resolve("Param.class")); + + // create services configuration file that lists two providers + createServicesConfigFile(classesDir, "Service", "Provider1", "Provider2"); + + // load the service interface + var loader = new URLClassLoader(new URL[] { classesDir.toUri().toURL() }); + Class service = loader.loadClass("Service"); + assertSame(loader, service.getClassLoader()); + + // find and collect all service providers and ServiceConfigurationErrors + var providers = new ArrayList(); + var errors = new ArrayList(); + forEachProvider(loader, service, providers::add, errors::add); + + // Provider1 should be found + assertEquals(1, providers.size()); + assertEquals("Provider1", providers.get(0).getClass().getName()); + + // loading Provider2 expected to fail with LinkageError + assertEquals(1, errors.size()); + assertInstanceOf(LinkageError.class, errors.get(0).getCause()); + } + + /** + * Compile the given java source files to a temporary directory. + */ + private Path compile(Map sources) throws IOException { + Map classes = InMemoryJavaCompiler.compile(sources); + Path dir = Files.createTempDirectory(Path.of("."), "classes"); + for (String cn : classes.keySet()) { + Path file = dir.resolve(cn.replace('.', File.separatorChar) + ".class"); + Files.createDirectories(file.getParent()); + Files.write(file, classes.get(cn)); + } + return dir; + } + + /** + * Create services configuration file for the given service and providers. + */ + private void createServicesConfigFile(Path dir, + String serviceName, + String... providerNames) throws IOException { + Path configFile = dir.resolve("META-INF", "services", serviceName); + Files.createDirectories(configFile.getParent()); + Files.write(configFile, Arrays.asList(providerNames)); + } + + /** + * Uses ServiceLoader to iterate over all service providers of the given service, + * invoking {@code providerConsumer} for each provider instantiated, and + * {@code errorConsumer} for each ServiceConfigurationError encountered. + */ + private void forEachProvider(ClassLoader loader, + Class service, + Consumer providerConsumer, + Consumer errorConsumer) { + Iterator iterator = ServiceLoader.load(service, loader).iterator(); + boolean done = false; + while (!done) { + try { + if (iterator.hasNext()) { + T provider = iterator.next(); + providerConsumer.accept(provider); + } else { + done = true; + } + } catch (ServiceConfigurationError e) { + errorConsumer.accept(e); + } + } + } +} diff --git a/test/langtools/tools/javac/annotations/8218152/MalformedAnnotationProcessorTests.java b/test/langtools/tools/javac/annotations/8218152/MalformedAnnotationProcessorTests.java index 68e4aea0ab2..85095cc5537 100644 --- a/test/langtools/tools/javac/annotations/8218152/MalformedAnnotationProcessorTests.java +++ b/test/langtools/tools/javac/annotations/8218152/MalformedAnnotationProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -91,8 +91,8 @@ public class MalformedAnnotationProcessorTests extends TestRunner{ .getOutputLines(Task.OutputKind.DIRECT); System.out.println(actualErrors.get(0)); - if (!actualErrors.get(0).contains("- compiler.err.proc.cant.load.class: " + - "Incompatible magic value")) { + if (!actualErrors.get(0).contains("- compiler.err.proc.bad.config.file: " + + "javax.annotation.processing.Processor: Provider BadAnnoProcessor not found")) { throw new AssertionError("Unexpected errors reported: " + actualErrors); } } @@ -162,8 +162,8 @@ public class MalformedAnnotationProcessorTests extends TestRunner{ .writeAll() .getOutputLines(Task.OutputKind.DIRECT); - if (!actualErrors.get(0).contains("- compiler.err.proc.cant.load.class: " + - "WrongClassFileVersion has been compiled by a more recent version")) { + if (!actualErrors.get(0).contains("- compiler.err.proc.bad.config.file: " + + "javax.annotation.processing.Processor: Provider WrongClassFileVersion not found")) { throw new AssertionError("Unexpected errors reported: " + actualErrors); } }