diff --git a/core/src/main/java/io/grpc/InternalServiceProviders.java b/core/src/main/java/io/grpc/InternalServiceProviders.java new file mode 100644 index 0000000000..0904b529d3 --- /dev/null +++ b/core/src/main/java/io/grpc/InternalServiceProviders.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017, gRPC Authors All rights reserved. + * + * Licensed 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 io.grpc; + +import com.google.common.annotations.VisibleForTesting; +import java.util.List; + +@Internal +public final class InternalServiceProviders { + private InternalServiceProviders() { + } + + /** + * Accessor for method. + */ + public static T load( + Class klass, + Iterable> hardcoded, + ClassLoader classLoader, + PriorityAccessor priorityAccessor) { + return ServiceProviders.load(klass, hardcoded, classLoader, priorityAccessor); + } + + /** + * Accessor for method. + */ + public static List loadAll( + Class klass, + Iterable> hardCodedClasses, + ClassLoader classLoader, + PriorityAccessor priorityAccessor) { + return ServiceProviders.loadAll(klass, hardCodedClasses, classLoader, priorityAccessor); + } + + /** + * Accessor for method. + */ + @VisibleForTesting + public static Iterable getCandidatesViaServiceLoader(Class klass, ClassLoader cl) { + return ServiceProviders.getCandidatesViaServiceLoader(klass, cl); + } + + /** + * Accessor for method. + */ + @VisibleForTesting + public static Iterable getCandidatesViaHardCoded( + Class klass, Iterable> hardcoded) { + return ServiceProviders.getCandidatesViaHardCoded(klass, hardcoded); + } + + /** + * Accessor for {@link ServiceProviders#isAndroid}. + */ + public static boolean isAndroid(ClassLoader cl) { + return ServiceProviders.isAndroid(cl); + } + + public interface PriorityAccessor extends ServiceProviders.PriorityAccessor {} +} diff --git a/core/src/main/java/io/grpc/ServiceProviders.java b/core/src/main/java/io/grpc/ServiceProviders.java new file mode 100644 index 0000000000..22d8ee173c --- /dev/null +++ b/core/src/main/java/io/grpc/ServiceProviders.java @@ -0,0 +1,153 @@ +/* + * Copyright 2017, gRPC Authors All rights reserved. + * + * Licensed 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 io.grpc; + +import com.google.common.annotations.VisibleForTesting; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; + +final class ServiceProviders { + private ServiceProviders() { + // do not instantiate + } + + /** + * If this is not Android, returns the highest priority implementation of the class via + * {@link ServiceLoader}. + * If this is Android, returns an instance of the highest priority class in {@code hardcoded}. + */ + public static T load( + Class klass, + Iterable> hardcoded, + ClassLoader cl, + PriorityAccessor priorityAccessor) { + List candidates = loadAll(klass, hardcoded, cl, priorityAccessor); + if (candidates.isEmpty()) { + return null; + } + return candidates.get(0); + } + + /** + * If this is not Android, returns all available implementations discovered via + * {@link ServiceLoader}. + * If this is Android, returns all available implementations in {@code hardcoded}. + * The list is sorted in descending priority order. + */ + public static List loadAll( + Class klass, + Iterable> hardcoded, + ClassLoader cl, + final PriorityAccessor priorityAccessor) { + Iterable candidates; + if (isAndroid(cl)) { + candidates = getCandidatesViaHardCoded(klass, hardcoded); + } else { + candidates = getCandidatesViaServiceLoader(klass, cl); + } + List list = new ArrayList(); + for (T current: candidates) { + if (!priorityAccessor.isAvailable(current)) { + continue; + } + list.add(current); + } + + // Sort descending based on priority. + Collections.sort(list, Collections.reverseOrder(new Comparator() { + @Override + public int compare(T f1, T f2) { + return priorityAccessor.getPriority(f1) - priorityAccessor.getPriority(f2); + } + })); + return Collections.unmodifiableList(list); + } + + /** + * Returns true if the {@link ClassLoader} is for android. + */ + static boolean isAndroid(ClassLoader cl) { + try { + // Specify a class loader instead of null because we may be running under Robolectric + Class.forName("android.app.Application", /*initialize=*/ false, cl); + return true; + } catch (Exception e) { + // If Application isn't loaded, it might as well not be Android. + return false; + } + } + + /** + * Loads service providers for the {@code klass} service using {@link ServiceLoader}. + */ + @VisibleForTesting + public static Iterable getCandidatesViaServiceLoader(Class klass, ClassLoader cl) { + Iterable i = ServiceLoader.load(klass, cl); + // Attempt to load using the context class loader and ServiceLoader. + // This allows frameworks like http://aries.apache.org/modules/spi-fly.html to plug in. + if (!i.iterator().hasNext()) { + i = ServiceLoader.load(klass); + } + return i; + } + + /** + * Load providers from a hard-coded list. This avoids using getResource(), which has performance + * problems on Android (see https://github.com/grpc/grpc-java/issues/2037). + */ + @VisibleForTesting + static Iterable getCandidatesViaHardCoded(Class klass, Iterable> hardcoded) { + List list = new ArrayList(); + for (Class candidate : hardcoded) { + list.add(create(klass, candidate)); + } + return list; + } + + @VisibleForTesting + static T create(Class klass, Class rawClass) { + try { + return rawClass.asSubclass(klass).getConstructor().newInstance(); + } catch (Throwable t) { + throw new ServiceConfigurationError( + String.format("Provider %s could not be instantiated %s", rawClass.getName(), t), t); + } + } + + /** + * An interface that allows us to get priority information about a provider. + */ + public interface PriorityAccessor { + /** + * Checks this provider is available for use, taking the current environment into consideration. + * If {@code false}, no other methods are safe to be called. + */ + boolean isAvailable(T provider); + + /** + * A priority, from 0 to 10 that this provider should be used, taking the current environment + * into consideration. 5 should be considered the default, and then tweaked based on environment + * detection. A priority of 0 does not imply that the provider wouldn't work; just that it + * should be last in line. + */ + int getPriority(T provider); + } +} diff --git a/core/src/test/java/io/grpc/ServiceProvidersTest.java b/core/src/test/java/io/grpc/ServiceProvidersTest.java new file mode 100644 index 0000000000..7337fe90d0 --- /dev/null +++ b/core/src/test/java/io/grpc/ServiceProvidersTest.java @@ -0,0 +1,316 @@ +/* + * Copyright 2015, gRPC Authors All rights reserved. + * + * Licensed 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 io.grpc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import io.grpc.InternalServiceProviders.PriorityAccessor; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ServiceConfigurationError; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ServiceProviders}. */ +@RunWith(JUnit4.class) +public class ServiceProvidersTest { + private static final List> NO_HARDCODED = Collections.emptyList(); + private static final PriorityAccessor ACCESSOR = + new PriorityAccessor() { + @Override + public boolean isAvailable(FooProvider provider) { + return provider.isAvailable(); + } + + @Override + public int getPriority(FooProvider provider) { + return provider.priority(); + } + }; + private final String serviceFile = "META-INF/services/io.grpc.ServiceProvidersTest$FooProvider"; + + @Test + public void contextClassLoaderProvider() { + ClassLoader ccl = Thread.currentThread().getContextClassLoader(); + try { + ClassLoader cl = new ReplacingClassLoader( + getClass().getClassLoader(), + serviceFile, + "io/grpc/ServiceProvidersTest$FooProvider-multipleProvider.txt"); + + // test that the context classloader is used as fallback + ClassLoader rcll = new ReplacingClassLoader( + getClass().getClassLoader(), + serviceFile, + "io/grpc/ServiceProvidersTest$FooProvider-empty.txt"); + Thread.currentThread().setContextClassLoader(rcll); + assertEquals( + Available7Provider.class, + ServiceProviders.load(FooProvider.class, NO_HARDCODED, cl, ACCESSOR).getClass()); + } finally { + Thread.currentThread().setContextClassLoader(ccl); + } + } + + @Test + public void noProvider() { + ClassLoader ccl = Thread.currentThread().getContextClassLoader(); + try { + ClassLoader cl = new ReplacingClassLoader( + getClass().getClassLoader(), + serviceFile, + "io/grpc/ServiceProvidersTest$FooProvider-doesNotExist.txt"); + Thread.currentThread().setContextClassLoader(cl); + assertNull(ServiceProviders.load(FooProvider.class, NO_HARDCODED, cl, ACCESSOR)); + } finally { + Thread.currentThread().setContextClassLoader(ccl); + } + } + + @Test + public void multipleProvider() throws Exception { + ClassLoader cl = new ReplacingClassLoader(getClass().getClassLoader(), serviceFile, + "io/grpc/ServiceProvidersTest$FooProvider-multipleProvider.txt"); + assertSame( + Available7Provider.class, + ServiceProviders.load(FooProvider.class, NO_HARDCODED, cl, ACCESSOR).getClass()); + + List providers = ServiceProviders.loadAll( + FooProvider.class, NO_HARDCODED, cl, ACCESSOR); + assertEquals(3, providers.size()); + assertEquals(Available7Provider.class, providers.get(0).getClass()); + assertEquals(Available5Provider.class, providers.get(1).getClass()); + assertEquals(Available0Provider.class, providers.get(2).getClass()); + } + + @Test + public void unavailableProvider() { + // tries to load Available7 and UnavailableProvider, which has priority 10 + ClassLoader cl = new ReplacingClassLoader(getClass().getClassLoader(), serviceFile, + "io/grpc/ServiceProvidersTest$FooProvider-unavailableProvider.txt"); + assertEquals( + Available7Provider.class, + ServiceProviders.load(FooProvider.class, NO_HARDCODED, cl, ACCESSOR).getClass()); + } + + @Test + public void unknownClassProvider() { + ClassLoader cl = new ReplacingClassLoader(getClass().getClassLoader(), serviceFile, + "io/grpc/ServiceProvidersTest$FooProvider-unknownClassProvider.txt"); + try { + FooProvider ignored = ServiceProviders.load(FooProvider.class, NO_HARDCODED, cl, ACCESSOR); + fail("Exception expected"); + } catch (ServiceConfigurationError e) { + // noop + } + } + + @Test + public void exceptionSurfacedToCaller_failAtInit() { + ClassLoader cl = new ReplacingClassLoader(getClass().getClassLoader(), serviceFile, + "io/grpc/ServiceProvidersTest$FooProvider-failAtInitProvider.txt"); + try { + // Even though there is a working provider, if any providers fail then we should fail + // completely to avoid returning something unexpected. + FooProvider ignored = ServiceProviders.load(FooProvider.class, NO_HARDCODED, cl, ACCESSOR); + fail("Expected exception"); + } catch (ServiceConfigurationError expected) { + // noop + } + } + + @Test + public void exceptionSurfacedToCaller_failAtPriority() { + ClassLoader cl = new ReplacingClassLoader(getClass().getClassLoader(), serviceFile, + "io/grpc/ServiceProvidersTest$FooProvider-failAtPriorityProvider.txt"); + try { + // The exception should be surfaced to the caller + FooProvider ignored = ServiceProviders.load(FooProvider.class, NO_HARDCODED, cl, ACCESSOR); + fail("Expected exception"); + } catch (FailAtPriorityProvider.PriorityException expected) { + // noop + } + } + + @Test + public void exceptionSurfacedToCaller_failAtAvailable() { + ClassLoader cl = new ReplacingClassLoader(getClass().getClassLoader(), serviceFile, + "io/grpc/ServiceProvidersTest$FooProvider-failAtAvailableProvider.txt"); + try { + // The exception should be surfaced to the caller + FooProvider ignored = ServiceProviders.load(FooProvider.class, NO_HARDCODED, cl, ACCESSOR); + fail("Expected exception"); + } catch (FailAtAvailableProvider.AvailableException expected) { + // noop + } + } + + @Test + public void getCandidatesViaHardCoded_multipleProvider() throws Exception { + Iterator candidates = ServiceProviders.getCandidatesViaHardCoded( + FooProvider.class, + ImmutableList.>of( + Available7Provider.class, + Available0Provider.class)) + .iterator(); + assertEquals(Available7Provider.class, candidates.next().getClass()); + assertEquals(Available0Provider.class, candidates.next().getClass()); + assertFalse(candidates.hasNext()); + } + + @Test + public void getCandidatesViaHardCoded_failAtInit() throws Exception { + try { + Iterable ignored = ServiceProviders.getCandidatesViaHardCoded( + FooProvider.class, + Collections.>singletonList(FailAtInitProvider.class)); + fail("Expected exception"); + } catch (ServiceConfigurationError expected) { + // noop + } + } + + @Test + public void getCandidatesViaHardCoded_failAtInit_moreCandidates() throws Exception { + try { + Iterable ignored = ServiceProviders.getCandidatesViaHardCoded( + FooProvider.class, + ImmutableList.>of(FailAtInitProvider.class, Available0Provider.class)); + fail("Expected exception"); + } catch (ServiceConfigurationError expected) { + // noop + } + } + + @Test + public void create_throwsErrorOnMisconfiguration() throws Exception { + class PrivateClass {} + + try { + FooProvider ignored = ServiceProviders.create(FooProvider.class, PrivateClass.class); + fail("Expected exception"); + } catch (ServiceConfigurationError expected) { + assertTrue("Expected ClassCastException cause: " + expected.getCause(), + expected.getCause() instanceof ClassCastException); + } + } + + /** + * A provider class for this unit test. + */ + public abstract static class FooProvider { + abstract boolean isAvailable(); + + abstract int priority(); + } + + private static class BaseProvider extends FooProvider { + private final boolean isAvailable; + private final int priority; + + public BaseProvider(boolean isAvailable, int priority) { + this.isAvailable = isAvailable; + this.priority = priority; + } + + @Override + public boolean isAvailable() { + return isAvailable; + } + + @Override + public int priority() { + return priority; + } + } + + public static final class Available0Provider extends BaseProvider { + public Available0Provider() { + super(true, 0); + } + } + + public static final class Available5Provider extends BaseProvider { + public Available5Provider() { + super(true, 5); + } + } + + public static final class Available7Provider extends BaseProvider { + public Available7Provider() { + super(true, 7); + } + } + + public static final class UnavailableProvider extends BaseProvider { + public UnavailableProvider() { + super(false, 10); + } + } + + public static final class FailAtInitProvider extends FooProvider { + public FailAtInitProvider() { + throw new RuntimeException("intentionally broken"); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public int priority() { + return 0; + } + } + + public static final class FailAtPriorityProvider extends FooProvider { + @Override + public boolean isAvailable() { + return true; + } + + @Override + public int priority() { + throw new PriorityException(); + } + + public static final class PriorityException extends RuntimeException {} + } + + public static final class FailAtAvailableProvider extends FooProvider { + @Override + public boolean isAvailable() { + throw new AvailableException(); + } + + @Override + public int priority() { + return 0; + } + + public static final class AvailableException extends RuntimeException {} + } +} diff --git a/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-empty.txt b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-empty.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-failAtAvailableProvider.txt b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-failAtAvailableProvider.txt new file mode 100644 index 0000000000..61cc58e152 --- /dev/null +++ b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-failAtAvailableProvider.txt @@ -0,0 +1,2 @@ +io.grpc.ServiceProvidersTest$FailAtAvailableProvider +io.grpc.ServiceProvidersTest$Available5Provider diff --git a/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-failAtInitProvider.txt b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-failAtInitProvider.txt new file mode 100644 index 0000000000..c2ec06a1b5 --- /dev/null +++ b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-failAtInitProvider.txt @@ -0,0 +1,2 @@ +io.grpc.ServiceProvidersTest$FailAtInitProvider +io.grpc.ServiceProvidersTest$Available5Provider diff --git a/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-failAtPriorityProvider.txt b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-failAtPriorityProvider.txt new file mode 100644 index 0000000000..3a3aa79293 --- /dev/null +++ b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-failAtPriorityProvider.txt @@ -0,0 +1,2 @@ +io.grpc.ServiceProvidersTest$FailAtPriorityProvider +io.grpc.ServiceProvidersTest$Available5Provider diff --git a/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-multipleProvider.txt b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-multipleProvider.txt new file mode 100644 index 0000000000..cb24790329 --- /dev/null +++ b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-multipleProvider.txt @@ -0,0 +1,3 @@ +io.grpc.ServiceProvidersTest$Available5Provider +io.grpc.ServiceProvidersTest$Available7Provider +io.grpc.ServiceProvidersTest$Available0Provider diff --git a/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-unavailableProvider.txt b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-unavailableProvider.txt new file mode 100644 index 0000000000..55175ff180 --- /dev/null +++ b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-unavailableProvider.txt @@ -0,0 +1,2 @@ +io.grpc.ServiceProvidersTest$UnavailableProvider +io.grpc.ServiceProvidersTest$Available7Provider diff --git a/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-unknownClassProvider.txt b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-unknownClassProvider.txt new file mode 100644 index 0000000000..58c264107c --- /dev/null +++ b/core/src/test/resources/io/grpc/ServiceProvidersTest$FooProvider-unknownClassProvider.txt @@ -0,0 +1 @@ +io.grpc.ServiceProvidersTest$UnknownClassProvider