Skip to content

Instantly share code, notes, and snippets.

@gissuebot
Created July 7, 2014 17:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gissuebot/2fc80c4e9fd07c29f9c7 to your computer and use it in GitHub Desktop.
Save gissuebot/2fc80c4e9fd07c29f9c7 to your computer and use it in GitHub Desktop.
Migrated attachment for Guice issue 131, comment 4
Index: extensions/assistedinject/src/com/google/inject/assistedinject/FactoryModule.java
===================================================================
--- extensions/assistedinject/src/com/google/inject/assistedinject/FactoryModule.java (revision 0)
+++ extensions/assistedinject/src/com/google/inject/assistedinject/FactoryModule.java (revision 0)
@@ -0,0 +1,335 @@
+package com.google.inject.assistedinject;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.AbstractModule;
+import com.google.inject.Binder;
+import com.google.inject.BindingAnnotation;
+import com.google.inject.CreationException;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Stage;
+import com.google.inject.internal.Errors;
+import com.google.inject.spi.Element;
+import com.google.inject.spi.Elements;
+import com.google.inject.spi.ModuleWriter;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+
+/**
+ * Provides a mechanism to combine user-specified parameters with
+ * {@link Injector}-specified parameters when creating new objects.
+ *
+ * <p>To use a {@link FactoryModule}:
+ *
+ * <p>Annotate your implementation class' constructor with
+ * {@literal @}{@link Inject} and binding annotations on parameters as necessary:
+ * <pre><code>public class RealPayment implements Payment {
+ * {@literal @}Inject
+ * public RealPayment(CreditService creditService, AuthService authService,
+ * {@literal @}Named("startDate") Date startDate,
+ * {@literal @}Named("amount") Money amount) {
+ * ...
+ * }
+ * }</code></pre>
+ *
+ * <p>Write an interface with a method that accepts the user-specified
+ * parameters with the same binding annotations as in the implementation
+ * class' constructor:
+ * <pre><code>public interface PaymentFactory {
+ * Payment create(Date startDate, Money amount);
+ * }</code></pre>
+ *
+ * <p>You can name your create methods whatever you like, such as <i>create</i>,
+ * or <i>createPayment</i> or <i>newPayment</i>. The concrete class must
+ * be assignable to the return type of your create method. You can also provide
+ * multiple factory methods within the same factory interface.
+ *
+ * <p>In your Guice {@link com.google.inject.Module module}, bind your factory
+ * interface to an instance of {@link FactoryModule} and tell the
+ * {@link FactoryModule} to use your implementation class:
+ * <pre><code>install(FactoryModule.forInterface(PaymentFactory.class) {
+ * @Override protected void configureFactoryBindings() {
+ * bind(Payment.class).to(RealPayment.class);
+ * });</code></pre>
+ *
+ * <p>If there are other classes your factory should know about, you can
+ * chain together several {@code binding()} calls.
+ *
+ * <p>Now you can {@literal @}{@code Inject} your factory interface into your
+ * Guice-injected classes. When you invoke the create method on that factory,
+ * the {@link FactoryModule} will instantiate the implementation
+ * class using parameters from the injector and the factory method.
+ *
+ * <pre><code>public class PaymentAction {
+ * {@literal @}Inject private PaymentFactory paymentFactory;
+ *
+ * public void doPayment(Money amount) {
+ * Payment payment = paymentFactory.create(new Date(), amount);
+ * payment.apply();
+ * }
+ * }</code></pre>
+ *
+ * @param <F> The factory interface
+ *
+ * @author dtm@google.com (Daniel Martin)
+ */
+public class FactoryModule extends AbstractModule {
+ private final Class<?> factoryClass;
+ private boolean inChildMode = false;
+
+ public FactoryModule(Class<?> factoryClass) {
+ this.factoryClass = factoryClass;
+ }
+
+ private static class FactoryProvider<F> implements Provider<F> {
+ private final Class<F> factoryClass;
+ private final FactoryModule srcModule;
+ private F factory;
+
+ FactoryProvider(Class<F> factoryClass, FactoryModule srcModule) {
+ this.factoryClass = factoryClass;
+ this.srcModule = srcModule;
+ }
+
+ @SuppressWarnings("unused")
+ @Inject private void initFactoryProvider(Injector injector) {
+ List<Element> childConfig = Elements.getElements(new AbstractModule() {
+ @Override protected void configure() {
+ srcModule.doChildConfiguration(binder());
+ }
+ });
+
+ final Map<Method, FactoryMethodImplementation> methodImplementations =
+ new HashMap<Method, FactoryMethodImplementation>();
+ for (Method method : getFactoryInterfaceMethods()) {
+ FactoryMethodImplementation methodImplementation =
+ new FactoryMethodImplementation(method, childConfig, injector);
+ methodImplementations.put(method, methodImplementation);
+ }
+
+ InvocationHandler proxyImplementation = new InvocationHandler() {
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+ FactoryMethodImplementation methodImplementation =
+ methodImplementations.get(method);
+ if (methodImplementation == null) {
+ if (method.getDeclaringClass().equals(Object.class)) {
+ if (method.getName().equals("equals")) {
+ return proxy == args[0];
+ }
+ return method.invoke(this, args);
+ }
+ throw new IllegalStateException(
+ "Unexpected method called: " + method);
+ }
+ return methodImplementation.invoke(args);
+ }
+
+ @Override
+ public String toString() {
+ return "Proxy for " + factoryClass;
+ }
+ };
+
+ factory = factoryClass.cast(Proxy.newProxyInstance(
+ factoryClass.getClassLoader(),
+ new Class[] {factoryClass}, proxyImplementation));
+ }
+
+ public F get() {
+ return factory;
+ }
+
+ /**
+ * Return all the methods we need our proxy to implement, including those
+ * defined on super interfaces. Do not return those defined on
+ * {@link Object}.
+ */
+ private List<Method> getFactoryInterfaceMethods() {
+ List<Method> retval = new ArrayList<Method>();
+ for (Method method : factoryClass.getMethods()) {
+ if (method.getDeclaringClass().isInterface()) {
+ retval.add(method);
+ }
+ }
+ return retval;
+ }
+
+ static <F> void bind(Binder binder, Class<F> clazz, FactoryModule mod) {
+ binder.bind(clazz).toProvider(new FactoryProvider<F>(clazz, mod));
+ }
+ }
+
+
+ @Override
+ protected final void configure() {
+ if (inChildMode) {
+ configureFactoryBindings();
+ } else {
+ FactoryProvider.bind(binder(), factoryClass, this);
+ }
+ }
+
+ private void doChildConfiguration(Binder binder) {
+ try {
+ inChildMode = true;
+ configure(binder);
+ } finally {
+ inChildMode = false;
+ }
+ }
+
+ /**
+ * Configure the bindings that should be present in the factory that are not
+ * supplied by user parameters to factory methods. Subclasses will want to
+ * override this in all but the simplest cases.
+ */
+ protected void configureFactoryBindings() {
+ }
+
+ /**
+ * Find the {@link Key}s that a particular method's parameters take.
+ *
+ * @param method The method to inspect.
+ * @return A {@link List} of {@code Key}s in the same order as the method's
+ * parameters.
+ */
+ private static List<Key<?>> getParameterKeys(Method method) {
+ List<Key<?>> retval = new ArrayList<Key<?>>();
+ Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+ Type[] parameterTypes = method.getGenericParameterTypes();
+ for(int i=0; i < parameterTypes.length; i++) {
+ retval.add(getKey(parameterTypes[i], parameterAnnotations[i],
+ "Parameter " + (i + 1) + " of method " + method));
+ }
+ return retval;
+ }
+
+ /**
+ * Find the {@link Key} that describes a particular method's return type.
+ *
+ * @param method The method to inspect.
+ * @return A {@link Key} that has the same type and binding annotations
+ * as {@code method}'s return type.
+ */
+ private static Key<?> getReturnKey(Method method) {
+ return getKey(method.getGenericReturnType(), method.getAnnotations(),
+ "Return type of method " + method);
+ }
+
+ /**
+ * Find a key for a given type and annotations.
+ *
+ * @param type The {@link Type} of the key to construct
+ * @param annotations An array of {@link Annotation}s to examine. It is an
+ * error if more than one of these is marked
+ * {@literal @}{@link BindingAnnotation}
+ * @param where Context to use for error messages
+ * @throws IllegalArgumentException If more than one of {@code annotations}
+ * is marked {@literal @}{@link BindingAnnotation}
+ */
+ private static Key<?> getKey(Type type, Annotation[] annotations,
+ String where) {
+ Annotation bindingAnnotation = null;
+ for (Annotation parameterAnnotation : annotations) {
+ if (parameterAnnotation.annotationType()
+ .getAnnotation(BindingAnnotation.class) != null) {
+ if (bindingAnnotation != null) {
+ throw new IllegalArgumentException(
+ where + ": multiple binding annotations found");
+ }
+ bindingAnnotation = parameterAnnotation;
+ }
+ }
+ if (bindingAnnotation != null) {
+ return Key.get(type, bindingAnnotation);
+ } else {
+ return Key.get(type);
+ }
+ }
+
+ private static class FactoryMethodImplementation {
+ static final Set<Key<?>> GUICE_TYPES =
+ ImmutableSet.of(Key.get(Injector.class), Key.get(Logger.class),
+ Key.get(Stage.class));
+ final List<Key<?>> parameterKeys;
+ final Injector parentInjector;
+ final Key<?> returnKey;
+ final List<Element> extraChildConfig;
+
+ FactoryMethodImplementation(
+ Method method, List<Element> extraChildConfig, Injector parentInjector) {
+ parameterKeys = getParameterKeys(method);
+ returnKey = FactoryModule.getReturnKey(method);
+ this.parentInjector = parentInjector;
+ this.extraChildConfig = extraChildConfig;
+ try {
+ testBindings();
+ } catch (CreationException ce) {
+ throw new RuntimeException(
+ Errors.format("Errors found testing bindings for " + method,
+ ce.getErrorMessages()));
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ Object invoke(final Object[] args) {
+ Injector childInjector =
+ parentInjector.createChildInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ for (int i = 0; i < args.length; i++) {
+ bind((Key)parameterKeys.get(i)).toInstance(args[i]);
+ }
+ new ModuleWriter().apply(binder(), extraChildConfig);
+ }
+ });
+ return childInjector.getInstance(returnKey);
+ }
+
+ private void testBindings() {
+ Guice.createInjector(Stage.TOOL, new AbstractModule() {
+ @Override
+ protected void configure() {
+ for (Key<?> key : parameterKeys) {
+ bindToThrowingProvider(key);
+ }
+ // TODO: iterate through parent's parent, etc.
+ for (Key<?> key : parentInjector.getBindings().keySet()) {
+ if (!GUICE_TYPES.contains(key)) {
+ bindToThrowingProvider(key);
+ }
+ }
+ new ModuleWriter().apply(binder(), extraChildConfig);
+ requireBinding(returnKey);
+ }
+
+ private <T> void bindToThrowingProvider(Key<T> key) {
+ bind(key).toProvider(new Provider<T>() {
+ public T get() {
+ throw new RuntimeException("This is a test injector");
+ }
+ });
+ }
+ });
+
+ }
+
+ Key<?> getReturnKey() {
+ return returnKey;
+ }
+ }
+}
Index: extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider.java
===================================================================
--- extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider.java (revision 647)
+++ extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider.java (working copy)
@@ -64,8 +64,8 @@
* FactoryProvider.newFactory(PaymentFactory.class, RealPayment.class));</code></pre>
*
* <p>Now you can {@literal @}{@code Inject} your factory interface into your
- * Guice-injected classes. When you invoke the create method on that factory, the
- * {@link FactoryProvider} will instantiate the implementation class using
+ * Guice-injected classes. When you invoke the create method on that factory,
+ * the {@link FactoryProvider} will instantiate the implementation class using
* parameters from the injector and the factory method.
*
* <pre><code>public class PaymentAction {
@@ -78,25 +78,24 @@
* }</code></pre>
*
* @param <F> The factory interface
- * @param <R> The concrete class to be created.
*
* @author jmourits@google.com (Jerome Mourits)
* @author jessewilson@google.com (Jesse Wilson)
*/
-public class FactoryProvider<F, R> implements Provider<F> {
+public class FactoryProvider<F> implements Provider<F> {
private Injector injector;
private final Class<F> factoryType;
- private final Class<R> implementationType;
+ private final Class<?> implementationType;
private final Map<Method, AssistedConstructor<?>> factoryMethodToConstructor;
- public static <X,Y> FactoryProvider<X,Y> newFactory(
- Class<X> factoryType, Class<Y> implementationType){
- return new FactoryProvider<X, Y>(factoryType,implementationType);
+ public static <X> FactoryProvider<X> newFactory(
+ Class<X> factoryType, Class<?> implementationType){
+ return new FactoryProvider<X>(factoryType,implementationType);
}
- private FactoryProvider(Class<F> factoryType, Class<R> implementationType) {
+ private FactoryProvider(Class<F> factoryType, Class<?> implementationType) {
this.factoryType = factoryType;
this.implementationType = implementationType;
this.factoryMethodToConstructor = createMethodMapping();
@@ -104,7 +103,7 @@
}
@Inject
- @SuppressWarnings({"unchecked", "unused"})
+ @SuppressWarnings({"unused"})
private void setInjectorAndCheckUnboundParametersAreInjectable(
Injector injector) {
this.injector = injector;
@@ -145,7 +144,6 @@
return false;
}
- @SuppressWarnings("unchecked")
private boolean paramCanBeInjected(Parameter parameter, Injector injector) {
return parameter.isBound(injector);
}
@@ -204,7 +202,6 @@
return result;
}
- @SuppressWarnings({"unchecked"})
public F get() {
InvocationHandler invocationHandler = new InvocationHandler() {
@@ -242,7 +239,7 @@
}
};
- return (F) Proxy.newProxyInstance(factoryType.getClassLoader(),
- new Class[] {factoryType}, invocationHandler);
+ return factoryType.cast(Proxy.newProxyInstance(factoryType.getClassLoader(),
+ new Class[] {factoryType}, invocationHandler));
}
}
Index: extensions/assistedinject/test/com/google/inject/assistedinject/FactoryModuleTest.java
===================================================================
--- extensions/assistedinject/test/com/google/inject/assistedinject/FactoryModuleTest.java (revision 0)
+++ extensions/assistedinject/test/com/google/inject/assistedinject/FactoryModuleTest.java (revision 0)
@@ -0,0 +1,252 @@
+package com.google.inject.assistedinject;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.CreationException;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.matcher.Matchers;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import junit.framework.TestCase;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class FactoryModuleTest extends TestCase {
+ @Retention(RetentionPolicy.RUNTIME)
+ public static @interface Traced {}
+ public static interface StringService {
+ String getString();
+ }
+
+ public static interface StringServiceFactory {
+ StringService create(
+ @Named("user") String user, @Named("server") String server);
+ }
+
+ public static class RemoteStringService implements StringService {
+ private final String user;
+ private final String server;
+
+ @Inject public RemoteStringService(
+ @Named("user") String user, @Named("server") String server) {
+ this.user = user;
+ this.server = server;
+ }
+
+ public String getString() {
+ return "Service for " + user + "@" + server;
+ }
+ }
+
+ public void testBasic() {
+ StringServiceFactory factory = Guice.createInjector(
+ new FactoryModule(StringServiceFactory.class) {
+ @Override
+ protected void configureFactoryBindings() {
+ bind(StringService.class).to(RemoteStringService.class);
+ }
+ }).getInstance(StringServiceFactory.class);
+ assertEquals("Factory-constructed remote service string",
+ "Service for bob@veblen", factory.create("bob", "veblen").getString());
+ }
+
+ public static class LocalStringService implements StringService {
+ private final String user;
+
+ @Inject public LocalStringService(@Named("user") String user) {
+ this.user = user;
+ }
+
+ @Traced
+ public String getString() {
+ return "Local service for " + user;
+ }
+ }
+
+ public void testOverparametrized() {
+ StringServiceFactory factory = Guice.createInjector(
+ new FactoryModule(StringServiceFactory.class) {
+ @Override
+ protected void configureFactoryBindings() {
+ bind(StringService.class).to(LocalStringService.class);
+ }
+ }).getInstance(StringServiceFactory.class);
+ assertEquals("Factory-constructed local service string",
+ "Local service for bob", factory.create("bob", "veblen").getString());
+ }
+
+ static class DelegatingStringService implements StringService {
+ private final StringService backend;
+
+ @Inject public DelegatingStringService(
+ @Named("backend") StringService backend) {
+ this.backend = backend;
+ }
+
+ public String getString() {
+ return "Delegate to " + backend.getString();
+ }
+ }
+
+ public void testIntermediateBinding() {
+ StringServiceFactory factory = Guice.createInjector(
+ new FactoryModule(StringServiceFactory.class) {
+ @Override
+ protected void configureFactoryBindings() {
+ bind(StringService.class).to(DelegatingStringService.class);
+ bind(StringService.class).annotatedWith(Names.named("backend"))
+ .to(RemoteStringService.class);
+ }
+ }).getInstance(StringServiceFactory.class);
+
+ assertEquals("Delegated service response",
+ "Delegate to Service for bob@veblen",
+ factory.create("bob", "veblen").getString());
+ }
+
+ interface DualStringServiceFactory extends StringServiceFactory {
+ @Named("secondary") StringService createSecondary(
+ @Named("user") String user, @Named("server") String server);
+ }
+
+ public void testInterfaceInheritance() {
+ DualStringServiceFactory factory = Guice.createInjector(
+ new FactoryModule(DualStringServiceFactory.class) {
+ @Override
+ protected void configureFactoryBindings() {
+ bind(StringService.class).to(RemoteStringService.class);
+ bind(StringService.class).annotatedWith(Names.named("secondary"))
+ .to(LocalStringService.class);
+ }
+ }).getInstance(DualStringServiceFactory.class);
+ assertEquals("Factory-constructed remote service string",
+ "Service for bob@veblen",
+ factory.create("bob", "veblen").getString());
+ assertEquals("Factory-constructed local service string",
+ "Local service for bob",
+ factory.createSecondary("bob", "veblen").getString());
+ }
+
+ interface LocalStringServiceFactory {
+ LocalStringService create(@Named("user") String user);
+ }
+
+ public void testNonDelegating() {
+ LocalStringServiceFactory factory = Guice.createInjector(
+ new FactoryModule(LocalStringServiceFactory.class)
+ ).getInstance(LocalStringServiceFactory.class);
+ assertEquals("Factory-constructed local service string",
+ "Local service for bob", factory.create("bob").getString());
+ }
+
+ static class DelayedLocalStringService implements StringService {
+ private final Provider<String> userProvider;
+
+ @Inject public DelayedLocalStringService(
+ @Named("user") Provider<String> userProvider) {
+ this.userProvider = userProvider;
+ }
+
+ public String getString() {
+ return "Local service for " + userProvider.get();
+ }
+ }
+
+ public void testDelayed() throws Throwable {
+ StringServiceFactory factory = Guice.createInjector(
+ new FactoryModule(StringServiceFactory.class) {
+ @Override
+ protected void configureFactoryBindings() {
+ bind(StringService.class).to(DelayedLocalStringService.class);
+ }
+ }).getInstance(StringServiceFactory.class);
+ final StringService bobService = factory.create("bob", "");
+ assertEquals("Factory-constructed local service string",
+ "Local service for bob", bobService.getString());
+ final StringService joeService = factory.create("joe", "");
+ assertEquals("Factory-constructed local service string",
+ "Local service for joe", joeService.getString());
+ final AtomicReference<Throwable> thrown = new AtomicReference<Throwable>();
+ Thread bobThread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ assertEquals("Factory-constructed local service string",
+ "Local service for bob", bobService.getString());
+ } catch (Throwable t) {
+ thrown.set(t);
+ }
+ }
+ };
+ bobThread.run();
+ bobThread.join();
+ if (thrown.get() != null) {
+ throw thrown.get();
+ }
+ Thread joeThread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ assertEquals("Factory-constructed local service string",
+ "Local service for joe", joeService.getString());
+ } catch (Throwable t) {
+ thrown.set(t);
+ }
+ }
+ };
+ joeThread.run();
+ joeThread.join();
+ if (thrown.get() != null) {
+ throw thrown.get();
+ }
+ }
+
+ public void testUnderSpecifiedBindings() {
+ try {
+ Guice.createInjector(new FactoryModule(StringServiceFactory.class));
+ fail("We shouldn't be able to create this injector");
+ } catch (CreationException ce) {
+ assertTrue("The exception message should have contained " +
+ "'StringServiceFactory.create'; it was: " + ce.getMessage(),
+ ce.getMessage().contains("StringServiceFactory.create"));
+ }
+ }
+
+ public void disabled_testPermgenSpace() {
+ LocalStringServiceFactory factory = Guice.createInjector(
+ new FactoryModule(LocalStringServiceFactory.class),
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ bindInterceptor(Matchers.subclassesOf(StringService.class),
+ Matchers.annotatedWith(Traced.class),
+ new MethodInterceptor() {
+ public Object invoke(MethodInvocation arg0) throws Throwable {
+ Class<?> serviceClass = arg0.getThis().getClass();
+ String noPackageName = serviceClass.getName().replaceFirst(".*\\.", "");
+ return "intercepted in class " + noPackageName + ": " + arg0.proceed();
+ }
+ });
+ }
+ }
+ ).getInstance(LocalStringServiceFactory.class);
+ for (int i=0; i < 5000; i++) {
+ LocalStringService service = factory.create("User " + i);
+ System.out.println(service.getString());
+ }
+ }
+
+ public static void main(String args[]) {
+ long now = System.currentTimeMillis();
+ System.out.println(now);
+ new FactoryModuleTest().disabled_testPermgenSpace();
+ System.out.println(System.currentTimeMillis() - now);
+ }
+}
Property changes on: extensions\assistedinject\test\com\google\inject\assistedinject\FactoryModuleTest.java
___________________________________________________________________
Added: svn:mergeinfo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment