From bd30107666e2614ddcd0d81df81dff0248a774b1 Mon Sep 17 00:00:00 2001 From: Taylor Wicksell Date: Thu, 22 Feb 2018 09:39:29 -0600 Subject: [PATCH] Ensure bean post processors are applied before injector is created In this way, Guice modules can declare bindings asEagerSingleton() --- .../guice/annotation/GuiceFactoryBean.java | 15 +- .../ModuleRegistryConfiguration.java | 162 +++++++++++------- .../guice/module/SpringModule.java | 10 +- .../guice/BeanPostProcessorTest.java | 112 ++++++++++++ .../annotation/EnableGuiceModulesTests.java | 4 +- 5 files changed, 237 insertions(+), 66 deletions(-) create mode 100644 src/test/java/org/springframework/guice/BeanPostProcessorTest.java diff --git a/src/main/java/org/springframework/guice/annotation/GuiceFactoryBean.java b/src/main/java/org/springframework/guice/annotation/GuiceFactoryBean.java index 9943192..c531dff 100644 --- a/src/main/java/org/springframework/guice/annotation/GuiceFactoryBean.java +++ b/src/main/java/org/springframework/guice/annotation/GuiceFactoryBean.java @@ -15,6 +15,10 @@ package org.springframework.guice.annotation; import javax.inject.Provider; import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Autowired; + +import com.google.inject.Injector; +import com.google.inject.Key; /** * Convenience class used to map a Guice {@link Provider} to a Spring bean. @@ -22,17 +26,20 @@ import org.springframework.beans.factory.FactoryBean; * @author Dave Syer */ class GuiceFactoryBean implements FactoryBean { - private final Provider provider; + private final Key key; private final Class beanType; + + @Autowired + private Injector injector; - public GuiceFactoryBean(Class beanType, Provider provider) { - this.provider = provider; + public GuiceFactoryBean(Class beanType, Key key) { this.beanType = beanType; + this.key = key; } @Override public T getObject() throws Exception { - return (T) provider.get(); + return (T) injector.getInstance(key); } @Override diff --git a/src/main/java/org/springframework/guice/annotation/ModuleRegistryConfiguration.java b/src/main/java/org/springframework/guice/annotation/ModuleRegistryConfiguration.java index b65a48f..118db66 100644 --- a/src/main/java/org/springframework/guice/annotation/ModuleRegistryConfiguration.java +++ b/src/main/java/org/springframework/guice/annotation/ModuleRegistryConfiguration.java @@ -14,50 +14,87 @@ package org.springframework.guice.annotation; import java.util.ArrayList; -import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import com.google.inject.Binding; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Stage; +import com.google.inject.name.Named; +import com.google.inject.spi.Element; +import com.google.inject.spi.ElementSource; +import com.google.inject.spi.Elements; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextException; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.guice.module.SpringModule; -import com.google.inject.Binding; -import com.google.inject.Guice; -import com.google.inject.Injector; -import com.google.inject.Key; -import com.google.inject.Module; -import com.google.inject.name.Named; -import com.google.inject.spi.ElementSource; - +/** + * Configuration postprocessor that registers all the bindings in Guice modules as Spring + * beans. + * + * @author Dave Syer + * @author Talylor Wicksell + * + */ @Configuration -@Order(Ordered.HIGHEST_PRECEDENCE + 10) -class ModuleRegistryConfiguration implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware { +@Order(Ordered.HIGHEST_PRECEDENCE) +class ModuleRegistryConfiguration implements BeanDefinitionRegistryPostProcessor, + ApplicationContextAware, ApplicationListener { ApplicationContext applicationContext; + private List modules; + private ConfigurableListableBeanFactory beanFactory; - private Injector createInjector(Collection modules) { - return Guice.createInjector(modules); + private void createInjector(List modules, + ConfigurableListableBeanFactory beanFactory) { + Injector injector = null; + try { + Map beansOfType = beanFactory + .getBeansOfType(InjectorFactory.class); + if (beansOfType.size() > 1) { + throw new ApplicationContextException("Found multiple beans of type " + + InjectorFactory.class.getName() + + " Please ensure that only one InjectorFactory bean is defined. InjectorFactory beans found: " + + beansOfType.keySet()); + } + else if (beansOfType.size() == 1) { + InjectorFactory injectorFactory = beansOfType.values().iterator().next(); + injector = injectorFactory.createInjector(modules); + } + } + catch (NoSuchBeanDefinitionException e) { + + } + if (injector == null) { + injector = Guice.createInjector(modules); + } + beanFactory.registerResolvableDependency(Injector.class, injector); } - private void mapBindings(Injector injector, BeanDefinitionRegistry registry) - { - for (Entry, Binding> entry : injector.getBindings().entrySet()) { - if (entry.getKey().getTypeLiteral().getRawType().equals(Injector.class) || - "spring-guice".equals(entry.getValue().getSource().toString())) { + private void mapBindings(Map, Binding> bindings, + BeanDefinitionRegistry registry) { + for (Entry, Binding> entry : bindings.entrySet()) { + if (entry.getKey().getTypeLiteral().getRawType().equals(Injector.class) + || "spring-guice".equals(entry.getValue().getSource().toString())) { continue; } @@ -68,22 +105,19 @@ class ModuleRegistryConfiguration implements BeanDefinitionRegistryPostProcessor RootBeanDefinition bean = new RootBeanDefinition(GuiceFactoryBean.class); ConstructorArgumentValues args = new ConstructorArgumentValues(); args.addIndexedArgumentValue(0, key.getTypeLiteral().getRawType()); - args.addIndexedArgumentValue(1, binding.getProvider()); + args.addIndexedArgumentValue(1, key); bean.setConstructorArgumentValues(args); if (source != null && source instanceof ElementSource) { - bean.setResourceDescription(((ElementSource) source).getDeclaringSource().toString()); - } else { + bean.setResourceDescription( + ((ElementSource) source).getDeclaringSource().toString()); + } + else { bean.setResourceDescription("spring-guice"); } + bean.setAttribute("spring-guice", true); registry.registerBeanDefinition(extractName(key), bean); } - if(injector.getParent() != null) - { - mapBindings(injector.getParent(), registry); - } - - ((ConfigurableListableBeanFactory) registry).registerResolvableDependency(Injector.class, injector); } private String extractName(Key key) { @@ -94,40 +128,52 @@ class ModuleRegistryConfiguration implements BeanDefinitionRegistryPostProcessor } @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) + throws BeansException { + modules = new ArrayList(((ConfigurableListableBeanFactory) registry) + .getBeansOfType(Module.class).values()); + Map, Binding> bindings = new HashMap, Binding>(); + for (Element e : Elements.getElements(Stage.TOOL, modules)) { + if (e instanceof Binding) { + Binding binding = (Binding) e; + bindings.put(binding.getKey(), binding); + } + } + mapBindings(bindings, registry); + modules.add(new SpringModule(this.applicationContext)); + // This event can be published now and it wont actually be processed until later + // (during onRefresh()). There's no other way to get a hook into this phase of the + // lifecycle. + applicationContext.publishEvent(new CreateInjectorSignalEvent()); } - @Override - public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { - List modules = new ArrayList( - ((DefaultListableBeanFactory) registry).getBeansOfType(Module.class).values()); - modules.add(new SpringModule(this.applicationContext)); - Injector injector = null; - try { - Map beansOfType = ((DefaultListableBeanFactory) registry).getBeansOfType(InjectorFactory.class); - if (beansOfType.size() > 1) { - throw new ApplicationContextException("Found multiple beans of type " + InjectorFactory.class.getName() - + " Please ensure that only one InjectorFactory bean is defined. InjectorFactory beans found: " - + beansOfType.keySet()); - } - else if(beansOfType.size() == 1) { - InjectorFactory injectorFactory = beansOfType.values().iterator().next(); - injector = injectorFactory.createInjector(modules); - } - } catch (NoSuchBeanDefinitionException e) { - - } - if (injector == null) { - injector = createInjector(modules); - } - mapBindings(injector, registry); - } - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext=applicationContext; + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + this.beanFactory = beanFactory; } + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void onApplicationEvent(CreateInjectorSignalEvent event) { + createInjector(modules, beanFactory); + } +} + +/** + * Signaling event used to trigger injector creation after BeanPostProcessors have been + * applied. + */ +class CreateInjectorSignalEvent extends ApplicationEvent { + private static final long serialVersionUID = -6546970378679850504L; + + public CreateInjectorSignalEvent() { + super(serialVersionUID); + } } diff --git a/src/main/java/org/springframework/guice/module/SpringModule.java b/src/main/java/org/springframework/guice/module/SpringModule.java index 7ff3814..ee87c9d 100644 --- a/src/main/java/org/springframework/guice/module/SpringModule.java +++ b/src/main/java/org/springframework/guice/module/SpringModule.java @@ -36,6 +36,7 @@ import com.google.inject.Binder; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.ProvisionException; +import com.google.inject.Scopes; import com.google.inject.Stage; import com.google.inject.TypeLiteral; import com.google.inject.matcher.Matchers; @@ -95,6 +96,9 @@ public class SpringModule extends AbstractModule { private void bind(ConfigurableListableBeanFactory beanFactory) { for (String name : beanFactory.getBeanDefinitionNames()) { BeanDefinition definition = beanFactory.getBeanDefinition(name); + if(definition.hasAttribute("spring-guice")){ + continue; + } if (definition.isAutowireCandidate() && definition.getRole() == AbstractBeanDefinition.ROLE_APPLICATION) { Class type = beanFactory.getType(name); @@ -140,8 +144,10 @@ public class SpringModule extends AbstractModule { if (this.bound.get(type) == null) { // Only bind one provider for each type binder.withSource("spring-guice").bind(Key.get(type)) - .toProvider(typeProvider); - this.bound.put(type, typeProvider); + .toProvider(typeProvider).in(Scopes.SINGLETON); + if(binder.currentStage() != Stage.TOOL) { + this.bound.put(type, typeProvider); + } } // But allow binding to named beans binder.withSource("spring-guice").bind(TypeLiteral.get(type)) diff --git a/src/test/java/org/springframework/guice/BeanPostProcessorTest.java b/src/test/java/org/springframework/guice/BeanPostProcessorTest.java new file mode 100644 index 0000000..0e5e9c0 --- /dev/null +++ b/src/test/java/org/springframework/guice/BeanPostProcessorTest.java @@ -0,0 +1,112 @@ +package org.springframework.guice; + +import static org.junit.Assert.assertTrue; + +import javax.inject.Inject; + +import org.junit.Test; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.guice.BeanPostProcessorTest.GuiceBeanThatWantsPostProcessedBean; +import org.springframework.guice.BeanPostProcessorTest.GuiceBeanThatWantsSpringBean; +import org.springframework.guice.BeanPostProcessorTest.PostProcessedBean; +import org.springframework.guice.BeanPostProcessorTest.SpringBeanThatWantsPostProcessedBean; +import org.springframework.guice.annotation.EnableGuiceModules; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; + +public class BeanPostProcessorTest { + + /** + * Verify BeanPostProcessor's such as Spring Boot's + * ConfigurationPropertiesBindingPostProcessor are applied. + */ + @Test + public void testBeanPostProcessorsApplied() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BeanPostProcessorTestConfig.class); + PostProcessedBean postProcessedBean = context.getBean(PostProcessedBean.class); + assertTrue(postProcessedBean.postProcessed); + GuiceBeanThatWantsPostProcessedBean guiceBean1 = context.getBean(GuiceBeanThatWantsPostProcessedBean.class); + assertTrue(guiceBean1.ppb.postProcessed); + GuiceBeanThatWantsSpringBean guiceBean2 = context.getBean(GuiceBeanThatWantsSpringBean.class); + assertTrue(guiceBean2.springBean.ppb.postProcessed); + context.close(); + } + + public static class PostProcessedBean { + Boolean postProcessed = false; + } + + public static class SpringBeanThatWantsPostProcessedBean { + PostProcessedBean ppb; + public SpringBeanThatWantsPostProcessedBean(PostProcessedBean ppb) { + this.ppb = ppb; + } + } + + public static class GuiceBeanThatWantsPostProcessedBean { + PostProcessedBean ppb; + @Inject + public GuiceBeanThatWantsPostProcessedBean(PostProcessedBean ppb) { + this.ppb = ppb; + } + } + + public static class GuiceBeanThatWantsSpringBean { + SpringBeanThatWantsPostProcessedBean springBean; + @Inject + public GuiceBeanThatWantsSpringBean(SpringBeanThatWantsPostProcessedBean springBean) { + this.springBean = springBean; + } + } +} + + +@EnableGuiceModules +@Configuration +class BeanPostProcessorTestConfig { + @Bean + public BeanPostProcessor postProcessor() { + return new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if(bean instanceof PostProcessedBean) { + ((PostProcessedBean)bean).postProcessed = true; + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + }; + } + + @Bean + public PostProcessedBean postProcessedBean() { + return new PostProcessedBean(); + } + + @Bean + public SpringBeanThatWantsPostProcessedBean springBean(PostProcessedBean ppb) { + return new SpringBeanThatWantsPostProcessedBean(ppb); + } + + @Bean + public Module someGuiceModule() { + return new AbstractModule() { + + @Override + protected void configure() { + binder().requireExplicitBindings(); + bind(GuiceBeanThatWantsPostProcessedBean.class).asEagerSingleton(); + bind(GuiceBeanThatWantsSpringBean.class).asEagerSingleton(); + } + }; + } +} diff --git a/src/test/java/org/springframework/guice/annotation/EnableGuiceModulesTests.java b/src/test/java/org/springframework/guice/annotation/EnableGuiceModulesTests.java index 0e28264..2aac10c 100644 --- a/src/test/java/org/springframework/guice/annotation/EnableGuiceModulesTests.java +++ b/src/test/java/org/springframework/guice/annotation/EnableGuiceModulesTests.java @@ -14,6 +14,7 @@ package org.springframework.guice.annotation; import javax.inject.Inject; +import javax.inject.Named; import com.google.inject.AbstractModule; import com.google.inject.Injector; @@ -68,10 +69,9 @@ public class EnableGuiceModulesTests { public static class Foo { @Inject - public Foo(Service service) { + public Foo(@Named("service") Service service) { service.toString(); } - } @Configuration