diff --git a/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryModuleBuilder.java b/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryModuleBuilder.java index 562e031fa0..db32626689 100644 --- a/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryModuleBuilder.java +++ b/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryModuleBuilder.java @@ -305,8 +305,9 @@ public FactoryModuleBuilder implement(Key source, TypeLiteralGuice will try to work properly even if this method is not called (or called with a lookups * that doesn't have access to the factory), but doing so requires reflection into the JDK, which diff --git a/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider2.java b/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider2.java index f2806eb41f..0c4d086ffe 100644 --- a/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider2.java +++ b/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider2.java @@ -271,9 +271,9 @@ public TypeLiteral getImplementationType() { continue; } - // Skip default methods that java8 may have created. - if (isDefault(method) && (method.isBridge() || method.isSynthetic())) { - // Even synthetic default methods need the return type validation... + // Skip default methods that java8 may have created (or the user specifies to skip). + if (isSkippableDefaultMethod(factoryRawType, method)) { + // Even default methods need the return type validation... // unavoidable consequence of javac8. :-( validateFactoryReturnType(errors, method.getReturnType(), factoryRawType); defaultMethods.put(method.getName(), method); @@ -388,7 +388,7 @@ public TypeLiteral getImplementationType() { warnedAboutUserLookups = true; logger.log( Level.WARNING, - "AssistedInject factory {0} is non-public and has javac-generated default methods. " + "AssistedInject factory {0} is non-public and has default methods." + " Please pass a `MethodHandles.lookup()` with" + " FactoryModuleBuilder.withLookups when using this factory so that Guice can" + " properly call the default methods. Guice will try to workaround this, but " @@ -436,6 +436,10 @@ public TypeLiteral getImplementationType() { + " public."; if (handle != null) { methodHandleBuilder.put(defaultMethod, handle); + } else if (userSpecifiedDefaultMethod(factoryRawType, defaultMethod)) { + // Don't try to find matching signature for user-specified default methods + errors.addMessage(failureMsg.get()); + throw new IllegalStateException("Can't find method compatible with: " + defaultMethod); } else if (!allowMethodHandleWorkaround) { errors.addMessage(failureMsg.get()); } else { @@ -452,8 +456,7 @@ public TypeLiteral getImplementationType() { } } // We always expect to find at least one match, because we only deal with javac-generated - // default methods. If we ever allow user-specified default methods, this will need to - // change. + // default methods here. if (!foundMatch) { throw new IllegalStateException("Can't find method compatible with: " + defaultMethod); } @@ -473,6 +476,19 @@ public TypeLiteral getImplementationType() { } } + private static boolean isSkippableDefaultMethod(Class factoryRawType, Method method) { + final boolean synthetic = method.isBridge() || method.isSynthetic(); + final boolean annotated = method.isAnnotationPresent(PassthroughDefaultMethods.class) + || factoryRawType.isAnnotationPresent(PassthroughDefaultMethods.class); + return isDefault(method) && (synthetic || annotated); + } + + private static boolean userSpecifiedDefaultMethod(Class factoryRawType, Method defaultMethod) { + return defaultMethod.isAnnotationPresent(PassthroughDefaultMethods.class) + || (factoryRawType.isAnnotationPresent(PassthroughDefaultMethods.class) + && !defaultMethod.isBridge() && !defaultMethod.isSynthetic()); + } + static boolean isDefault(Method method) { // Per the javadoc, default methods are non-abstract, public, non-static. // They're also in interfaces, but we can guarantee that already since we only act diff --git a/extensions/assistedinject/src/com/google/inject/assistedinject/PassthroughDefaultMethods.java b/extensions/assistedinject/src/com/google/inject/assistedinject/PassthroughDefaultMethods.java new file mode 100644 index 0000000000..83d96650e1 --- /dev/null +++ b/extensions/assistedinject/src/com/google/inject/assistedinject/PassthroughDefaultMethods.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * 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 com.google.inject.assistedinject; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.invoke.MethodHandles; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotates a factory interface to indicate its default methods + * should be pass-through on the generated factory implementation, + * instead of treated as standard factory methods. + * + *

This annotation may also be used on individual default methods + * of factory interfaces, but it is named with the assumption that + * the general use case wants default methods treated in a uniform + * fashion for an entire factory.

+ * + * @see FactoryModuleBuilder#withLookups(MethodHandles.Lookup) + */ +@BindingAnnotation +@Target({METHOD, TYPE}) +@Retention(RUNTIME) +public @interface PassthroughDefaultMethods { +} diff --git a/extensions/assistedinject/test/com/google/inject/assistedinject/PassthroughDefaultMethodsTest.java b/extensions/assistedinject/test/com/google/inject/assistedinject/PassthroughDefaultMethodsTest.java new file mode 100644 index 0000000000..598f22e144 --- /dev/null +++ b/extensions/assistedinject/test/com/google/inject/assistedinject/PassthroughDefaultMethodsTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * 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 com.google.inject.assistedinject; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import java.lang.invoke.MethodHandles; +import junit.framework.TestCase; + +public class PassthroughDefaultMethodsTest extends TestCase { + private static class Thing { + final int i; + + @Inject + Thing(@Assisted int i) { + this.i = i; + } + } + + @PassthroughDefaultMethods + private interface Factory { + Thing create(int i); + + default Thing one() { + return this.create(1); + } + + default Thing createPow(int i, int pow) { + return this.create((int) Math.pow(i, pow)); + } + } + + public void testAssistedInjection() throws IllegalAccessException { + MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Factory.class, MethodHandles.lookup()); + Injector injector = + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + install(new FactoryModuleBuilder().withLookups(lookup).build(Factory.class)); + } + }); + Factory factory = injector.getInstance(Factory.class); + assertEquals(1, factory.create(1).i); + assertEquals(1, factory.one().i); + assertEquals(256, factory.createPow(2, 8).i); + } +}