/* * Copyright 2017 The gRPC Authors * * 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. If priorities are equal, compare the class names to // get a reliable result. Collections.sort(list, Collections.reverseOrder(new Comparator() { @Override public int compare(T f1, T f2) { int pd = priorityAccessor.getPriority(f1) - priorityAccessor.getPriority(f2); if (pd != 0) { return pd; } return f1.getClass().getName().compareTo(f2.getClass().getName()); } })); 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) { T t = createForHardCoded(klass, candidate); if (t == null) { continue; } list.add(t); } return list; } private static T createForHardCoded(Class klass, Class rawClass) { try { return rawClass.asSubclass(klass).getConstructor().newInstance(); } catch (ClassCastException ex) { // Tools like Proguard that perform obfuscation rewrite strings only when the class they // reference is known, as otherwise they wouldn't know its new name. This means some // hard-coded Class.forNames() won't be rewritten. This can cause ClassCastException at // runtime if the class ends up appearing on the classpath but that class is part of a // separate copy of grpc. With tools like Maven Shade Plugin the class wouldn't be found at // all and so would be skipped. We want to skip in this case as well. return null; } 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); } }