core: Use Class.forName(String) in provider for Android

Class.forName(String) is understood by ProGuard, removing the need for
manual ProGuard configuration and allows ProGuard to rename the provider
classes. Previously the provider classes could not be renamed.

Fixes #2633
This commit is contained in:
Eric Anderson 2017-07-19 10:15:03 -07:00 committed by GitHub
parent 8572f5ff6b
commit d325919f62
11 changed files with 221 additions and 49 deletions

View File

@ -18,5 +18,3 @@
-dontwarn sun.reflect.**
# Ignores: can't find referenced class javax.lang.model.element.Modifier
-dontwarn com.google.errorprone.annotations.**
-keep class io.grpc.internal.DnsNameResolverProvider
-keep class io.grpc.okhttp.OkHttpChannelProvider

View File

@ -40,7 +40,7 @@ public abstract class ManagedChannelProvider {
static ManagedChannelProvider load(ClassLoader classLoader) {
Iterable<ManagedChannelProvider> candidates;
if (isAndroid()) {
candidates = getCandidatesViaHardCoded(classLoader);
candidates = getCandidatesViaHardCoded();
} else {
candidates = getCandidatesViaServiceLoader(classLoader);
}
@ -79,16 +79,18 @@ public abstract class ManagedChannelProvider {
* be used on Android is free to be added here.
*/
@VisibleForTesting
public static Iterable<ManagedChannelProvider> getCandidatesViaHardCoded(
ClassLoader classLoader) {
public static Iterable<ManagedChannelProvider> getCandidatesViaHardCoded() {
// Class.forName(String) is used to remove the need for ProGuard configuration. Note that
// ProGuard does not detect usages of Class.forName(String, boolean, ClassLoader):
// https://sourceforge.net/p/proguard/bugs/418/
List<ManagedChannelProvider> list = new ArrayList<ManagedChannelProvider>();
try {
list.add(create(Class.forName("io.grpc.okhttp.OkHttpChannelProvider", true, classLoader)));
list.add(create(Class.forName("io.grpc.okhttp.OkHttpChannelProvider")));
} catch (ClassNotFoundException ex) {
// ignore
}
try {
list.add(create(Class.forName("io.grpc.netty.NettyChannelProvider", true, classLoader)));
list.add(create(Class.forName("io.grpc.netty.NettyChannelProvider")));
} catch (ClassNotFoundException ex) {
// ignore
}

View File

@ -50,7 +50,7 @@ public abstract class NameResolverProvider extends NameResolver.Factory {
static List<NameResolverProvider> load(ClassLoader classLoader) {
Iterable<NameResolverProvider> candidates;
if (isAndroid()) {
candidates = getCandidatesViaHardCoded(classLoader);
candidates = getCandidatesViaHardCoded();
} else {
candidates = getCandidatesViaServiceLoader(classLoader);
}
@ -83,11 +83,13 @@ public abstract class NameResolverProvider extends NameResolver.Factory {
* be used on Android is free to be added here.
*/
@VisibleForTesting
public static Iterable<NameResolverProvider> getCandidatesViaHardCoded(ClassLoader classLoader) {
public static Iterable<NameResolverProvider> getCandidatesViaHardCoded() {
// Class.forName(String) is used to remove the need for ProGuard configuration. Note that
// ProGuard does not detect usages of Class.forName(String, boolean, ClassLoader):
// https://sourceforge.net/p/proguard/bugs/418/
List<NameResolverProvider> list = new ArrayList<NameResolverProvider>();
try {
list.add(create(
Class.forName("io.grpc.internal.DnsNameResolverProvider", true, classLoader)));
list.add(create(Class.forName("io.grpc.internal.DnsNameResolverProvider")));
} catch (ClassNotFoundException ex) {
// ignore
}

View File

@ -22,7 +22,10 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ServiceConfigurationError;
import java.util.regex.Pattern;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@ -52,15 +55,22 @@ public class ManagedChannelProviderTest {
}
@Test
public void getCandidatesViaHardCoded_usesProvidedClassLoader() {
public void getCandidatesViaHardCoded_triesToLoadClasses() throws Exception {
ClassLoader cl = getClass().getClassLoader();
final RuntimeException toThrow = new RuntimeException();
try {
ManagedChannelProvider.getCandidatesViaHardCoded(new ClassLoader() {
cl = new ClassLoader(cl) {
@Override
public Class<?> loadClass(String name) {
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("io.grpc.netty.") || name.startsWith("io.grpc.okhttp.")) {
throw toThrow;
} else {
return super.loadClass(name, resolve);
}
});
}
};
cl = new StaticTestingClassLoader(cl, Pattern.compile("io\\.grpc\\.[^.]*"));
try {
invokeGetCandidatesViaHardCoded(cl);
fail("Expected exception");
} catch (RuntimeException ex) {
assertSame(toThrow, ex);
@ -68,14 +78,20 @@ public class ManagedChannelProviderTest {
}
@Test
public void getCandidatesViaHardCoded_ignoresMissingClasses() {
Iterable<ManagedChannelProvider> i =
ManagedChannelProvider.getCandidatesViaHardCoded(new ClassLoader() {
public void getCandidatesViaHardCoded_ignoresMissingClasses() throws Exception {
ClassLoader cl = getClass().getClassLoader();
cl = new ClassLoader(cl) {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("io.grpc.netty.") || name.startsWith("io.grpc.okhttp.")) {
throw new ClassNotFoundException();
} else {
return super.loadClass(name, resolve);
}
});
}
};
cl = new StaticTestingClassLoader(cl, Pattern.compile("io\\.grpc\\.[^.]*"));
Iterable<?> i = invokeGetCandidatesViaHardCoded(cl);
assertFalse("Iterator should be empty", i.iterator().hasNext());
}
@ -92,6 +108,20 @@ public class ManagedChannelProviderTest {
}
}
private static Iterable<?> invokeGetCandidatesViaHardCoded(ClassLoader cl) throws Exception {
// An error before the invoke likely means there is a bug in the test
Class<?> klass = Class.forName(ManagedChannelProvider.class.getName(), true, cl);
Method getCandidatesViaHardCoded = klass.getMethod("getCandidatesViaHardCoded");
try {
return (Iterable<?>) getCandidatesViaHardCoded.invoke(null);
} catch (InvocationTargetException ex) {
if (ex.getCause() instanceof Exception) {
throw (Exception) ex.getCause();
}
throw ex;
}
}
private static class BaseProvider extends ManagedChannelProvider {
private final boolean isAvailable;
private final int priority;

View File

@ -25,10 +25,17 @@ import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import io.grpc.internal.DnsNameResolverProvider;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.ServiceConfigurationError;
import java.util.regex.Pattern;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@ -120,15 +127,24 @@ public class NameResolverProviderTest {
}
@Test
public void getCandidatesViaHardCoded_usesProvidedClassLoader() {
public void getCandidatesViaHardCoded_triesToLoadClasses() throws Exception {
ClassLoader cl = getClass().getClassLoader();
final RuntimeException toThrow = new RuntimeException();
try {
NameResolverProvider.getCandidatesViaHardCoded(new ClassLoader() {
// Prevent DnsNameResolverProvider from being known
cl = new FilteringClassLoader(cl, serviceFile);
cl = new ClassLoader(cl) {
@Override
public Class<?> loadClass(String name) {
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("io.grpc.internal.")) {
throw toThrow;
} else {
return super.loadClass(name, resolve);
}
});
}
};
cl = new StaticTestingClassLoader(cl, Pattern.compile("io\\.grpc\\.[^.]*"));
try {
invokeGetCandidatesViaHardCoded(cl);
fail("Expected exception");
} catch (RuntimeException ex) {
assertSame(toThrow, ex);
@ -136,14 +152,22 @@ public class NameResolverProviderTest {
}
@Test
public void getCandidatesViaHardCoded_ignoresMissingClasses() {
Iterable<NameResolverProvider> i =
NameResolverProvider.getCandidatesViaHardCoded(new ClassLoader() {
public void getCandidatesViaHardCoded_ignoresMissingClasses() throws Exception {
ClassLoader cl = getClass().getClassLoader();
// Prevent DnsNameResolverProvider from being known
cl = new FilteringClassLoader(cl, serviceFile);
cl = new ClassLoader(cl) {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("io.grpc.internal.")) {
throw new ClassNotFoundException();
} else {
return super.loadClass(name, resolve);
}
});
}
};
cl = new StaticTestingClassLoader(cl, Pattern.compile("io\\.grpc\\.[^.]*"));
Iterable<?> i = invokeGetCandidatesViaHardCoded(cl);
assertFalse("Iterator should be empty", i.iterator().hasNext());
}
@ -160,6 +184,53 @@ public class NameResolverProviderTest {
}
}
private static Iterable<?> invokeGetCandidatesViaHardCoded(ClassLoader cl) throws Exception {
// An error before the invoke likely means there is a bug in the test
Class<?> klass = Class.forName(NameResolverProvider.class.getName(), true, cl);
Method getCandidatesViaHardCoded = klass.getMethod("getCandidatesViaHardCoded");
try {
return (Iterable<?>) getCandidatesViaHardCoded.invoke(null);
} catch (InvocationTargetException ex) {
if (ex.getCause() instanceof Exception) {
throw (Exception) ex.getCause();
}
throw ex;
}
}
private static class FilteringClassLoader extends ClassLoader {
private final String resource;
public FilteringClassLoader(ClassLoader parent, String resource) {
super(parent);
this.resource = resource;
}
@Override
public URL getResource(String name) {
if (resource.equals(name)) {
return null;
}
return super.getResource(name);
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
if (resource.equals(name)) {
return new Enumeration<URL>() {
@Override public boolean hasMoreElements() {
return false;
}
@Override public URL nextElement() {
throw new NoSuchElementException();
}
};
}
return super.getResources(name);
}
}
private static class BaseProvider extends NameResolverProvider {
private final boolean isAvailable;
private final int priority;

View File

@ -0,0 +1,76 @@
/*
* 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.base.Preconditions;
import io.grpc.internal.IoUtils;
import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Pattern;
/**
* A class loader that can be used to repeatedly trigger static initialization of a class. A new
* instance is required per test.
*/
public final class StaticTestingClassLoader extends ClassLoader {
private final Pattern classesToDefine;
public StaticTestingClassLoader(ClassLoader parent, Pattern classesToDefine) {
super(parent);
this.classesToDefine = Preconditions.checkNotNull(classesToDefine, "classesToDefine");
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (!classesToDefine.matcher(name).matches()) {
throw new ClassNotFoundException(name);
}
InputStream is = getResourceAsStream(name.replace('.', '/') + ".class");
if (is == null) {
throw new ClassNotFoundException(name);
}
byte[] b;
try {
b = IoUtils.toByteArray(is);
} catch (IOException ex) {
throw new ClassNotFoundException(name, ex);
}
return defineClass(name, b, 0, b.length);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// Reverse normal loading order; check this class loader before its parent
synchronized (getClassLoadingLock(name)) {
Class<?> klass = findLoadedClass(name);
if (klass == null) {
try {
klass = findClass(name);
} catch (ClassNotFoundException e) {
// This ClassLoader doesn't know a class with that name; that's part of normal operation
}
}
if (klass == null) {
klass = super.loadClass(name, false);
}
if (resolve) {
resolveClass(klass);
}
return klass;
}
}
}

View File

@ -46,8 +46,7 @@ public class DnsNameResolverProviderTest {
@Test
public void providedHardCoded() {
for (NameResolverProvider current
: NameResolverProvider.getCandidatesViaHardCoded(getClass().getClassLoader())) {
for (NameResolverProvider current : NameResolverProvider.getCandidatesViaHardCoded()) {
if (current instanceof DnsNameResolverProvider) {
return;
}

View File

@ -15,5 +15,3 @@
-dontwarn javax.naming.**
-dontwarn okio.**
-dontwarn sun.misc.Unsafe
-keep class io.grpc.internal.DnsNameResolverProvider
-keep class io.grpc.okhttp.OkHttpChannelProvider

View File

@ -14,5 +14,3 @@
-dontwarn okio.**
# Ignores: can't find referenced class javax.lang.model.element.Modifier
-dontwarn com.google.errorprone.annotations.**
-keep class io.grpc.internal.DnsNameResolverProvider
-keep class io.grpc.okhttp.OkHttpChannelProvider

View File

@ -44,8 +44,7 @@ public class NettyChannelProviderTest {
@Test
public void providedHardCoded() {
for (ManagedChannelProvider current
: ManagedChannelProvider.getCandidatesViaHardCoded(getClass().getClassLoader())) {
for (ManagedChannelProvider current : ManagedChannelProvider.getCandidatesViaHardCoded()) {
if (current instanceof NettyChannelProvider) {
return;
}

View File

@ -43,8 +43,7 @@ public class OkHttpChannelProviderTest {
@Test
public void providedHardCoded() {
for (ManagedChannelProvider current
: ManagedChannelProvider.getCandidatesViaHardCoded(getClass().getClassLoader())) {
for (ManagedChannelProvider current : ManagedChannelProvider.getCandidatesViaHardCoded()) {
if (current instanceof OkHttpChannelProvider) {
return;
}