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 5, 2023
1 parent ddb6315 commit 1373b06
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,9 @@ public <T> FactoryModuleBuilder implement(Key<T> source, TypeLiteral<? extends T
* Typically called via {@code withLookups(MethodHandles.lookup())}. Sets the MethodHandles.Lookup
* that the factory implementation will use to call default methods on the factory interface.
* While this is not always required, it is always OK to set it. It is required if the factory
* passed to {@link #build} is non-public and javac generated default methods while compiling it
* (which javac can sometimes do if the factory uses generic types).
* passed to {@link #build} is non-public and has default methods, either javac generated default
* methods (which javac can sometimes emit if the factory uses generic types), or user-specified
* default methods marked by {@link PassthroughDefaultMethods}.
*
* <p>Guice 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -436,6 +436,10 @@ public TypeLiteral<?> getImplementationType() {
+ " public.";
if (handle != null) {
methodHandleBuilder.put(defaultMethod, handle);
} else if (isUserSpecifiedDefaultMethod(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 +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);
}
Expand All @@ -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 isUserSpecifiedDefaultMethod(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 1373b06

Please sign in to comment.