60
60
import java .lang .invoke .MethodHandles ;
61
61
import java .lang .invoke .MethodType ;
62
62
import java .lang .reflect .Constructor ;
63
+ import java .lang .reflect .Field ;
63
64
import java .lang .reflect .InvocationHandler ;
64
65
import java .lang .reflect .Method ;
65
66
import java .lang .reflect .Modifier ;
71
72
import java .util .List ;
72
73
import java .util .Map ;
73
74
import java .util .Set ;
74
- import java .util .concurrent . atomic . AtomicReference ;
75
+ import java .util .function . Supplier ;
75
76
import java .util .logging .Level ;
76
77
import java .util .logging .Logger ;
77
78
@@ -97,10 +98,18 @@ final class FactoryProvider2<F>
97
98
static final Logger logger = Logger .getLogger (AssistedInject .class .getName ());
98
99
99
100
/**
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.
102
103
*/
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 ;
104
113
105
114
/** if a factory method parameter isn't annotated, it gets this annotation. */
106
115
static final Assisted DEFAULT_ANNOTATION =
@@ -380,52 +389,71 @@ public TypeLiteral<?> getImplementationType() {
380
389
logger .log (
381
390
Level .WARNING ,
382
391
"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"
384
393
+ " 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)." ,
388
396
new Object [] {factoryType });
389
397
}
390
398
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
+
391
408
Method defaultMethod = entry .getValue ();
392
409
MethodHandle handle = null ;
393
410
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
+ }
407
427
}
408
428
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." ;
409
437
if (handle != null ) {
410
438
methodHandleBuilder .put (defaultMethod , handle );
439
+ } else if (!allowMethodHandleWorkaround ) {
440
+ errors .addMessage (failureMsg .get ());
411
441
} else {
412
- // TODO: remove this workaround when Java8 support is dropped
413
442
boolean foundMatch = false ;
414
443
for (Method otherMethod : otherMethods .get (defaultMethod .getName ())) {
415
444
if (dataSoFar .containsKey (otherMethod ) && isCompatible (defaultMethod , otherMethod )) {
416
445
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 ;
423
448
} else {
424
449
assistDataBuilder .put (defaultMethod , dataSoFar .get (otherMethod ));
425
450
foundMatch = true ;
426
451
}
427
452
}
428
453
}
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.
429
457
if (!foundMatch ) {
430
458
throw new IllegalStateException ("Can't find method compatible with: " + defaultMethod );
431
459
}
@@ -937,58 +965,82 @@ protected Object initialValue() {
937
965
}
938
966
}
939
967
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
+
940
1007
private static MethodHandle superMethodHandle (
941
- Method method , Object proxy , MethodHandles .Lookup userLookups )
1008
+ SuperMethodLookup strategy , Method method , Object proxy , MethodHandles .Lookup userLookups )
942
1009
throws ReflectiveOperationException {
943
1010
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 );
945
1012
return handle != null ? handle .bindTo (proxy ) : null ;
946
1013
}
947
1014
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
-
952
1015
private static enum SuperMethodLookup {
953
1016
UNREFLECT_SPECIAL {
954
1017
@ Override
955
1018
MethodHandle superMethodHandle (Method method , MethodHandles .Lookup lookup )
956
1019
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 ());
964
1021
}
965
1022
},
966
1023
FIND_SPECIAL {
967
1024
@ Override
968
1025
MethodHandle superMethodHandle (Method method , MethodHandles .Lookup lookup )
969
1026
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 );
986
1035
}
987
1036
},
988
1037
PRIVATE_LOOKUP {
989
1038
@ Override
990
1039
MethodHandle superMethodHandle (Method method , MethodHandles .Lookup unused )
991
1040
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.
992
1044
return PrivateLookup .superMethodHandle (method );
993
1045
}
994
1046
};
@@ -1020,7 +1072,10 @@ private static Constructor<MethodHandles.Lookup> findPrivateLookupCxtor() {
1020
1072
}
1021
1073
cxtor .setAccessible (true );
1022
1074
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
1024
1079
return null ;
1025
1080
}
1026
1081
}
0 commit comments