diff --git a/src/main/java/org/springframework/guice/annotation/ModuleRegistryConfiguration.java b/src/main/java/org/springframework/guice/annotation/ModuleRegistryConfiguration.java index 5038919..5fd4390 100644 --- a/src/main/java/org/springframework/guice/annotation/ModuleRegistryConfiguration.java +++ b/src/main/java/org/springframework/guice/annotation/ModuleRegistryConfiguration.java @@ -41,6 +41,9 @@ 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.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; @@ -70,8 +73,7 @@ import java.util.stream.Collectors; */ @Configuration @Order(Ordered.HIGHEST_PRECEDENCE) -class ModuleRegistryConfiguration - implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware { +class ModuleRegistryConfiguration implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware { private static final String SPRING_GUICE_DEDUPE_BINDINGS_PROPERTY_NAME = "spring.guice.dedup"; private static final String SPRING_GUICE_AUTOWIRE_JIT_PROPERTY_NAME = "spring.guice.autowireJIT"; @@ -80,39 +82,59 @@ class ModuleRegistryConfiguration private final Log logger = LogFactory.getLog(getClass()); private ApplicationContext applicationContext; - private List modules; - private AtomicBoolean injectorCreated = new AtomicBoolean(false); private boolean enableJustInTimeBinding = true; - 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); - beanFactory.registerSingleton("injector", injector); + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + this.enableJustInTimeBinding = applicationContext.getEnvironment() + .getProperty(SPRING_GUICE_AUTOWIRE_JIT_PROPERTY_NAME, Boolean.class, true); } - private void mapBindings(Map, Binding> bindings, - BeanDefinitionRegistry registry) { + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + List modules = new ArrayList<>(((ConfigurableListableBeanFactory) registry) + .getBeansOfType(Module.class).values()); + modules.add(new SpringModule((ConfigurableListableBeanFactory) registry, enableJustInTimeBinding)); + Map, Binding> bindings = new HashMap, Binding>(); + List elements = Elements.getElements(Stage.TOOL, modules); + if (applicationContext.getEnvironment().getProperty( + SPRING_GUICE_DEDUPE_BINDINGS_PROPERTY_NAME, Boolean.class, false)) { + elements = removeDuplicates(elements); + modules = Collections.singletonList(Elements.getModule(elements)); + } + if (applicationContext.getEnvironment().containsProperty("spring.guice.modules.exclude")) { + String[] modulesToFilter = applicationContext.getEnvironment() + .getProperty("spring.guice.modules.exclude", "").split(","); + elements = elements.stream().filter(e -> elementFilter(modulesToFilter, e)).collect(Collectors.toList()); + modules = Collections.singletonList(Elements.getModule(elements)); + } + for (Element e : elements) { + if (e instanceof Binding) { + Binding binding = (Binding) e; + bindings.put(binding.getKey(), binding); + } + else if (e instanceof PrivateElements) { + extractPrivateElements(bindings, (PrivateElements) e); + } + } + mapBindings(bindings, registry); + + // Register the injector initializer + RootBeanDefinition beanDefinition = new RootBeanDefinition(GuiceInjectorInitializer.class); + ConstructorArgumentValues args = new ConstructorArgumentValues(); + args.addIndexedArgumentValue(0, modules); + args.addIndexedArgumentValue(1, applicationContext); + beanDefinition.setConstructorArgumentValues(args); + registry.registerBeanDefinition("guiceInjectorInitializer", beanDefinition); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) { + + } + + private void mapBindings(Map, Binding> bindings, BeanDefinitionRegistry registry) { Stage stage = applicationContext.getEnvironment().getProperty(SPRING_GUICE_STAGE_PROPERTY_NAME, Stage.class, Stage.PRODUCTION); boolean ifLazyInit = stage.equals(Stage.DEVELOPMENT); for (Entry, Binding> entry : bindings.entrySet()) { @@ -188,37 +210,6 @@ class ModuleRegistryConfiguration } } - @Override - public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) - throws BeansException { - modules = new ArrayList(((ConfigurableListableBeanFactory) registry) - .getBeansOfType(Module.class).values()); - modules.add(new SpringModule((ConfigurableListableBeanFactory) registry, enableJustInTimeBinding)); - Map, Binding> bindings = new HashMap, Binding>(); - List elements = Elements.getElements(Stage.TOOL, modules); - if (applicationContext.getEnvironment().getProperty( - SPRING_GUICE_DEDUPE_BINDINGS_PROPERTY_NAME, Boolean.class, false)) { - elements = removeDuplicates(elements); - modules = Collections.singletonList(Elements.getModule(elements)); - } - if (applicationContext.getEnvironment().containsProperty("spring.guice.modules.exclude")) { - String[] modulesToFilter = applicationContext.getEnvironment() - .getProperty("spring.guice.modules.exclude", "").split(","); - elements = elements.stream().filter(e -> elementFilter(modulesToFilter, e)).collect(Collectors.toList()); - modules = Collections.singletonList(Elements.getModule(elements)); - } - for (Element e : elements) { - if (e instanceof Binding) { - Binding binding = (Binding) e; - bindings.put(binding.getKey(), binding); - } - else if (e instanceof PrivateElements) { - extractPrivateElements(bindings, (PrivateElements) e); - } - } - mapBindings(bindings, registry); - } - private boolean elementFilter(String[] modulesToFilter, Element element){ try { return Arrays.stream(modulesToFilter) @@ -298,42 +289,81 @@ class ModuleRegistryConfiguration } } } +} - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) - throws BeansException { - beanFactory.registerSingleton("guiceInjectorInitializer", - new GuiceInjectorInitializingBeanPostProcessor() { - @Override - public Object postProcessBeforeInitialization(Object bean, - String beanName) throws BeansException { - return bean; - } +/** + * Creates the Guice injector and registers it. + * + * The correct time to create the injector is after all Bean Post Processors were registered (after the + * registerBeanPostProcessors() phase), but before other beans get resolved. To achieve this, we create the injector + * when the first bean gets resolved - in its post-processing phase. However, this creates a possibility for a circular + * initialization error (i.e. if the first bean is also being dependant on by a Guice provided binding). To resolve + * this we publish an event that will be triggered in the registerListeners() phase, and create the injector then. + * Combining both initialization mechanisms (post-processor and the event publishing) ensures the injector will be + * created no later then the registerListeners() phase, but after the registerBeanPostProcessors() phase. + * For application contexts that override onRefresh() and create beans then (i.e. WebServer based application contexts) + * the post-processor initialization will kick-in and create the injector before. + */ +class GuiceInjectorInitializer implements BeanPostProcessor, ApplicationListener { + private final AtomicBoolean injectorCreated = new AtomicBoolean(false); + private final List modules; + private final ConfigurableApplicationContext applicationContext; - @Override - public Object postProcessAfterInitialization(Object bean, - String beanName) throws BeansException { - if (injectorCreated.compareAndSet(false, true)) { - createInjector(modules, beanFactory); - } - return bean; - } - }); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) - throws BeansException { + public GuiceInjectorInitializer(List modules, + ConfigurableApplicationContext applicationContext) { + this.modules = modules; this.applicationContext = applicationContext; - this.enableJustInTimeBinding = applicationContext.getEnvironment() - .getProperty(SPRING_GUICE_AUTOWIRE_JIT_PROPERTY_NAME, Boolean.class, true); + + applicationContext.publishEvent(new CreateInjectorEvent()); } - private static class GuiceInjectorInitializingBeanPostProcessor - implements BeanPostProcessor, Ordered { - @Override - public int getOrder() { - return Ordered.LOWEST_PRECEDENCE - 1; + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (injectorCreated.compareAndSet(false, true)) { + createInjector(); + } + return bean; + } + + @Override + public void onApplicationEvent(CreateInjectorEvent event) { + if (injectorCreated.compareAndSet(false, true)) { + createInjector(); + } + } + + private void createInjector() { + Injector injector = null; + try { + Map beansOfType = applicationContext.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); + } + applicationContext.getBeanFactory().registerResolvableDependency(Injector.class, injector); + applicationContext.getBeanFactory().registerSingleton("injector", injector); + } + + static class CreateInjectorEvent extends ApplicationEvent { + private static final long serialVersionUID = -6546970378679850504L; + + public CreateInjectorEvent() { + super(serialVersionUID); } } } + + diff --git a/src/test/java/org/springframework/guice/BeanPostProcessorTests.java b/src/test/java/org/springframework/guice/BeanPostProcessorTests.java index e606523..0305cb4 100644 --- a/src/test/java/org/springframework/guice/BeanPostProcessorTests.java +++ b/src/test/java/org/springframework/guice/BeanPostProcessorTests.java @@ -87,7 +87,7 @@ class BeanPostProcessorTestConfig { } - public static class TestBeanPostProcessor implements BeanPostProcessor, Ordered { + public static class TestBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if(bean instanceof PostProcessedBean) { @@ -100,11 +100,6 @@ class BeanPostProcessorTestConfig { public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } - - @Override - public int getOrder() { - return 0; - } } @Bean diff --git a/src/test/java/org/springframework/guice/BindingDeduplicationTests.java b/src/test/java/org/springframework/guice/BindingDeduplicationTests.java index a8e202f..144e91b 100644 --- a/src/test/java/org/springframework/guice/BindingDeduplicationTests.java +++ b/src/test/java/org/springframework/guice/BindingDeduplicationTests.java @@ -1,12 +1,12 @@ package org.springframework.guice; import com.google.inject.AbstractModule; +import com.google.inject.CreationException; import com.google.inject.Module; import com.google.inject.multibindings.OptionalBinder; import org.junit.AfterClass; import org.junit.Test; -import org.springframework.beans.factory.BeanCreationException; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -35,7 +35,7 @@ public class BindingDeduplicationTests { context.close(); } - @Test(expected = BeanCreationException.class) + @Test(expected = CreationException.class) public void verifyDuplicateBindingErrorWhenDedupeNotEnabled() { System.setProperty("spring.guice.dedup", "false"); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( diff --git a/src/test/java/org/springframework/guice/InjectorFactoryTests.java b/src/test/java/org/springframework/guice/InjectorFactoryTests.java index 8c12392..3b3916c 100644 --- a/src/test/java/org/springframework/guice/InjectorFactoryTests.java +++ b/src/test/java/org/springframework/guice/InjectorFactoryTests.java @@ -4,6 +4,7 @@ import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.ApplicationContextException; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -31,7 +32,7 @@ public class InjectorFactoryTests { context.close(); } - @Test(expected = BeanCreationException.class) + @Test(expected = ApplicationContextException.class) public void testMultipleInjectorFactoriesThrowsApplicationContextException() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(InjectorFactoryConfig.class, SecondInjectorFactoryConfig.class, ModulesConfig.class); diff --git a/src/test/java/org/springframework/guice/annotation/EnableGuiceModulesTests.java b/src/test/java/org/springframework/guice/annotation/EnableGuiceModulesTests.java index 46bef28..ae04175 100644 --- a/src/test/java/org/springframework/guice/annotation/EnableGuiceModulesTests.java +++ b/src/test/java/org/springframework/guice/annotation/EnableGuiceModulesTests.java @@ -19,6 +19,8 @@ import javax.inject.Named; import com.google.inject.AbstractModule; import com.google.inject.Injector; +import com.google.inject.Provides; +import com.google.inject.Singleton; import org.junit.After; import org.junit.Test; @@ -75,6 +77,13 @@ public class EnableGuiceModulesTests { context.close(); } + @Test + public void testInjectorCreationDoesNotCauseCircularDependencyError() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MySpringConfig.class); + assertNotNull(context.getBean(SpringProvidedBean.class)); + context.close(); + } + interface Service { } @@ -150,4 +159,50 @@ public class EnableGuiceModulesTests { } + public static class SpringProvidedBean { + public SpringProvidedBean(GuiceProvidedBean guiceProvidedBean) { + } + } + + public static class GuiceProvidedBean { + } + + public static class GuiceService { + + @Inject + public GuiceService(SpringProvidedBean springProvidedBean) { + } + } + + public static class MyGuiceModule extends AbstractModule { + + @Override + protected void configure() { + bind(GuiceService.class).asEagerSingleton(); + } + + @Provides + @Singleton + public GuiceProvidedBean guiceProvidedBean() { + return new GuiceProvidedBean(); + } + + } + + @Configuration + @EnableGuiceModules + public static class MySpringConfig { + + @Bean + public SpringProvidedBean baz(GuiceProvidedBean guiceProvidedBean) { + return new SpringProvidedBean(guiceProvidedBean); + } + + @Bean + public MyGuiceModule bazModule() { + return new MyGuiceModule(); + } + + } + }