933 lines
40 KiB
Java
933 lines
40 KiB
Java
package com.google.inject.assistedinject;
|
|
|
|
import com.google.common.base.MoreObjects;
|
|
import com.google.common.base.Objects;
|
|
import com.google.common.collect.HashMultimap;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableMap;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import com.google.common.collect.Iterables;
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.collect.Multimap;
|
|
import com.google.common.collect.Sets;
|
|
import com.google.inject.AbstractModule;
|
|
import com.google.inject.Binder;
|
|
import com.google.inject.Binding;
|
|
import com.google.inject.ConfigurationException;
|
|
import com.google.inject.Inject;
|
|
import com.google.inject.Injector;
|
|
import com.google.inject.Key;
|
|
import com.google.inject.Module;
|
|
import com.google.inject.Provider;
|
|
import com.google.inject.ProvisionException;
|
|
import com.google.inject.Scopes;
|
|
import com.google.inject.TypeLiteral;
|
|
import com.google.inject.internal.Annotations;
|
|
import com.google.inject.internal.Errors;
|
|
import com.google.inject.internal.ErrorsException;
|
|
import com.google.inject.internal.UniqueAnnotations;
|
|
import com.google.inject.internal.util.Classes;
|
|
import com.google.inject.spi.BindingTargetVisitor;
|
|
import com.google.inject.spi.Dependency;
|
|
import com.google.inject.spi.HasDependencies;
|
|
import com.google.inject.spi.InjectionPoint;
|
|
import com.google.inject.spi.Message;
|
|
import com.google.inject.spi.ProviderInstanceBinding;
|
|
import com.google.inject.spi.ProviderWithExtensionVisitor;
|
|
import com.google.inject.spi.Toolable;
|
|
import com.google.inject.util.Providers;
|
|
|
|
import java.lang.annotation.Annotation;
|
|
import java.lang.reflect.Constructor;
|
|
import java.lang.reflect.InvocationHandler;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.Modifier;
|
|
import java.lang.reflect.Proxy;
|
|
import java.util.Arrays;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
import static com.google.common.base.Preconditions.checkState;
|
|
import static com.google.common.collect.Iterables.getOnlyElement;
|
|
|
|
/**
|
|
* The newer implementation of factory provider. This implementation uses a child injector to
|
|
* create values.
|
|
*/
|
|
final class FactoryProvider2<F> implements InvocationHandler,
|
|
ProviderWithExtensionVisitor<F>, HasDependencies, AssistedInjectBinding<F> {
|
|
|
|
/**
|
|
* A constant annotation to denote the return value, instead of creating a new one each time.
|
|
*/
|
|
static final Annotation RETURN_ANNOTATION = UniqueAnnotations.create();
|
|
|
|
// use the logger under a well-known name, not FactoryProvider2
|
|
static final Logger logger = Logger.getLogger(AssistedInject.class.getName());
|
|
|
|
/**
|
|
* if a factory method parameter isn't annotated, it gets this annotation.
|
|
*/
|
|
static final Assisted DEFAULT_ANNOTATION = new Assisted() {
|
|
public String value() {
|
|
return "";
|
|
}
|
|
|
|
public Class<? extends Annotation> annotationType() {
|
|
return Assisted.class;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
return o instanceof Assisted && ((Assisted) o).value().isEmpty();
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return 127 * "value".hashCode() ^ "".hashCode();
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "@" + Assisted.class.getName() + "(value=)";
|
|
}
|
|
};
|
|
/**
|
|
* Mapping from method to the data about how the method will be assisted.
|
|
*/
|
|
private final ImmutableMap<Method, AssistData> assistDataByMethod;
|
|
/**
|
|
* Mapping from method to method handle, for generated default methods.
|
|
*/
|
|
private final ImmutableMap<Method, MethodHandleWrapper> methodHandleByMethod;
|
|
/**
|
|
* the factory interface, implemented and provided
|
|
*/
|
|
private final F factory;
|
|
/**
|
|
* The key that this is bound to.
|
|
*/
|
|
private final Key<F> factoryKey;
|
|
/**
|
|
* The binding collector, for equality/hashing purposes.
|
|
*/
|
|
private final BindingCollector collector;
|
|
/**
|
|
* the hosting injector, or null if we haven't been initialized yet
|
|
*/
|
|
private Injector injector;
|
|
|
|
/**
|
|
* @param factoryKey a key for a Java interface that defines one or more create methods.
|
|
* @param collector binding configuration that maps method return types to
|
|
* implementation types.
|
|
*/
|
|
FactoryProvider2(Key<F> factoryKey, BindingCollector collector) {
|
|
this.factoryKey = factoryKey;
|
|
this.collector = collector;
|
|
|
|
TypeLiteral<F> factoryType = factoryKey.getTypeLiteral();
|
|
Errors errors = new Errors();
|
|
|
|
@SuppressWarnings("unchecked") // we imprecisely treat the class literal of T as a Class<T>
|
|
Class<F> factoryRawType = (Class<F>) (Class<?>) factoryType.getRawType();
|
|
|
|
try {
|
|
if (!factoryRawType.isInterface()) {
|
|
throw errors.addMessage("%s must be an interface.", factoryRawType).toException();
|
|
}
|
|
|
|
Multimap<String, Method> defaultMethods = HashMultimap.create();
|
|
Multimap<String, Method> otherMethods = HashMultimap.create();
|
|
ImmutableMap.Builder<Method, AssistData> assistDataBuilder = ImmutableMap.builder();
|
|
// TODO: also grab methods from superinterfaces
|
|
for (Method method : factoryRawType.getMethods()) {
|
|
// Skip default methods that java8 may have created.
|
|
if (isDefault(method) && (method.isBridge() || method.isSynthetic())) {
|
|
// Even synthetic default methods need the return type validation...
|
|
// unavoidable consequence of javac8. :-(
|
|
validateFactoryReturnType(errors, method.getReturnType(), factoryRawType);
|
|
defaultMethods.put(method.getName(), method);
|
|
continue;
|
|
}
|
|
otherMethods.put(method.getName(), method);
|
|
|
|
TypeLiteral<?> returnTypeLiteral = factoryType.getReturnType(method);
|
|
Key<?> returnType;
|
|
try {
|
|
returnType = Annotations.getKey(returnTypeLiteral, method, method.getAnnotations(), errors);
|
|
} catch (ConfigurationException ce) {
|
|
// If this was an error due to returnTypeLiteral not being specified, rephrase
|
|
// it as our factory not being specified, so it makes more sense to users.
|
|
if (isTypeNotSpecified(returnTypeLiteral, ce)) {
|
|
throw errors.keyNotFullySpecified(TypeLiteral.get(factoryRawType)).toException();
|
|
} else {
|
|
throw ce;
|
|
}
|
|
}
|
|
validateFactoryReturnType(errors, returnType.getTypeLiteral().getRawType(), factoryRawType);
|
|
List<TypeLiteral<?>> params = factoryType.getParameterTypes(method);
|
|
Annotation[][] paramAnnotations = method.getParameterAnnotations();
|
|
int p = 0;
|
|
List<Key<?>> keys = Lists.newArrayList();
|
|
for (TypeLiteral<?> param : params) {
|
|
Key<?> paramKey = Annotations.getKey(param, method, paramAnnotations[p++], errors);
|
|
Class<?> underlylingType = paramKey.getTypeLiteral().getRawType();
|
|
if (underlylingType.equals(Provider.class)
|
|
|| underlylingType.equals(javax.inject.Provider.class)) {
|
|
errors.addMessage("A Provider may not be a type in a factory method of an AssistedInject."
|
|
+ "\n Offending instance is parameter [%s] with key [%s] on method [%s]",
|
|
p, paramKey, method);
|
|
}
|
|
keys.add(assistKey(method, paramKey, errors));
|
|
}
|
|
ImmutableList<Key<?>> immutableParamList = ImmutableList.copyOf(keys);
|
|
|
|
// try to match up the method to the constructor
|
|
TypeLiteral<?> implementation = collector.getBindings().get(returnType);
|
|
if (implementation == null) {
|
|
implementation = returnType.getTypeLiteral();
|
|
}
|
|
Class<? extends Annotation> scope =
|
|
Annotations.findScopeAnnotation(errors, implementation.getRawType());
|
|
if (scope != null) {
|
|
errors.addMessage("Found scope annotation [%s] on implementation class "
|
|
+ "[%s] of AssistedInject factory [%s].\nThis is not allowed, please"
|
|
+ " remove the scope annotation.",
|
|
scope, implementation.getRawType(), factoryType);
|
|
}
|
|
|
|
InjectionPoint ctorInjectionPoint;
|
|
try {
|
|
ctorInjectionPoint =
|
|
findMatchingConstructorInjectionPoint(method, returnType, implementation, immutableParamList);
|
|
} catch (ErrorsException ee) {
|
|
errors.merge(ee.getErrors());
|
|
continue;
|
|
}
|
|
|
|
Constructor<?> constructor = (Constructor<?>) ctorInjectionPoint.getMember();
|
|
List<ThreadLocalProvider> providers = Collections.emptyList();
|
|
Set<Dependency<?>> deps = getDependencies(ctorInjectionPoint, implementation);
|
|
boolean optimized = false;
|
|
// Now go through all dependencies of the implementation and see if it is OK to
|
|
// use an optimized form of assistedinject2. The optimized form requires that
|
|
// all injections directly inject the object itself (and not a Provider of the object,
|
|
// or an Injector), because it caches a single child injector and mutates the Provider
|
|
// of the arguments in a ThreadLocal.
|
|
if (isValidForOptimizedAssistedInject(deps, implementation.getRawType(), factoryType)) {
|
|
ImmutableList.Builder<ThreadLocalProvider> providerListBuilder = ImmutableList.builder();
|
|
for (int i = 0; i < params.size(); i++) {
|
|
providerListBuilder.add(new ThreadLocalProvider());
|
|
}
|
|
providers = providerListBuilder.build();
|
|
optimized = true;
|
|
}
|
|
|
|
AssistData data = new AssistData(constructor,
|
|
returnType,
|
|
immutableParamList,
|
|
implementation,
|
|
method,
|
|
removeAssistedDeps(deps),
|
|
optimized,
|
|
providers);
|
|
assistDataBuilder.put(method, data);
|
|
}
|
|
|
|
factory = factoryRawType.cast(Proxy.newProxyInstance(
|
|
factoryRawType.getClassLoader(), new Class<?>[]{factoryRawType}, this));
|
|
|
|
// Now go back through default methods. Try to use MethodHandles to make things
|
|
// work. If that doesn't work, fallback to trying to find compatible method
|
|
// signatures.
|
|
Map<Method, AssistData> dataSoFar = assistDataBuilder.build();
|
|
ImmutableMap.Builder<Method, MethodHandleWrapper> methodHandleBuilder = ImmutableMap.builder();
|
|
for (Map.Entry<String, Method> entry : defaultMethods.entries()) {
|
|
Method defaultMethod = entry.getValue();
|
|
MethodHandleWrapper handle = MethodHandleWrapper.create(defaultMethod, factory);
|
|
if (handle != null) {
|
|
methodHandleBuilder.put(defaultMethod, handle);
|
|
} else {
|
|
boolean foundMatch = false;
|
|
for (Method otherMethod : otherMethods.get(defaultMethod.getName())) {
|
|
if (dataSoFar.containsKey(otherMethod) && isCompatible(defaultMethod, otherMethod)) {
|
|
if (foundMatch) {
|
|
errors.addMessage("Generated default method %s with parameters %s is"
|
|
+ " signature-compatible with more than one non-default method."
|
|
+ " Unable to create factory. As a workaround, remove the override"
|
|
+ " so javac stops generating a default method.",
|
|
defaultMethod, Arrays.asList(defaultMethod.getParameterTypes()));
|
|
} else {
|
|
assistDataBuilder.put(defaultMethod, dataSoFar.get(otherMethod));
|
|
foundMatch = true;
|
|
}
|
|
}
|
|
}
|
|
if (!foundMatch) {
|
|
throw new IllegalStateException("Can't find method compatible with: " + defaultMethod);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we generated any errors (from finding matching constructors, for instance), throw an exception.
|
|
if (errors.hasErrors()) {
|
|
throw errors.toException();
|
|
}
|
|
|
|
assistDataByMethod = assistDataBuilder.build();
|
|
methodHandleByMethod = methodHandleBuilder.build();
|
|
} catch (ErrorsException e) {
|
|
throw new ConfigurationException(e.getErrors().getMessages());
|
|
}
|
|
}
|
|
|
|
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
|
|
// on interfaces.
|
|
return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC))
|
|
== Modifier.PUBLIC;
|
|
}
|
|
|
|
/**
|
|
* Returns true if {@code thrown} can be thrown by {@code invoked} without wrapping.
|
|
*/
|
|
static boolean canRethrow(Method invoked, Throwable thrown) {
|
|
if (thrown instanceof Error || thrown instanceof RuntimeException) {
|
|
return true;
|
|
}
|
|
|
|
for (Class<?> declared : invoked.getExceptionTypes()) {
|
|
if (declared.isInstance(thrown)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private boolean isCompatible(Method src, Method dst) {
|
|
if (!src.getReturnType().isAssignableFrom(dst.getReturnType())) {
|
|
return false;
|
|
}
|
|
Class<?>[] srcParams = src.getParameterTypes();
|
|
Class<?>[] dstParams = dst.getParameterTypes();
|
|
if (srcParams.length != dstParams.length) {
|
|
return false;
|
|
}
|
|
for (int i = 0; i < srcParams.length; i++) {
|
|
if (!srcParams[i].isAssignableFrom(dstParams[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public F get() {
|
|
return factory;
|
|
}
|
|
|
|
public Set<Dependency<?>> getDependencies() {
|
|
Set<Dependency<?>> combinedDeps = new HashSet<Dependency<?>>();
|
|
for (AssistData data : assistDataByMethod.values()) {
|
|
combinedDeps.addAll(data.dependencies);
|
|
}
|
|
return ImmutableSet.copyOf(combinedDeps);
|
|
}
|
|
|
|
public Key<F> getKey() {
|
|
return factoryKey;
|
|
}
|
|
|
|
// Safe cast because values are typed to AssistedData, which is an AssistedMethod, and
|
|
// the collection is immutable.
|
|
@SuppressWarnings("unchecked")
|
|
public Collection<AssistedMethod> getAssistedMethods() {
|
|
return (Collection<AssistedMethod>) (Collection<?>) assistDataByMethod.values();
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
public <T, V> V acceptExtensionVisitor(BindingTargetVisitor<T, V> visitor,
|
|
ProviderInstanceBinding<? extends T> binding) {
|
|
if (visitor instanceof AssistedInjectTargetVisitor) {
|
|
return ((AssistedInjectTargetVisitor<T, V>) visitor).visit((AssistedInjectBinding<T>) this);
|
|
}
|
|
return visitor.visit(binding);
|
|
}
|
|
|
|
private void validateFactoryReturnType(Errors errors, Class<?> returnType, Class<?> factoryType) {
|
|
if (Modifier.isPublic(factoryType.getModifiers())
|
|
&& !Modifier.isPublic(returnType.getModifiers())) {
|
|
errors.addMessage("%s is public, but has a method that returns a non-public type: %s. "
|
|
+ "Due to limitations with java.lang.reflect.Proxy, this is not allowed. "
|
|
+ "Please either make the factory non-public or the return type public.",
|
|
factoryType, returnType);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if the ConfigurationException is due to an error of TypeLiteral not being fully
|
|
* specified.
|
|
*/
|
|
private boolean isTypeNotSpecified(TypeLiteral<?> typeLiteral, ConfigurationException ce) {
|
|
Collection<Message> messages = ce.getErrorMessages();
|
|
if (messages.size() == 1) {
|
|
Message msg = Iterables.getOnlyElement(
|
|
new Errors().keyNotFullySpecified(typeLiteral).getMessages());
|
|
return msg.getMessage().equals(Iterables.getOnlyElement(messages).getMessage());
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds a constructor suitable for the method. If the implementation contained any constructors
|
|
* marked with {@link AssistedInject}, this requires all {@link Assisted} parameters to exactly
|
|
* match the parameters (in any order) listed in the method. Otherwise, if no
|
|
* {@link AssistedInject} constructors exist, this will default to looking for an
|
|
* {@literal @}{@link Inject} constructor.
|
|
*/
|
|
private <T> InjectionPoint findMatchingConstructorInjectionPoint(
|
|
Method method, Key<?> returnType, TypeLiteral<T> implementation, List<Key<?>> paramList)
|
|
throws ErrorsException {
|
|
Errors errors = new Errors(method);
|
|
if (returnType.getTypeLiteral().equals(implementation)) {
|
|
errors = errors.withSource(implementation);
|
|
} else {
|
|
errors = errors.withSource(returnType).withSource(implementation);
|
|
}
|
|
|
|
Class<?> rawType = implementation.getRawType();
|
|
if (Modifier.isInterface(rawType.getModifiers())) {
|
|
errors.addMessage(
|
|
"%s is an interface, not a concrete class. Unable to create AssistedInject factory.",
|
|
implementation);
|
|
throw errors.toException();
|
|
} else if (Modifier.isAbstract(rawType.getModifiers())) {
|
|
errors.addMessage(
|
|
"%s is abstract, not a concrete class. Unable to create AssistedInject factory.",
|
|
implementation);
|
|
throw errors.toException();
|
|
} else if (Classes.isInnerClass(rawType)) {
|
|
errors.cannotInjectInnerClass(rawType);
|
|
throw errors.toException();
|
|
}
|
|
|
|
Constructor<?> matchingConstructor = null;
|
|
boolean anyAssistedInjectConstructors = false;
|
|
// Look for AssistedInject constructors...
|
|
for (Constructor<?> constructor : rawType.getDeclaredConstructors()) {
|
|
if (constructor.isAnnotationPresent(AssistedInject.class)) {
|
|
anyAssistedInjectConstructors = true;
|
|
if (constructorHasMatchingParams(implementation, constructor, paramList, errors)) {
|
|
if (matchingConstructor != null) {
|
|
errors
|
|
.addMessage(
|
|
"%s has more than one constructor annotated with @AssistedInject"
|
|
+ " that matches the parameters in method %s. Unable to create "
|
|
+ "AssistedInject factory.",
|
|
implementation, method);
|
|
throw errors.toException();
|
|
} else {
|
|
matchingConstructor = constructor;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!anyAssistedInjectConstructors) {
|
|
// If none existed, use @Inject.
|
|
try {
|
|
return InjectionPoint.forConstructorOf(implementation);
|
|
} catch (ConfigurationException e) {
|
|
errors.merge(e.getErrorMessages());
|
|
throw errors.toException();
|
|
}
|
|
} else {
|
|
// Otherwise, use it or fail with a good error message.
|
|
if (matchingConstructor != null) {
|
|
// safe because we got the constructor from this implementation.
|
|
@SuppressWarnings("unchecked")
|
|
InjectionPoint ip = InjectionPoint.forConstructor(
|
|
(Constructor<? super T>) matchingConstructor, implementation);
|
|
return ip;
|
|
} else {
|
|
errors.addMessage(
|
|
"%s has @AssistedInject constructors, but none of them match the"
|
|
+ " parameters in method %s. Unable to create AssistedInject factory.",
|
|
implementation, method);
|
|
throw errors.toException();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Matching logic for constructors annotated with AssistedInject.
|
|
* This returns true if and only if all @Assisted parameters in the
|
|
* constructor exactly match (in any order) all @Assisted parameters
|
|
* the method's parameter.
|
|
*/
|
|
private boolean constructorHasMatchingParams(TypeLiteral<?> type,
|
|
Constructor<?> constructor, List<Key<?>> paramList, Errors errors)
|
|
throws ErrorsException {
|
|
List<TypeLiteral<?>> params = type.getParameterTypes(constructor);
|
|
Annotation[][] paramAnnotations = constructor.getParameterAnnotations();
|
|
int p = 0;
|
|
List<Key<?>> constructorKeys = Lists.newArrayList();
|
|
for (TypeLiteral<?> param : params) {
|
|
Key<?> paramKey = Annotations.getKey(param, constructor, paramAnnotations[p++],
|
|
errors);
|
|
constructorKeys.add(paramKey);
|
|
}
|
|
// Require that every key exist in the constructor to match up exactly.
|
|
for (Key<?> key : paramList) {
|
|
// If it didn't exist in the constructor set, we can't use it.
|
|
if (!constructorKeys.remove(key)) {
|
|
return false;
|
|
}
|
|
}
|
|
// If any keys remain and their annotation is Assisted, we can't use it.
|
|
for (Key<?> key : constructorKeys) {
|
|
if (key.getAnnotationType() == Assisted.class) {
|
|
return false;
|
|
}
|
|
}
|
|
// All @Assisted params match up to the method's parameters.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Calculates all dependencies required by the implementation and constructor.
|
|
*/
|
|
private Set<Dependency<?>> getDependencies(InjectionPoint ctorPoint, TypeLiteral<?> implementation) {
|
|
ImmutableSet.Builder<Dependency<?>> builder = ImmutableSet.builder();
|
|
builder.addAll(ctorPoint.getDependencies());
|
|
if (!implementation.getRawType().isInterface()) {
|
|
for (InjectionPoint ip : InjectionPoint.forInstanceMethodsAndFields(implementation)) {
|
|
builder.addAll(ip.getDependencies());
|
|
}
|
|
}
|
|
return builder.build();
|
|
}
|
|
|
|
/**
|
|
* Return all non-assisted dependencies.
|
|
*/
|
|
private Set<Dependency<?>> removeAssistedDeps(Set<Dependency<?>> deps) {
|
|
ImmutableSet.Builder<Dependency<?>> builder = ImmutableSet.builder();
|
|
for (Dependency<?> dep : deps) {
|
|
Class<?> annotationType = dep.getKey().getAnnotationType();
|
|
if (annotationType == null || !annotationType.equals(Assisted.class)) {
|
|
builder.add(dep);
|
|
}
|
|
}
|
|
return builder.build();
|
|
}
|
|
|
|
/**
|
|
* Returns true if all dependencies are suitable for the optimized version of AssistedInject. The
|
|
* optimized version caches the binding & uses a ThreadLocal Provider, so can only be applied if
|
|
* the assisted bindings are immediately provided. This looks for hints that the values may be
|
|
* lazily retrieved, by looking for injections of Injector or a Provider for the assisted values.
|
|
*/
|
|
private boolean isValidForOptimizedAssistedInject(Set<Dependency<?>> dependencies,
|
|
Class<?> implementation, TypeLiteral<?> factoryType) {
|
|
Set<Dependency<?>> badDeps = null; // optimization: create lazily
|
|
for (Dependency<?> dep : dependencies) {
|
|
if (isInjectorOrAssistedProvider(dep)) {
|
|
if (badDeps == null) {
|
|
badDeps = Sets.newHashSet();
|
|
}
|
|
badDeps.add(dep);
|
|
}
|
|
}
|
|
if (badDeps != null && !badDeps.isEmpty()) {
|
|
logger.log(Level.WARNING, "AssistedInject factory {0} will be slow "
|
|
+ "because {1} has assisted Provider dependencies or injects the Injector. "
|
|
+ "Stop injecting @Assisted Provider<T> (instead use @Assisted T) "
|
|
+ "or Injector to speed things up. (It will be a ~6500% speed bump!) "
|
|
+ "The exact offending deps are: {2}",
|
|
new Object[]{factoryType, implementation, badDeps});
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the dependency is for {@link Injector} or if the dependency
|
|
* is a {@link Provider} for a parameter that is {@literal @}{@link Assisted}.
|
|
*/
|
|
private boolean isInjectorOrAssistedProvider(Dependency<?> dependency) {
|
|
Class<?> annotationType = dependency.getKey().getAnnotationType();
|
|
if (annotationType != null && annotationType.equals(Assisted.class)) { // If it's assisted..
|
|
if (dependency.getKey().getTypeLiteral().getRawType().equals(Provider.class)) { // And a Provider...
|
|
return true;
|
|
}
|
|
} else if (dependency.getKey().getTypeLiteral().getRawType().equals(Injector.class)) { // If it's the Injector...
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns a key similar to {@code key}, but with an {@literal @}Assisted binding annotation.
|
|
* This fails if another binding annotation is clobbered in the process. If the key already has
|
|
* the {@literal @}Assisted annotation, it is returned as-is to preserve any String value.
|
|
*/
|
|
private <T> Key<T> assistKey(Method method, Key<T> key, Errors errors) throws ErrorsException {
|
|
if (key.getAnnotationType() == null) {
|
|
return Key.get(key.getTypeLiteral(), DEFAULT_ANNOTATION);
|
|
} else if (key.getAnnotationType() == Assisted.class) {
|
|
return key;
|
|
} else {
|
|
errors.withSource(method).addMessage(
|
|
"Only @Assisted is allowed for factory parameters, but found @%s",
|
|
key.getAnnotationType());
|
|
throw errors.toException();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* At injector-creation time, we initialize the invocation handler. At this time we make sure
|
|
* all factory methods will be able to build the target types.
|
|
*/
|
|
@Inject
|
|
@Toolable
|
|
void initialize(Injector injector) {
|
|
if (this.injector != null) {
|
|
throw new ConfigurationException(ImmutableList.of(new Message(FactoryProvider2.class,
|
|
"Factories.create() factories may only be used in one Injector!")));
|
|
}
|
|
|
|
this.injector = injector;
|
|
|
|
for (Map.Entry<Method, AssistData> entry : assistDataByMethod.entrySet()) {
|
|
Method method = entry.getKey();
|
|
AssistData data = entry.getValue();
|
|
Object[] args;
|
|
if (!data.optimized) {
|
|
args = new Object[method.getParameterTypes().length];
|
|
Arrays.fill(args, "dummy object for validating Factories");
|
|
} else {
|
|
args = null; // won't be used -- instead will bind to data.providers.
|
|
}
|
|
getBindingFromNewInjector(method, args, data); // throws if the binding isn't properly configured
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a child injector that binds the args, and returns the binding for the method's result.
|
|
*/
|
|
public Binding<?> getBindingFromNewInjector(
|
|
final Method method, final Object[] args, final AssistData data) {
|
|
checkState(injector != null,
|
|
"Factories.create() factories cannot be used until they're initialized by Guice.");
|
|
|
|
final Key<?> returnType = data.returnType;
|
|
|
|
// We ignore any pre-existing binding annotation.
|
|
final Key<?> returnKey = Key.get(returnType.getTypeLiteral(), RETURN_ANNOTATION);
|
|
|
|
Module assistedModule = new AbstractModule() {
|
|
@Override
|
|
@SuppressWarnings({
|
|
"unchecked", "rawtypes"}) // raw keys are necessary for the args array and return value
|
|
protected void configure() {
|
|
Binder binder = binder().withSource(method);
|
|
|
|
int p = 0;
|
|
if (!data.optimized) {
|
|
for (Key<?> paramKey : data.paramTypes) {
|
|
// Wrap in a Provider to cover null, and to prevent Guice from injecting the parameter
|
|
binder.bind((Key) paramKey).toProvider(Providers.of(args[p++]));
|
|
}
|
|
} else {
|
|
for (Key<?> paramKey : data.paramTypes) {
|
|
// Bind to our ThreadLocalProviders.
|
|
binder.bind((Key) paramKey).toProvider(data.providers.get(p++));
|
|
}
|
|
}
|
|
|
|
Constructor constructor = data.constructor;
|
|
// Constructor *should* always be non-null here,
|
|
// but if it isn't, we'll end up throwing a fairly good error
|
|
// message for the user.
|
|
if (constructor != null) {
|
|
binder.bind(returnKey)
|
|
.toConstructor(constructor, (TypeLiteral) data.implementationType)
|
|
.in(Scopes.NO_SCOPE); // make sure we erase any scope on the implementation type
|
|
}
|
|
}
|
|
};
|
|
|
|
Injector forCreate = injector.createChildInjector(assistedModule);
|
|
Binding<?> binding = forCreate.getBinding(returnKey);
|
|
// If we have providers cached in data, cache the binding for future optimizations.
|
|
if (data.optimized) {
|
|
data.cachedBinding = binding;
|
|
}
|
|
return binding;
|
|
}
|
|
|
|
/**
|
|
* When a factory method is invoked, we create a child injector that binds all parameters, then
|
|
* use that to get an instance of the return type.
|
|
*/
|
|
public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
|
|
// If we setup a method handle earlier for this method, call it.
|
|
// This is necessary for default methods that java8 creates, so we
|
|
// can call the default method implementation (and not our proxied version of it).
|
|
if (methodHandleByMethod.containsKey(method)) {
|
|
return methodHandleByMethod.get(method).invokeWithArguments(args);
|
|
}
|
|
|
|
if (method.getDeclaringClass().equals(Object.class)) {
|
|
if ("equals".equals(method.getName())) {
|
|
return proxy == args[0];
|
|
} else if ("hashCode".equals(method.getName())) {
|
|
return System.identityHashCode(proxy);
|
|
} else {
|
|
return method.invoke(this, args);
|
|
}
|
|
}
|
|
|
|
AssistData data = assistDataByMethod.get(method);
|
|
checkState(data != null, "No data for method: %s", method);
|
|
Provider<?> provider;
|
|
if (data.cachedBinding != null) { // Try to get optimized form...
|
|
provider = data.cachedBinding.getProvider();
|
|
} else {
|
|
provider = getBindingFromNewInjector(method, args, data).getProvider();
|
|
}
|
|
try {
|
|
int p = 0;
|
|
for (ThreadLocalProvider tlp : data.providers) {
|
|
tlp.set(args[p++]);
|
|
}
|
|
return provider.get();
|
|
} catch (ProvisionException e) {
|
|
// if this is an exception declared by the factory method, throw it as-is
|
|
if (e.getErrorMessages().size() == 1) {
|
|
Message onlyError = getOnlyElement(e.getErrorMessages());
|
|
Throwable cause = onlyError.getCause();
|
|
if (cause != null && canRethrow(method, cause)) {
|
|
throw cause;
|
|
}
|
|
}
|
|
throw e;
|
|
} finally {
|
|
for (ThreadLocalProvider tlp : data.providers) {
|
|
tlp.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return factory.getClass().getInterfaces()[0].getName();
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hashCode(factoryKey, collector);
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (!(obj instanceof FactoryProvider2)) {
|
|
return false;
|
|
}
|
|
FactoryProvider2<?> other = (FactoryProvider2<?>) obj;
|
|
return factoryKey.equals(other.factoryKey) && Objects.equal(collector, other.collector);
|
|
}
|
|
|
|
/**
|
|
* All the data necessary to perform an assisted inject.
|
|
*/
|
|
private static class AssistData implements AssistedMethod {
|
|
/**
|
|
* the constructor the implementation is constructed with.
|
|
*/
|
|
final Constructor<?> constructor;
|
|
/**
|
|
* the return type in the factory method that the constructor is bound to.
|
|
*/
|
|
final Key<?> returnType;
|
|
/**
|
|
* the parameters in the factory method associated with this data.
|
|
*/
|
|
final ImmutableList<Key<?>> paramTypes;
|
|
/**
|
|
* the type of the implementation constructed
|
|
*/
|
|
final TypeLiteral<?> implementationType;
|
|
|
|
/**
|
|
* All non-assisted dependencies required by this method.
|
|
*/
|
|
final Set<Dependency<?>> dependencies;
|
|
/**
|
|
* The factory method associated with this data
|
|
*/
|
|
final Method factoryMethod;
|
|
|
|
/**
|
|
* true if {@link #isValidForOptimizedAssistedInject} returned true.
|
|
*/
|
|
final boolean optimized;
|
|
/**
|
|
* the list of optimized providers, empty if not optimized.
|
|
*/
|
|
final List<ThreadLocalProvider> providers;
|
|
/**
|
|
* used to perform optimized factory creations.
|
|
*/
|
|
volatile Binding<?> cachedBinding; // TODO: volatile necessary?
|
|
|
|
AssistData(Constructor<?> constructor, Key<?> returnType, ImmutableList<Key<?>> paramTypes,
|
|
TypeLiteral<?> implementationType, Method factoryMethod,
|
|
Set<Dependency<?>> dependencies,
|
|
boolean optimized, List<ThreadLocalProvider> providers) {
|
|
this.constructor = constructor;
|
|
this.returnType = returnType;
|
|
this.paramTypes = paramTypes;
|
|
this.implementationType = implementationType;
|
|
this.factoryMethod = factoryMethod;
|
|
this.dependencies = dependencies;
|
|
this.optimized = optimized;
|
|
this.providers = providers;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return MoreObjects.toStringHelper(getClass())
|
|
.add("ctor", constructor)
|
|
.add("return type", returnType)
|
|
.add("param type", paramTypes)
|
|
.add("implementation type", implementationType)
|
|
.add("dependencies", dependencies)
|
|
.add("factory method", factoryMethod)
|
|
.add("optimized", optimized)
|
|
.add("providers", providers)
|
|
.add("cached binding", cachedBinding)
|
|
.toString();
|
|
}
|
|
|
|
public Set<Dependency<?>> getDependencies() {
|
|
return dependencies;
|
|
}
|
|
|
|
public Method getFactoryMethod() {
|
|
return factoryMethod;
|
|
}
|
|
|
|
public Constructor<?> getImplementationConstructor() {
|
|
return constructor;
|
|
}
|
|
|
|
public TypeLiteral<?> getImplementationType() {
|
|
return implementationType;
|
|
}
|
|
}
|
|
|
|
// not <T> because we'll never know and this is easier than suppressing warnings.
|
|
private static class ThreadLocalProvider extends ThreadLocal<Object> implements Provider<Object> {
|
|
@Override
|
|
protected Object initialValue() {
|
|
throw new IllegalStateException(
|
|
"Cannot use optimized @Assisted provider outside the scope of the constructor."
|
|
+ " (This should never happen. If it does, please report it.)");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrapper around MethodHandles/MethodHandle, so we can compile+run on java6.
|
|
*/
|
|
private static class MethodHandleWrapper {
|
|
static final int ALL_MODES = Modifier.PRIVATE
|
|
| Modifier.STATIC /* package */
|
|
| Modifier.PUBLIC
|
|
| Modifier.PROTECTED;
|
|
|
|
static final Method unreflectSpecial;
|
|
static final Method bindTo;
|
|
static final Method invokeWithArguments;
|
|
static final Constructor<?> lookupCxtor;
|
|
static final boolean valid;
|
|
|
|
static {
|
|
Method unreflectSpecialTmp = null;
|
|
Method bindToTmp = null;
|
|
Method invokeWithArgumentsTmp = null;
|
|
boolean validTmp = false;
|
|
Constructor<?> lookupCxtorTmp = null;
|
|
try {
|
|
Class<?> lookupClass = Class.forName("java.lang.invoke.MethodHandles$Lookup");
|
|
unreflectSpecialTmp = lookupClass.getMethod("unreflectSpecial", Method.class, Class.class);
|
|
Class<?> methodHandleClass = Class.forName("java.lang.invoke.MethodHandle");
|
|
bindToTmp = methodHandleClass.getMethod("bindTo", Object.class);
|
|
invokeWithArgumentsTmp = methodHandleClass.getMethod("invokeWithArguments", Object[].class);
|
|
lookupCxtorTmp = lookupClass.getDeclaredConstructor(Class.class, int.class);
|
|
lookupCxtorTmp.setAccessible(true);
|
|
validTmp = true;
|
|
} catch (Exception invalid) {
|
|
// Ignore the exception, store the values & exit early in create(..) if invalid.
|
|
}
|
|
|
|
// Store refs to later.
|
|
valid = validTmp;
|
|
unreflectSpecial = unreflectSpecialTmp;
|
|
bindTo = bindToTmp;
|
|
invokeWithArguments = invokeWithArgumentsTmp;
|
|
lookupCxtor = lookupCxtorTmp;
|
|
}
|
|
|
|
final Object handle;
|
|
|
|
MethodHandleWrapper(Object handle) {
|
|
this.handle = handle;
|
|
}
|
|
|
|
static MethodHandleWrapper create(Method method, Object proxy) {
|
|
if (!valid) {
|
|
return null;
|
|
}
|
|
try {
|
|
Class<?> declaringClass = method.getDeclaringClass();
|
|
// Note: this isn't a public API, but we need to use it in order to call default methods.
|
|
Object lookup = lookupCxtor.newInstance(declaringClass, ALL_MODES);
|
|
method.setAccessible(true);
|
|
// These are part of the public API, but we use reflection since we run on java6
|
|
// and they were introduced in java7.
|
|
lookup = unreflectSpecial.invoke(lookup, method, declaringClass);
|
|
Object handle = bindTo.invoke(lookup, proxy);
|
|
return new MethodHandleWrapper(handle);
|
|
} catch (InvocationTargetException ite) {
|
|
return null;
|
|
} catch (IllegalAccessException iae) {
|
|
return null;
|
|
} catch (InstantiationException ie) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Object invokeWithArguments(Object[] args) throws Exception {
|
|
// We must cast the args to an object so the Object[] is the first param,
|
|
// as opposed to each individual varargs param.
|
|
return invokeWithArguments.invoke(handle, (Object) args);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return handle.toString();
|
|
}
|
|
}
|
|
}
|