Ensure bean post processors are applied before injector is created

In this way, Guice modules can declare bindings asEagerSingleton()
This commit is contained in:
Taylor Wicksell
2018-02-22 09:39:29 -06:00
committed by Dave Syer
parent ee35353c02
commit bd30107666
5 changed files with 237 additions and 66 deletions

View File

@@ -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<T> implements FactoryBean<T> {
private final Provider<T> provider;
private final Key<T> key;
private final Class<T> beanType;
@Autowired
private Injector injector;
public GuiceFactoryBean(Class<T> beanType, Provider<T> provider) {
this.provider = provider;
public GuiceFactoryBean(Class<T> beanType, Key<T> key) {
this.beanType = beanType;
this.key = key;
}
@Override
public T getObject() throws Exception {
return (T) provider.get();
return (T) injector.getInstance(key);
}
@Override

View File

@@ -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<CreateInjectorSignalEvent> {
ApplicationContext applicationContext;
private List<Module> modules;
private ConfigurableListableBeanFactory beanFactory;
private Injector createInjector(Collection<Module> modules) {
return Guice.createInjector(modules);
private void createInjector(List<Module> modules,
ConfigurableListableBeanFactory beanFactory) {
Injector injector = null;
try {
Map<String, InjectorFactory> 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<Key<?>, Binding<?>> entry : injector.getBindings().entrySet()) {
if (entry.getKey().getTypeLiteral().getRawType().equals(Injector.class) ||
"spring-guice".equals(entry.getValue().getSource().toString())) {
private void mapBindings(Map<Key<?>, Binding<?>> bindings,
BeanDefinitionRegistry registry) {
for (Entry<Key<?>, 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<Module>(((ConfigurableListableBeanFactory) registry)
.getBeansOfType(Module.class).values());
Map<Key<?>, Binding<?>> bindings = new HashMap<Key<?>, 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<Module> modules = new ArrayList<Module>(
((DefaultListableBeanFactory) registry).getBeansOfType(Module.class).values());
modules.add(new SpringModule(this.applicationContext));
Injector injector = null;
try {
Map<String, InjectorFactory> 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);
}
}

View File

@@ -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))

View File

@@ -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();
}
};
}
}

View File

@@ -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