Skip to content

Commit bcc9fe4

Browse files
samebGuice Team
authored and
Guice Team
committed
Fix JDK17+ assistedinject private lookup behavior by catching InaccessibleObjectException (as Throwable) when attempting to reflect on private JDK internals.
Rewrite the way we iterate through method handle lookups. Don't cache fallback behavior. Instead, eagerly discover what the JDK supports & cache that. On use, if it fails (likely because a proper lookups wasn't supplied), then fallback (but do not cache the fallback). All these shenanigans are only necessary if javac generated a default method that Guice needs to invoke. Because AssistedInject uses a JDK Proxy, which also proxies default interface methods, we need to use invokespecial to target the proxy (otherwise proxying the method will end up with a stack overflow due to infinite recursion). Note that if we implemented this by code generation, we wouldn't need these hacks... so we take some pains to allow non-public factories without *requiring* the user to call withLookups(..). But the workarounds don't always work, so we still need the withLookups(..) method. PiperOrigin-RevId: 423822309
1 parent 6d50309 commit bcc9fe4

File tree

3 files changed

+216
-96
lines changed

3 files changed

+216
-96
lines changed

extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider2.java

+113-58
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import java.lang.invoke.MethodHandles;
6161
import java.lang.invoke.MethodType;
6262
import java.lang.reflect.Constructor;
63+
import java.lang.reflect.Field;
6364
import java.lang.reflect.InvocationHandler;
6465
import java.lang.reflect.Method;
6566
import java.lang.reflect.Modifier;
@@ -71,7 +72,7 @@
7172
import java.util.List;
7273
import java.util.Map;
7374
import java.util.Set;
74-
import java.util.concurrent.atomic.AtomicReference;
75+
import java.util.function.Supplier;
7576
import java.util.logging.Level;
7677
import java.util.logging.Logger;
7778

@@ -97,10 +98,18 @@ final class FactoryProvider2<F>
9798
static final Logger logger = Logger.getLogger(AssistedInject.class.getName());
9899

99100
/**
100-
* A constant that determines if we allow fallback to reflection. Typically always true, but
101-
* reflectively set to false in tests.
101+
* A constant that determines if we allow fallback to using the JDK internals to make a "private
102+
* lookup". Typically always true, but reflectively set to false in tests.
102103
*/
103-
private static boolean allowLookupReflection = true;
104+
@SuppressWarnings("FieldCanBeFinal") // non-final for testing
105+
private static boolean allowPrivateLookupFallback = true;
106+
107+
/**
108+
* A constant that determines if we allow fallback to using method handle workarounds (if
109+
* required). Typically always true, but reflectively set to false in tests.
110+
*/
111+
@SuppressWarnings("FieldCanBeFinal") // non-final for testing
112+
private static boolean allowMethodHandleWorkaround = true;
104113

105114
/** if a factory method parameter isn't annotated, it gets this annotation. */
106115
static final Assisted DEFAULT_ANNOTATION =
@@ -380,52 +389,71 @@ public TypeLiteral<?> getImplementationType() {
380389
logger.log(
381390
Level.WARNING,
382391
"AssistedInject factory {0} is non-public and has javac-generated default methods. "
383-
+ " Please pass a `MethodHandles.lookups()` with"
392+
+ " Please pass a `MethodHandles.lookup()` with"
384393
+ " FactoryModuleBuilder.withLookups when using this factory so that Guice can"
385-
+ " properly call the default methods. Guice will try to work around the"
386-
+ " problem, but doing so requires reflection into the JDK and may break at any"
387-
+ " time.",
394+
+ " properly call the default methods. Guice will try to workaround this, but "
395+
+ "it does not always work (depending on the method signatures of the factory).",
388396
new Object[] {factoryType});
389397
}
390398

399+
// Note: If the user didn't supply a valid lookup, we always try to fallback to the hacky
400+
// signature comparing workaround below.
401+
// This is because all these shenanigans are only necessary because we're implementing
402+
// AssistedInject through a Proxy. If we were to generate a subclass (which we theoretically
403+
// _could_ do), then we wouldn't inadvertantly proxy the javac-generated default methods
404+
// too (and end up with a stack overflow from infinite recursion).
405+
// As such, we try our hardest to "make things work" requiring requiring extra effort from
406+
// the user.
407+
391408
Method defaultMethod = entry.getValue();
392409
MethodHandle handle = null;
393410
try {
394-
// Note: this can return null if we fallback to reflecting the private lookup cxtor and it
395-
// fails. In that case, we try the super hacky workaround below (w/ 'foundMatch').
396-
// It can throw an exception if we're not doing private reflection, or if unreflectSpecial
397-
// _still_ fails.
398-
handle = superMethodHandle(defaultMethod, factory, userLookups);
399-
} catch (ReflectiveOperationException roe) {
400-
errors.addMessage(
401-
new Message(
402-
"Unable to use factory "
403-
+ factoryRawType.getName()
404-
+ ". Did you call FactoryModuleBuilder.withLookups(MethodHandles.lookups())"
405-
+ " (with a lookups that has access to the factory)?"));
406-
continue;
411+
handle =
412+
superMethodHandle(
413+
SuperMethodSupport.METHOD_LOOKUP, defaultMethod, factory, userLookups);
414+
} catch (ReflectiveOperationException e1) {
415+
// If the user-specified lookup failed, try again w/ the private lookup hack.
416+
// If _that_ doesn't work, try the below workaround.
417+
if (allowPrivateLookupFallback
418+
&& SuperMethodSupport.METHOD_LOOKUP != SuperMethodLookup.PRIVATE_LOOKUP) {
419+
try {
420+
handle =
421+
superMethodHandle(
422+
SuperMethodLookup.PRIVATE_LOOKUP, defaultMethod, factory, userLookups);
423+
} catch (ReflectiveOperationException e2) {
424+
// ignored, use below workaround.
425+
}
426+
}
407427
}
408428

429+
Supplier<String> failureMsg =
430+
() ->
431+
"Unable to use non-public factory "
432+
+ factoryRawType.getName()
433+
+ ". Please call"
434+
+ " FactoryModuleBuilder.withLookups(MethodHandles.lookup()) (with a"
435+
+ " lookups that has access to the factory), or make the factory"
436+
+ " public.";
409437
if (handle != null) {
410438
methodHandleBuilder.put(defaultMethod, handle);
439+
} else if (!allowMethodHandleWorkaround) {
440+
errors.addMessage(failureMsg.get());
411441
} else {
412-
// TODO: remove this workaround when Java8 support is dropped
413442
boolean foundMatch = false;
414443
for (Method otherMethod : otherMethods.get(defaultMethod.getName())) {
415444
if (dataSoFar.containsKey(otherMethod) && isCompatible(defaultMethod, otherMethod)) {
416445
if (foundMatch) {
417-
errors.addMessage(
418-
"Generated default method %s with parameters %s is"
419-
+ " signature-compatible with more than one non-default method."
420-
+ " Unable to create factory. As a workaround, remove the override"
421-
+ " so javac stops generating a default method.",
422-
defaultMethod, Arrays.asList(defaultMethod.getParameterTypes()));
446+
errors.addMessage(failureMsg.get());
447+
break;
423448
} else {
424449
assistDataBuilder.put(defaultMethod, dataSoFar.get(otherMethod));
425450
foundMatch = true;
426451
}
427452
}
428453
}
454+
// We always expect to find at least one match, because we only deal with javac-generated
455+
// default methods. If we ever allow user-specified default methods, this will need to
456+
// change.
429457
if (!foundMatch) {
430458
throw new IllegalStateException("Can't find method compatible with: " + defaultMethod);
431459
}
@@ -937,58 +965,82 @@ protected Object initialValue() {
937965
}
938966
}
939967

968+
/**
969+
* Holder for the appropriate kind of method lookup to use. Due to bugs in Java releases, we have
970+
* to evaluate what approach to take at runtime. We do this by emulating the buggy scenarios: can
971+
* a lookup access private details that it should be able to see? If not, we fail down to using
972+
* full private access. Unfortunately, private access doesn't work in the JDK17+.... but it
973+
* shouldn't be necessary there either, because the buggy lookup checks should be fixed.
974+
*/
975+
private static class SuperMethodSupport {
976+
private static final SuperMethodLookup METHOD_LOOKUP;
977+
978+
static {
979+
SuperMethodLookup workingLookup = null;
980+
try {
981+
Class<?> hidden =
982+
Class.forName("com.google.inject.assistedinject.internal.LookupTester$Hidden");
983+
Method method = hidden.getMethod("method");
984+
Field lookupsField = hidden.getEnclosingClass().getDeclaredField("LOOKUP");
985+
lookupsField.setAccessible(true);
986+
MethodHandles.Lookup lookups = (MethodHandles.Lookup) lookupsField.get(null);
987+
for (SuperMethodLookup attempt : SuperMethodLookup.values()) {
988+
try {
989+
attempt.superMethodHandle(method, lookups);
990+
workingLookup = attempt;
991+
break;
992+
} catch (ReflectiveOperationException ignored) {
993+
// Keep looping to find a working lookup
994+
}
995+
}
996+
} catch (ReflectiveOperationException ignored) {
997+
// Bail if our internal tests don't exist.
998+
}
999+
// If everything failed, use the worst option.
1000+
if (workingLookup == null) {
1001+
workingLookup = SuperMethodLookup.PRIVATE_LOOKUP;
1002+
}
1003+
METHOD_LOOKUP = workingLookup;
1004+
}
1005+
}
1006+
9401007
private static MethodHandle superMethodHandle(
941-
Method method, Object proxy, MethodHandles.Lookup userLookups)
1008+
SuperMethodLookup strategy, Method method, Object proxy, MethodHandles.Lookup userLookups)
9421009
throws ReflectiveOperationException {
9431010
MethodHandles.Lookup lookup = userLookups == null ? MethodHandles.lookup() : userLookups;
944-
MethodHandle handle = SUPER_METHOD_LOOKUP.get().superMethodHandle(method, lookup);
1011+
MethodHandle handle = strategy.superMethodHandle(method, lookup);
9451012
return handle != null ? handle.bindTo(proxy) : null;
9461013
}
9471014

948-
// begin by trying unreflectSpecial to find super method handles; this should work on Java14+
949-
private static final AtomicReference<SuperMethodLookup> SUPER_METHOD_LOOKUP =
950-
new AtomicReference<>(SuperMethodLookup.UNREFLECT_SPECIAL);
951-
9521015
private static enum SuperMethodLookup {
9531016
UNREFLECT_SPECIAL {
9541017
@Override
9551018
MethodHandle superMethodHandle(Method method, MethodHandles.Lookup lookup)
9561019
throws ReflectiveOperationException {
957-
try {
958-
return lookup.unreflectSpecial(method, method.getDeclaringClass());
959-
} catch (ReflectiveOperationException e) {
960-
// fall back to findSpecial which should work on Java9+; use that for future lookups
961-
SUPER_METHOD_LOOKUP.compareAndSet(this, FIND_SPECIAL);
962-
return SUPER_METHOD_LOOKUP.get().superMethodHandle(method, lookup);
963-
}
1020+
return lookup.unreflectSpecial(method, method.getDeclaringClass());
9641021
}
9651022
},
9661023
FIND_SPECIAL {
9671024
@Override
9681025
MethodHandle superMethodHandle(Method method, MethodHandles.Lookup lookup)
9691026
throws ReflectiveOperationException {
970-
try {
971-
Class<?> declaringClass = method.getDeclaringClass();
972-
// use findSpecial to workaround https://bugs.openjdk.java.net/browse/JDK-8209005
973-
return lookup.findSpecial(
974-
declaringClass,
975-
method.getName(),
976-
MethodType.methodType(method.getReturnType(), method.getParameterTypes()),
977-
declaringClass);
978-
} catch (ReflectiveOperationException e) {
979-
if (!allowLookupReflection) {
980-
throw e;
981-
}
982-
// fall back to private Lookup which should work on Java8; use that for future lookups
983-
SUPER_METHOD_LOOKUP.compareAndSet(this, PRIVATE_LOOKUP);
984-
return SUPER_METHOD_LOOKUP.get().superMethodHandle(method, lookup);
985-
}
1027+
Class<?> declaringClass = method.getDeclaringClass();
1028+
// Before JDK14, unreflectSpecial didn't work in some scenarios.
1029+
// So we workaround using findSpecial. See: https://bugs.openjdk.java.net/browse/JDK-8209005
1030+
return lookup.findSpecial(
1031+
declaringClass,
1032+
method.getName(),
1033+
MethodType.methodType(method.getReturnType(), method.getParameterTypes()),
1034+
declaringClass);
9861035
}
9871036
},
9881037
PRIVATE_LOOKUP {
9891038
@Override
9901039
MethodHandle superMethodHandle(Method method, MethodHandles.Lookup unused)
9911040
throws ReflectiveOperationException {
1041+
// Even findSpecial fails on JDK8, so we need to manually reflect on private details.
1042+
// But note that this will fail 100% of the time on JDK17+, which doesn't allow reflection
1043+
// into the JDK internals.
9921044
return PrivateLookup.superMethodHandle(method);
9931045
}
9941046
};
@@ -1020,7 +1072,10 @@ private static Constructor<MethodHandles.Lookup> findPrivateLookupCxtor() {
10201072
}
10211073
cxtor.setAccessible(true);
10221074
return cxtor;
1023-
} catch (ReflectiveOperationException | SecurityException e) {
1075+
} catch (Exception e) {
1076+
// Note: we catch Exception because we want to handle InaccessibleObjectException too,
1077+
// but we target JDK8.
1078+
// TODO(sameb): When we drop JDK8 support, catch ReflectiveOperation|Security|Inaccessible
10241079
return null;
10251080
}
10261081
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.google.inject.assistedinject.internal;
2+
3+
import java.lang.invoke.MethodHandles;
4+
5+
/**
6+
* An interface in a different package, so that AssistedInject's main package can't see it. Used at
7+
* runtime to determine which kind of Lookup method we'll support.
8+
*/
9+
class LookupTester {
10+
static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
11+
12+
interface Hidden {
13+
default Hidden method() {
14+
return null;
15+
}
16+
}
17+
18+
private LookupTester() {}
19+
}

0 commit comments

Comments
 (0)