Skip to content

Commit 6b69db1

Browse files
committed
Add @PassthroughDefaultMethods annotation to allow using default methods on assisted factories (but only when explicitly requested by the user, see #1347 (comment))
1 parent ddb6315 commit 6b69db1

File tree

4 files changed

+133
-8
lines changed

4 files changed

+133
-8
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,9 @@ public <T> FactoryModuleBuilder implement(Key<T> source, TypeLiteral<? extends T
305305
* Typically called via {@code withLookups(MethodHandles.lookup())}. Sets the MethodHandles.Lookup
306306
* that the factory implementation will use to call default methods on the factory interface.
307307
* While this is not always required, it is always OK to set it. It is required if the factory
308-
* passed to {@link #build} is non-public and javac generated default methods while compiling it
309-
* (which javac can sometimes do if the factory uses generic types).
308+
* passed to {@link #build} is non-public and has default methods, either javac generated default
309+
* methods (which javac can sometimes emit if the factory uses generic types), or user-specified
310+
* default methods marked by {@link PassthroughDefaultMethods}.
310311
*
311312
* <p>Guice will try to work properly even if this method is not called (or called with a lookups
312313
* that doesn't have access to the factory), but doing so requires reflection into the JDK, which

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,9 @@ public TypeLiteral<?> getImplementationType() {
271271
continue;
272272
}
273273

274-
// Skip default methods that java8 may have created.
275-
if (isDefault(method) && (method.isBridge() || method.isSynthetic())) {
276-
// Even synthetic default methods need the return type validation...
274+
// Skip default methods that java8 may have created (or the user specifies to skip).
275+
if (isSkippableDefaultMethod(factoryRawType, method)) {
276+
// Even default methods need the return type validation...
277277
// unavoidable consequence of javac8. :-(
278278
validateFactoryReturnType(errors, method.getReturnType(), factoryRawType);
279279
defaultMethods.put(method.getName(), method);
@@ -388,7 +388,7 @@ public TypeLiteral<?> getImplementationType() {
388388
warnedAboutUserLookups = true;
389389
logger.log(
390390
Level.WARNING,
391-
"AssistedInject factory {0} is non-public and has javac-generated default methods. "
391+
"AssistedInject factory {0} is non-public and has default methods."
392392
+ " Please pass a `MethodHandles.lookup()` with"
393393
+ " FactoryModuleBuilder.withLookups when using this factory so that Guice can"
394394
+ " properly call the default methods. Guice will try to workaround this, but "
@@ -436,6 +436,10 @@ public TypeLiteral<?> getImplementationType() {
436436
+ " public.";
437437
if (handle != null) {
438438
methodHandleBuilder.put(defaultMethod, handle);
439+
} else if (userSpecifiedDefaultMethod(factoryRawType, defaultMethod)) {
440+
// Don't try to find matching signature for user-specified default methods
441+
errors.addMessage(failureMsg.get());
442+
throw new IllegalStateException("Can't find method compatible with: " + defaultMethod);
439443
} else if (!allowMethodHandleWorkaround) {
440444
errors.addMessage(failureMsg.get());
441445
} else {
@@ -452,8 +456,7 @@ public TypeLiteral<?> getImplementationType() {
452456
}
453457
}
454458
// 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.
459+
// default methods here.
457460
if (!foundMatch) {
458461
throw new IllegalStateException("Can't find method compatible with: " + defaultMethod);
459462
}
@@ -473,6 +476,19 @@ public TypeLiteral<?> getImplementationType() {
473476
}
474477
}
475478

479+
private static boolean isSkippableDefaultMethod(Class<?> factoryRawType, Method method) {
480+
final boolean synthetic = method.isBridge() || method.isSynthetic();
481+
final boolean annotated = method.isAnnotationPresent(PassthroughDefaultMethods.class)
482+
|| factoryRawType.isAnnotationPresent(PassthroughDefaultMethods.class);
483+
return isDefault(method) && (synthetic || annotated);
484+
}
485+
486+
private static boolean userSpecifiedDefaultMethod(Class<?> factoryRawType, Method defaultMethod) {
487+
return defaultMethod.isAnnotationPresent(PassthroughDefaultMethods.class)
488+
|| (factoryRawType.isAnnotationPresent(PassthroughDefaultMethods.class)
489+
&& !defaultMethod.isBridge() && !defaultMethod.isSynthetic());
490+
}
491+
476492
static boolean isDefault(Method method) {
477493
// Per the javadoc, default methods are non-abstract, public, non-static.
478494
// They're also in interfaces, but we can guarantee that already since we only act
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (C) 2023 Google Inc.
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 com.google.inject.assistedinject;
18+
19+
import com.google.inject.BindingAnnotation;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.Target;
22+
import java.lang.invoke.MethodHandles;
23+
24+
import static java.lang.annotation.ElementType.METHOD;
25+
import static java.lang.annotation.ElementType.TYPE;
26+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
27+
28+
/**
29+
* Annotates a factory interface to indicate its default methods
30+
* should be pass-through on the generated factory implementation,
31+
* instead of treated as standard factory methods.
32+
*
33+
* <p>This annotation may also be used on individual default methods
34+
* of factory interfaces, but it is named with the assumption that
35+
* the general use case wants default methods treated in a uniform
36+
* fashion for an entire factory.</p>
37+
*
38+
* @see FactoryModuleBuilder#withLookups(MethodHandles.Lookup)
39+
*/
40+
@BindingAnnotation
41+
@Target({METHOD, TYPE})
42+
@Retention(RUNTIME)
43+
public @interface PassthroughDefaultMethods {
44+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright (C) 2023 Google Inc.
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 com.google.inject.assistedinject;
18+
19+
import com.google.inject.AbstractModule;
20+
import com.google.inject.Guice;
21+
import com.google.inject.Inject;
22+
import com.google.inject.Injector;
23+
import java.lang.invoke.MethodHandles;
24+
import junit.framework.TestCase;
25+
26+
public class PassthroughDefaultMethodsTest extends TestCase {
27+
private static class Thing {
28+
final int i;
29+
30+
@Inject
31+
Thing(@Assisted int i) {
32+
this.i = i;
33+
}
34+
}
35+
36+
@PassthroughDefaultMethods
37+
private interface Factory {
38+
Thing create(int i);
39+
40+
default Thing one() {
41+
return this.create(1);
42+
}
43+
44+
default Thing createPow(int i, int pow) {
45+
return this.create((int) Math.pow(i, pow));
46+
}
47+
}
48+
49+
public void testAssistedInjection() throws IllegalAccessException {
50+
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Factory.class, MethodHandles.lookup());
51+
Injector injector =
52+
Guice.createInjector(
53+
new AbstractModule() {
54+
@Override
55+
protected void configure() {
56+
install(new FactoryModuleBuilder().withLookups(lookup).build(Factory.class));
57+
}
58+
});
59+
Factory factory = injector.getInstance(Factory.class);
60+
assertEquals(1, factory.create(1).i);
61+
assertEquals(1, factory.one().i);
62+
assertEquals(256, factory.createPow(2, 8).i);
63+
}
64+
}

0 commit comments

Comments
 (0)