Skip to content

Commit d325919

Browse files
authored
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 grpc#2633
1 parent 8572f5f commit d325919

11 files changed

Lines changed: 221 additions & 49 deletions

File tree

android-interop-testing/app/proguard-rules.pro

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,3 @@
1818
-dontwarn sun.reflect.**
1919
# Ignores: can't find referenced class javax.lang.model.element.Modifier
2020
-dontwarn com.google.errorprone.annotations.**
21-
-keep class io.grpc.internal.DnsNameResolverProvider
22-
-keep class io.grpc.okhttp.OkHttpChannelProvider

core/src/main/java/io/grpc/ManagedChannelProvider.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public abstract class ManagedChannelProvider {
4040
static ManagedChannelProvider load(ClassLoader classLoader) {
4141
Iterable<ManagedChannelProvider> candidates;
4242
if (isAndroid()) {
43-
candidates = getCandidatesViaHardCoded(classLoader);
43+
candidates = getCandidatesViaHardCoded();
4444
} else {
4545
candidates = getCandidatesViaServiceLoader(classLoader);
4646
}
@@ -79,16 +79,18 @@ public static Iterable<ManagedChannelProvider> getCandidatesViaServiceLoader(
7979
* be used on Android is free to be added here.
8080
*/
8181
@VisibleForTesting
82-
public static Iterable<ManagedChannelProvider> getCandidatesViaHardCoded(
83-
ClassLoader classLoader) {
82+
public static Iterable<ManagedChannelProvider> getCandidatesViaHardCoded() {
83+
// Class.forName(String) is used to remove the need for ProGuard configuration. Note that
84+
// ProGuard does not detect usages of Class.forName(String, boolean, ClassLoader):
85+
// https://sourceforge.net/p/proguard/bugs/418/
8486
List<ManagedChannelProvider> list = new ArrayList<ManagedChannelProvider>();
8587
try {
86-
list.add(create(Class.forName("io.grpc.okhttp.OkHttpChannelProvider", true, classLoader)));
88+
list.add(create(Class.forName("io.grpc.okhttp.OkHttpChannelProvider")));
8789
} catch (ClassNotFoundException ex) {
8890
// ignore
8991
}
9092
try {
91-
list.add(create(Class.forName("io.grpc.netty.NettyChannelProvider", true, classLoader)));
93+
list.add(create(Class.forName("io.grpc.netty.NettyChannelProvider")));
9294
} catch (ClassNotFoundException ex) {
9395
// ignore
9496
}

core/src/main/java/io/grpc/NameResolverProvider.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public abstract class NameResolverProvider extends NameResolver.Factory {
5050
static List<NameResolverProvider> load(ClassLoader classLoader) {
5151
Iterable<NameResolverProvider> candidates;
5252
if (isAndroid()) {
53-
candidates = getCandidatesViaHardCoded(classLoader);
53+
candidates = getCandidatesViaHardCoded();
5454
} else {
5555
candidates = getCandidatesViaServiceLoader(classLoader);
5656
}
@@ -83,11 +83,13 @@ public static Iterable<NameResolverProvider> getCandidatesViaServiceLoader(
8383
* be used on Android is free to be added here.
8484
*/
8585
@VisibleForTesting
86-
public static Iterable<NameResolverProvider> getCandidatesViaHardCoded(ClassLoader classLoader) {
86+
public static Iterable<NameResolverProvider> getCandidatesViaHardCoded() {
87+
// Class.forName(String) is used to remove the need for ProGuard configuration. Note that
88+
// ProGuard does not detect usages of Class.forName(String, boolean, ClassLoader):
89+
// https://sourceforge.net/p/proguard/bugs/418/
8790
List<NameResolverProvider> list = new ArrayList<NameResolverProvider>();
8891
try {
89-
list.add(create(
90-
Class.forName("io.grpc.internal.DnsNameResolverProvider", true, classLoader)));
92+
list.add(create(Class.forName("io.grpc.internal.DnsNameResolverProvider")));
9193
} catch (ClassNotFoundException ex) {
9294
// ignore
9395
}

core/src/test/java/io/grpc/ManagedChannelProviderTest.java

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
import static org.junit.Assert.assertTrue;
2323
import static org.junit.Assert.fail;
2424

25+
import java.lang.reflect.InvocationTargetException;
26+
import java.lang.reflect.Method;
2527
import java.util.ServiceConfigurationError;
28+
import java.util.regex.Pattern;
2629
import org.junit.Test;
2730
import org.junit.runner.RunWith;
2831
import org.junit.runners.JUnit4;
@@ -52,30 +55,43 @@ public void unavailableProvider() {
5255
}
5356

5457
@Test
55-
public void getCandidatesViaHardCoded_usesProvidedClassLoader() {
58+
public void getCandidatesViaHardCoded_triesToLoadClasses() throws Exception {
59+
ClassLoader cl = getClass().getClassLoader();
5660
final RuntimeException toThrow = new RuntimeException();
57-
try {
58-
ManagedChannelProvider.getCandidatesViaHardCoded(new ClassLoader() {
59-
@Override
60-
public Class<?> loadClass(String name) {
61+
cl = new ClassLoader(cl) {
62+
@Override
63+
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
64+
if (name.startsWith("io.grpc.netty.") || name.startsWith("io.grpc.okhttp.")) {
6165
throw toThrow;
66+
} else {
67+
return super.loadClass(name, resolve);
6268
}
63-
});
69+
}
70+
};
71+
cl = new StaticTestingClassLoader(cl, Pattern.compile("io\\.grpc\\.[^.]*"));
72+
try {
73+
invokeGetCandidatesViaHardCoded(cl);
6474
fail("Expected exception");
6575
} catch (RuntimeException ex) {
6676
assertSame(toThrow, ex);
6777
}
6878
}
6979

7080
@Test
71-
public void getCandidatesViaHardCoded_ignoresMissingClasses() {
72-
Iterable<ManagedChannelProvider> i =
73-
ManagedChannelProvider.getCandidatesViaHardCoded(new ClassLoader() {
74-
@Override
75-
public Class<?> loadClass(String name) throws ClassNotFoundException {
76-
throw new ClassNotFoundException();
77-
}
78-
});
81+
public void getCandidatesViaHardCoded_ignoresMissingClasses() throws Exception {
82+
ClassLoader cl = getClass().getClassLoader();
83+
cl = new ClassLoader(cl) {
84+
@Override
85+
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
86+
if (name.startsWith("io.grpc.netty.") || name.startsWith("io.grpc.okhttp.")) {
87+
throw new ClassNotFoundException();
88+
} else {
89+
return super.loadClass(name, resolve);
90+
}
91+
}
92+
};
93+
cl = new StaticTestingClassLoader(cl, Pattern.compile("io\\.grpc\\.[^.]*"));
94+
Iterable<?> i = invokeGetCandidatesViaHardCoded(cl);
7995
assertFalse("Iterator should be empty", i.iterator().hasNext());
8096
}
8197

@@ -92,6 +108,20 @@ class PrivateClass {}
92108
}
93109
}
94110

111+
private static Iterable<?> invokeGetCandidatesViaHardCoded(ClassLoader cl) throws Exception {
112+
// An error before the invoke likely means there is a bug in the test
113+
Class<?> klass = Class.forName(ManagedChannelProvider.class.getName(), true, cl);
114+
Method getCandidatesViaHardCoded = klass.getMethod("getCandidatesViaHardCoded");
115+
try {
116+
return (Iterable<?>) getCandidatesViaHardCoded.invoke(null);
117+
} catch (InvocationTargetException ex) {
118+
if (ex.getCause() instanceof Exception) {
119+
throw (Exception) ex.getCause();
120+
}
121+
throw ex;
122+
}
123+
}
124+
95125
private static class BaseProvider extends ManagedChannelProvider {
96126
private final boolean isAvailable;
97127
private final int priority;

core/src/test/java/io/grpc/NameResolverProviderTest.java

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,17 @@
2525
import static org.mockito.Mockito.mock;
2626

2727
import io.grpc.internal.DnsNameResolverProvider;
28+
import java.io.IOException;
29+
import java.lang.reflect.InvocationTargetException;
30+
import java.lang.reflect.Method;
2831
import java.net.URI;
32+
import java.net.URL;
2933
import java.util.Collections;
34+
import java.util.Enumeration;
3035
import java.util.List;
36+
import java.util.NoSuchElementException;
3137
import java.util.ServiceConfigurationError;
38+
import java.util.regex.Pattern;
3239
import org.junit.Test;
3340
import org.junit.runner.RunWith;
3441
import org.junit.runners.JUnit4;
@@ -120,30 +127,47 @@ public void baseProviders() {
120127
}
121128

122129
@Test
123-
public void getCandidatesViaHardCoded_usesProvidedClassLoader() {
130+
public void getCandidatesViaHardCoded_triesToLoadClasses() throws Exception {
131+
ClassLoader cl = getClass().getClassLoader();
124132
final RuntimeException toThrow = new RuntimeException();
125-
try {
126-
NameResolverProvider.getCandidatesViaHardCoded(new ClassLoader() {
127-
@Override
128-
public Class<?> loadClass(String name) {
133+
// Prevent DnsNameResolverProvider from being known
134+
cl = new FilteringClassLoader(cl, serviceFile);
135+
cl = new ClassLoader(cl) {
136+
@Override
137+
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
138+
if (name.startsWith("io.grpc.internal.")) {
129139
throw toThrow;
140+
} else {
141+
return super.loadClass(name, resolve);
130142
}
131-
});
143+
}
144+
};
145+
cl = new StaticTestingClassLoader(cl, Pattern.compile("io\\.grpc\\.[^.]*"));
146+
try {
147+
invokeGetCandidatesViaHardCoded(cl);
132148
fail("Expected exception");
133149
} catch (RuntimeException ex) {
134150
assertSame(toThrow, ex);
135151
}
136152
}
137153

138154
@Test
139-
public void getCandidatesViaHardCoded_ignoresMissingClasses() {
140-
Iterable<NameResolverProvider> i =
141-
NameResolverProvider.getCandidatesViaHardCoded(new ClassLoader() {
142-
@Override
143-
public Class<?> loadClass(String name) throws ClassNotFoundException {
144-
throw new ClassNotFoundException();
145-
}
146-
});
155+
public void getCandidatesViaHardCoded_ignoresMissingClasses() throws Exception {
156+
ClassLoader cl = getClass().getClassLoader();
157+
// Prevent DnsNameResolverProvider from being known
158+
cl = new FilteringClassLoader(cl, serviceFile);
159+
cl = new ClassLoader(cl) {
160+
@Override
161+
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
162+
if (name.startsWith("io.grpc.internal.")) {
163+
throw new ClassNotFoundException();
164+
} else {
165+
return super.loadClass(name, resolve);
166+
}
167+
}
168+
};
169+
cl = new StaticTestingClassLoader(cl, Pattern.compile("io\\.grpc\\.[^.]*"));
170+
Iterable<?> i = invokeGetCandidatesViaHardCoded(cl);
147171
assertFalse("Iterator should be empty", i.iterator().hasNext());
148172
}
149173

@@ -160,6 +184,53 @@ class PrivateClass {}
160184
}
161185
}
162186

187+
private static Iterable<?> invokeGetCandidatesViaHardCoded(ClassLoader cl) throws Exception {
188+
// An error before the invoke likely means there is a bug in the test
189+
Class<?> klass = Class.forName(NameResolverProvider.class.getName(), true, cl);
190+
Method getCandidatesViaHardCoded = klass.getMethod("getCandidatesViaHardCoded");
191+
try {
192+
return (Iterable<?>) getCandidatesViaHardCoded.invoke(null);
193+
} catch (InvocationTargetException ex) {
194+
if (ex.getCause() instanceof Exception) {
195+
throw (Exception) ex.getCause();
196+
}
197+
throw ex;
198+
}
199+
}
200+
201+
private static class FilteringClassLoader extends ClassLoader {
202+
private final String resource;
203+
204+
public FilteringClassLoader(ClassLoader parent, String resource) {
205+
super(parent);
206+
this.resource = resource;
207+
}
208+
209+
@Override
210+
public URL getResource(String name) {
211+
if (resource.equals(name)) {
212+
return null;
213+
}
214+
return super.getResource(name);
215+
}
216+
217+
@Override
218+
public Enumeration<URL> getResources(String name) throws IOException {
219+
if (resource.equals(name)) {
220+
return new Enumeration<URL>() {
221+
@Override public boolean hasMoreElements() {
222+
return false;
223+
}
224+
225+
@Override public URL nextElement() {
226+
throw new NoSuchElementException();
227+
}
228+
};
229+
}
230+
return super.getResources(name);
231+
}
232+
}
233+
163234
private static class BaseProvider extends NameResolverProvider {
164235
private final boolean isAvailable;
165236
private final int priority;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2017, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc;
18+
19+
import com.google.common.base.Preconditions;
20+
import io.grpc.internal.IoUtils;
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.util.regex.Pattern;
24+
25+
/**
26+
* A class loader that can be used to repeatedly trigger static initialization of a class. A new
27+
* instance is required per test.
28+
*/
29+
public final class StaticTestingClassLoader extends ClassLoader {
30+
private final Pattern classesToDefine;
31+
32+
public StaticTestingClassLoader(ClassLoader parent, Pattern classesToDefine) {
33+
super(parent);
34+
this.classesToDefine = Preconditions.checkNotNull(classesToDefine, "classesToDefine");
35+
}
36+
37+
@Override
38+
protected Class<?> findClass(String name) throws ClassNotFoundException {
39+
if (!classesToDefine.matcher(name).matches()) {
40+
throw new ClassNotFoundException(name);
41+
}
42+
InputStream is = getResourceAsStream(name.replace('.', '/') + ".class");
43+
if (is == null) {
44+
throw new ClassNotFoundException(name);
45+
}
46+
byte[] b;
47+
try {
48+
b = IoUtils.toByteArray(is);
49+
} catch (IOException ex) {
50+
throw new ClassNotFoundException(name, ex);
51+
}
52+
return defineClass(name, b, 0, b.length);
53+
}
54+
55+
@Override
56+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
57+
// Reverse normal loading order; check this class loader before its parent
58+
synchronized (getClassLoadingLock(name)) {
59+
Class<?> klass = findLoadedClass(name);
60+
if (klass == null) {
61+
try {
62+
klass = findClass(name);
63+
} catch (ClassNotFoundException e) {
64+
// This ClassLoader doesn't know a class with that name; that's part of normal operation
65+
}
66+
}
67+
if (klass == null) {
68+
klass = super.loadClass(name, false);
69+
}
70+
if (resolve) {
71+
resolveClass(klass);
72+
}
73+
return klass;
74+
}
75+
}
76+
}

core/src/test/java/io/grpc/internal/DnsNameResolverProviderTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ public void provided() {
4646

4747
@Test
4848
public void providedHardCoded() {
49-
for (NameResolverProvider current
50-
: NameResolverProvider.getCandidatesViaHardCoded(getClass().getClassLoader())) {
49+
for (NameResolverProvider current : NameResolverProvider.getCandidatesViaHardCoded()) {
5150
if (current instanceof DnsNameResolverProvider) {
5251
return;
5352
}

examples/android/helloworld/app/proguard-rules.pro

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,3 @@
1515
-dontwarn javax.naming.**
1616
-dontwarn okio.**
1717
-dontwarn sun.misc.Unsafe
18-
-keep class io.grpc.internal.DnsNameResolverProvider
19-
-keep class io.grpc.okhttp.OkHttpChannelProvider

examples/android/routeguide/app/proguard-rules.pro

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,3 @@
1414
-dontwarn okio.**
1515
# Ignores: can't find referenced class javax.lang.model.element.Modifier
1616
-dontwarn com.google.errorprone.annotations.**
17-
-keep class io.grpc.internal.DnsNameResolverProvider
18-
-keep class io.grpc.okhttp.OkHttpChannelProvider

netty/src/test/java/io/grpc/netty/NettyChannelProviderTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ public void provided() {
4444

4545
@Test
4646
public void providedHardCoded() {
47-
for (ManagedChannelProvider current
48-
: ManagedChannelProvider.getCandidatesViaHardCoded(getClass().getClassLoader())) {
47+
for (ManagedChannelProvider current : ManagedChannelProvider.getCandidatesViaHardCoded()) {
4948
if (current instanceof NettyChannelProvider) {
5049
return;
5150
}

0 commit comments

Comments
 (0)