From edd671146dace3fc9c4eeefae3f8920353d9fc60 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Fri, 13 Mar 2020 16:59:33 +1100 Subject: [PATCH] Issue #4340 - ServiceLoader stream which doesn't break if hasNext throws Signed-off-by: Lachlan Roberts --- .../client/ALPNClientConnectionFactory.java | 2 +- .../server/ALPNServerConnectionFactory.java | 2 +- .../annotations/AnnotationConfiguration.java | 2 +- .../jetty/http/PreEncodedHttpField.java | 2 +- .../jetty/security/SecurityHandler.java | 2 +- .../jetty/util/ServiceLoaderSpliterator.java | 103 ++++++++++++++++++ .../java/org/eclipse/jetty/util/TypeUtil.java | 14 +++ .../jetty/util/security/Credential.java | 2 +- .../eclipse/jetty/webapp/Configurations.java | 2 +- .../eclipse/jetty/xml/XmlConfiguration.java | 4 +- 10 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 jetty-util/src/main/java/org/eclipse/jetty/util/ServiceLoaderSpliterator.java diff --git a/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java b/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java index f93b118515e6..176ff17ccdc1 100644 --- a/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java +++ b/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java @@ -54,7 +54,7 @@ public ALPNClientConnectionFactory(Executor executor, ClientConnectionFactory co IllegalStateException failure = new IllegalStateException("No Client ALPNProcessors!"); // Use a for loop on iterator so load exceptions can be caught and ignored - ServiceLoader.load(Client.class).stream().flatMap(TypeUtil::providerMap).forEach((processor) -> + TypeUtil.serviceLoaderStream(ServiceLoader.load(Client.class)).flatMap(TypeUtil::providerMap).forEach((processor) -> { try { diff --git a/jetty-alpn/jetty-alpn-server/src/main/java/org/eclipse/jetty/alpn/server/ALPNServerConnectionFactory.java b/jetty-alpn/jetty-alpn-server/src/main/java/org/eclipse/jetty/alpn/server/ALPNServerConnectionFactory.java index f612ed02d0c4..fefc8b39b803 100644 --- a/jetty-alpn/jetty-alpn-server/src/main/java/org/eclipse/jetty/alpn/server/ALPNServerConnectionFactory.java +++ b/jetty-alpn/jetty-alpn-server/src/main/java/org/eclipse/jetty/alpn/server/ALPNServerConnectionFactory.java @@ -51,7 +51,7 @@ public ALPNServerConnectionFactory(@Name("protocols") String... protocols) IllegalStateException failure = new IllegalStateException("No Server ALPNProcessors!"); // Use a for loop on iterator so load exceptions can be caught and ignored - ServiceLoader.load(Server.class).stream().flatMap(TypeUtil::providerMap).forEach((processor) -> + TypeUtil.serviceLoaderStream(ServiceLoader.load(Server.class)).flatMap(TypeUtil::providerMap).forEach((processor) -> { try { diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationConfiguration.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationConfiguration.java index 0210c509840b..76ebb3826f14 100644 --- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationConfiguration.java +++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationConfiguration.java @@ -812,7 +812,7 @@ public List getNonExcludedInitializers(WebAppContex long start = 0; if (LOG.isDebugEnabled()) start = System.nanoTime(); - List scis = ServiceLoader.load(ServletContainerInitializer.class).stream() + List scis = TypeUtil.serviceLoaderStream(ServiceLoader.load(ServletContainerInitializer.class)) .flatMap(TypeUtil::providerMap) .collect(Collectors.toList()); if (LOG.isDebugEnabled()) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java b/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java index 36a48a82138f..638c1c75a644 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java @@ -43,7 +43,7 @@ public class PreEncodedHttpField extends HttpField static { - List encoders = ServiceLoader.load(HttpFieldPreEncoder.class).stream() + List encoders = TypeUtil.serviceLoaderStream(ServiceLoader.load(HttpFieldPreEncoder.class)) .flatMap(TypeUtil::providerMap) .filter(encoder -> index(encoder.getHttpVersion()) >= 0) .collect(Collectors.toList()); diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java b/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java index c1b3a84064f1..2c30c28df6df 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java @@ -77,7 +77,7 @@ public abstract class SecurityHandler extends HandlerWrapper implements Authenti static { - ServiceLoader.load(Authenticator.Factory.class).stream() + TypeUtil.serviceLoaderStream(ServiceLoader.load(Authenticator.Factory.class)) .flatMap(TypeUtil::providerMap) .forEach(__knownAuthenticatorFactories::add); __knownAuthenticatorFactories.add(new DefaultAuthenticatorFactory()); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ServiceLoaderSpliterator.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ServiceLoaderSpliterator.java new file mode 100644 index 000000000000..f5508bee1e1f --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ServiceLoaderSpliterator.java @@ -0,0 +1,103 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import java.util.Iterator; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import java.util.Spliterator; +import java.util.function.Consumer; + +class ServiceLoaderSpliterator implements Spliterator> +{ + private final Iterator iterator; + + public ServiceLoaderSpliterator(ServiceLoader serviceLoader) + { + iterator = serviceLoader.iterator(); + } + + @Override + public boolean tryAdvance(Consumer> action) + { + Provider next = new Provider<>(); + try + { + if (!iterator.hasNext()) + return false; + next.setServiceProvider(iterator.next()); + } + catch (Throwable t) + { + next.setError(t); + } + + action.accept(next); + return true; + } + + @Override + public Spliterator> trySplit() + { + return null; + } + + @Override + public long estimateSize() + { + return Long.MAX_VALUE; + } + + @Override + public int characteristics() + { + return Spliterator.ORDERED; + } + + private static class Provider implements ServiceLoader.Provider + { + private T serviceProvider; + private Throwable error; + + public void setServiceProvider(T serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public void setError(Throwable error) + { + this.error = error; + } + + @Override + @SuppressWarnings("unchecked") + public Class type() + { + return (Class)get().getClass(); + } + + @Override + public T get() + { + if (error != null) + throw new ServiceConfigurationError("", error); + return serviceProvider; + } + } +} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java index 2178b56d5f7d..9ec2dbd94015 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java @@ -46,6 +46,7 @@ import java.util.ServiceLoader; import java.util.function.Function; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -778,4 +779,17 @@ public static Stream providerMap(ServiceLoader.Provider provider) return Stream.empty(); } } + + /** + * Utility to create a stream which provides the same functionality as {@link ServiceLoader#stream()}. + * However this also guards the case in which {@link Iterator#hasNext()} throws. Any exceptions + * from the underlying iterator will be cached until the {@link ServiceLoader.Provider#get()} is called. + * @param serviceLoader the ServiceLoader instance to use. + * @param the type of the service to load. + * @return A stream that lazily loads providers for this loader's service + */ + public static Stream> serviceLoaderStream(ServiceLoader serviceLoader) + { + return StreamSupport.stream(new ServiceLoaderSpliterator(serviceLoader), false); + } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/security/Credential.java b/jetty-util/src/main/java/org/eclipse/jetty/util/security/Credential.java index 367beb5762a8..0b8d076765d7 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/security/Credential.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/security/Credential.java @@ -44,7 +44,7 @@ public abstract class Credential implements Serializable { private static final long serialVersionUID = -7760551052768181572L; private static final Logger LOG = Log.getLogger(Credential.class); - private static final List CREDENTIAL_PROVIDERS = ServiceLoader.load(CredentialProvider.class).stream().flatMap(TypeUtil::providerMap).collect(Collectors.toList()); + private static final List CREDENTIAL_PROVIDERS = TypeUtil.serviceLoaderStream(ServiceLoader.load(CredentialProvider.class)).flatMap(TypeUtil::providerMap).collect(Collectors.toList()); /** * Check a credential diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Configurations.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Configurations.java index fd509cb2e6fe..563f768e4521 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Configurations.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Configurations.java @@ -75,7 +75,7 @@ public static synchronized List getKnown() { if (__known.isEmpty()) { - ServiceLoader.load(Configuration.class).stream().flatMap(TypeUtil::providerMap).forEach(configuration -> + TypeUtil.serviceLoaderStream(ServiceLoader.load(Configuration.class)).flatMap(TypeUtil::providerMap).forEach(configuration -> { if (!configuration.isAvailable()) { diff --git a/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java b/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java index 30c4e3a2c2fb..69f808d86e6e 100644 --- a/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java +++ b/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java @@ -96,8 +96,8 @@ public class XmlConfiguration { ArrayList.class, HashSet.class, Queue.class, List.class, Set.class, Collection.class }; - private static final List PROCESSOR_FACTORIES = ServiceLoader.load(ConfigurationProcessorFactory.class) - .stream().flatMap(TypeUtil::providerMap).collect(Collectors.toList()); + private static final List PROCESSOR_FACTORIES = TypeUtil.serviceLoaderStream(ServiceLoader.load(ConfigurationProcessorFactory.class)) + .flatMap(TypeUtil::providerMap).collect(Collectors.toList()); private static final XmlParser PARSER = initParser(); private static final Comparator EXECUTABLE_COMPARATOR = (o1, o2) -> {