Skip to content

Commit

Permalink
Add @PassthroughDefaultMethods annotation to allow using default me…
Browse files Browse the repository at this point in the history
…thods on assisted factories (but only when explicitly requested by the user, see #1347 (comment))
  • Loading branch information
jpenilla committed Aug 4, 2023
1 parent ddb6315 commit 41695cb
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -271,9 +271,12 @@ 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 (isDefault(method)
&& (method.isBridge() || method.isSynthetic()
|| method.isAnnotationPresent(PassthroughDefaultMethods.class)
|| factoryRawType.isAnnotationPresent(PassthroughDefaultMethods.class))) {
// Even default methods need the return type validation...
// unavoidable consequence of javac8. :-(
validateFactoryReturnType(errors, method.getReturnType(), factoryRawType);
defaultMethods.put(method.getName(), method);
Expand Down Expand Up @@ -388,7 +391,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 "
Expand Down Expand Up @@ -436,6 +439,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 {
Expand All @@ -452,8 +459,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);
}
Expand All @@ -473,6 +479,12 @@ public TypeLiteral<?> getImplementationType() {
}
}

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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.</p>
*
* @see FactoryModuleBuilder#withLookups(MethodHandles.Lookup)
*/
@BindingAnnotation
@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface PassthroughDefaultMethods {
}
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit 41695cb

Please sign in to comment.