From ef7444e7061b45d160c5989fdedbc7f0b39effa7 Mon Sep 17 00:00:00 2001 From: elandau Date: Wed, 26 Jun 2019 19:09:33 -0700 Subject: [PATCH] Load properties for @ConfigurationSource on @Provides methods Loading configuration with @Provides methods currently requires a hack to inject a second type that is annotated with @ConfigurationSource. This change makes it possible to annotate the @Provides with with @ConfigurationSource to have the configuration loaded before the bean is provisioned. For example, ``` @ConfigurationSource({"foo"}) @Provides @Singleton Foo getFoo() { } ``` will result in foo.properties (and it's variants) being loaded before getFoo is called. --- .../api/annotations/ConfigurationSource.java | 2 +- .../guice/ConfigurationInjectingListener.java | 175 ++++++++++++------ .../ConfigurationInjectingListenerTest.java | 33 ++++ 3 files changed, 151 insertions(+), 59 deletions(-) diff --git a/archaius2-api/src/main/java/com/netflix/archaius/api/annotations/ConfigurationSource.java b/archaius2-api/src/main/java/com/netflix/archaius/api/annotations/ConfigurationSource.java index 7094ddb9f..febe44b10 100644 --- a/archaius2-api/src/main/java/com/netflix/archaius/api/annotations/ConfigurationSource.java +++ b/archaius2-api/src/main/java/com/netflix/archaius/api/annotations/ConfigurationSource.java @@ -36,7 +36,7 @@ * @author elandau * */ -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ConfigurationSource { /** diff --git a/archaius2-guice/src/main/java/com/netflix/archaius/guice/ConfigurationInjectingListener.java b/archaius2-guice/src/main/java/com/netflix/archaius/guice/ConfigurationInjectingListener.java index 5b8820c57..7e6ad369d 100644 --- a/archaius2-guice/src/main/java/com/netflix/archaius/guice/ConfigurationInjectingListener.java +++ b/archaius2-guice/src/main/java/com/netflix/archaius/guice/ConfigurationInjectingListener.java @@ -1,9 +1,13 @@ package com.netflix.archaius.guice; +import com.google.inject.Binding; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.ProvisionException; import com.google.inject.name.Names; +import com.google.inject.spi.DefaultBindingTargetVisitor; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProvidesMethodBinding; import com.google.inject.spi.ProvisionListener; import com.netflix.archaius.ConfigMapper; import com.netflix.archaius.api.CascadeStrategy; @@ -16,15 +20,18 @@ import com.netflix.archaius.api.exceptions.ConfigException; import com.netflix.archaius.api.inject.LibrariesLayer; import com.netflix.archaius.cascade.NoCascadeStrategy; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Provider; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; public class ConfigurationInjectingListener implements ProvisionListener { private static final Logger LOG = LoggerFactory.getLogger(ConfigurationInjectingListener.class); @@ -43,7 +50,11 @@ public class ConfigurationInjectingListener implements ProvisionListener { @com.google.inject.Inject(optional = true) private CascadeStrategy cascadeStrategy; - + + private Map, ConfigurationSourceHolder> bindingToHolder = new ConcurrentHashMap<>(); + + private ConfigMapper mapper = new ConfigMapper(); + @Inject public static void init(ConfigurationInjectingListener listener) { LOG.info("Initializing ConfigurationInjectingListener"); @@ -52,63 +63,111 @@ public static void init(ConfigurationInjectingListener listener) { CascadeStrategy getCascadeStrategy() { return cascadeStrategy != null ? cascadeStrategy : NoCascadeStrategy.INSTANCE; } - - private ConfigMapper mapper = new ConfigMapper(); - - @Override - public void onProvision(ProvisionInvocation provision) { - Class clazz = provision.getBinding().getKey().getTypeLiteral().getRawType(); - - // - // Configuration Loading - // - final ConfigurationSource source = clazz.getDeclaredAnnotation(ConfigurationSource.class); - if (source != null) { - if (injector == null) { - LOG.warn("Can't inject configuration into {} until ConfigurationInjectingListener has been initialized", clazz.getName()); - return; - } - - CascadeStrategy strategy = source.cascading() != ConfigurationSource.NullCascadeStrategy.class - ? injector.getInstance(source.cascading()) : getCascadeStrategy(); - - List sources = Arrays.asList(source.value()); - Collections.reverse(sources); - for (String resourceName : sources) { - LOG.debug("Trying to loading configuration resource {}", resourceName); - try { - CompositeConfig loadedConfig = loader - .newLoader() - .withCascadeStrategy(strategy) - .load(resourceName); - libraries.addConfig(resourceName, loadedConfig); - } catch (ConfigException e) { - throw new ProvisionException("Unable to load configuration for " + resourceName, e); - } + + private static class ConfigurationSourceHolder { + private final ConfigurationSource configurationSource; + private final Configuration configuration; + private final AtomicBoolean loaded = new AtomicBoolean(); + + public ConfigurationSourceHolder(ConfigurationSource configurationSource, Configuration configuration) { + this.configurationSource = configurationSource; + this.configuration = configuration; + } + + public void ifNotLoaded(BiConsumer consumer) { + if (loaded.compareAndSet(false, true) && (configurationSource != null || configuration != null)) { + consumer.accept(configurationSource, configuration); } } - - // - // Configuration binding - // - Configuration configAnnot = clazz.getAnnotation(Configuration.class); - if (configAnnot != null) { - if (injector == null) { - LOG.warn("Can't inject configuration into {} until ConfigurationInjectingListener has been initialized", clazz.getName()); - return; + } + + private ConfigurationSourceHolder getConfigurationSource(Binding binding) { + final ConfigurationSourceHolder holder = bindingToHolder.get(binding); + if (holder != null) { + return holder; + } + return bindingToHolder.computeIfAbsent(binding, this::createConfigurationSourceHolder); + } + + private ConfigurationSourceHolder createConfigurationSourceHolder(Binding binding) { + return binding.acceptTargetVisitor(new DefaultBindingTargetVisitor() { + @Override + protected ConfigurationSourceHolder visitOther(Binding binding) { + final Class clazz = binding.getKey().getTypeLiteral().getRawType(); + return new ConfigurationSourceHolder( + clazz.getDeclaredAnnotation(ConfigurationSource.class), + clazz.getDeclaredAnnotation(Configuration.class) + ); } - - try { - mapper.mapConfig(provision.provision(), config, new IoCContainer() { - @Override - public S getInstance(String name, Class type) { - return injector.getInstance(Key.get(type, Names.named(name))); + + @Override + public ConfigurationSourceHolder visit(ProviderInstanceBinding providerInstanceBinding) { + final Provider provider = providerInstanceBinding.getUserSuppliedProvider(); + if (provider instanceof ProvidesMethodBinding) { + final ProvidesMethodBinding providesMethodBinding = (ProvidesMethodBinding)provider; + return new ConfigurationSourceHolder( + providesMethodBinding.getMethod().getAnnotation(ConfigurationSource.class), + null); + } + + return super.visit(providerInstanceBinding); + } + }); + } + + @Override + public void onProvision(ProvisionInvocation provision) { + getConfigurationSource(provision.getBinding()).ifNotLoaded((source, configAnnot) -> { + Class clazz = provision.getBinding().getKey().getTypeLiteral().getRawType(); + // + // Configuration Loading + // + if (source != null) { + if (injector == null) { + LOG.warn("Can't inject configuration into {} until ConfigurationInjectingListener has been initialized", clazz.getName()); + return; + } + + CascadeStrategy strategy = source.cascading() != ConfigurationSource.NullCascadeStrategy.class + ? injector.getInstance(source.cascading()) : getCascadeStrategy(); + + List sources = Arrays.asList(source.value()); + Collections.reverse(sources); + for (String resourceName : sources) { + LOG.debug("Trying to loading configuration resource {}", resourceName); + try { + CompositeConfig loadedConfig = loader + .newLoader() + .withCascadeStrategy(strategy) + .load(resourceName); + libraries.addConfig(resourceName, loadedConfig); + } catch (ConfigException e) { + throw new ProvisionException("Unable to load configuration for " + resourceName, e); } - }); + } } - catch (Exception e) { - throw new ProvisionException("Unable to bind configuration to " + clazz, e); + + // + // Configuration binding + // + if (configAnnot != null) { + if (injector == null) { + LOG.warn("Can't inject configuration into {} until ConfigurationInjectingListener has been initialized", clazz.getName()); + return; + } + + try { + mapper.mapConfig(provision.provision(), config, new IoCContainer() { + @Override + public S getInstance(String name, Class type) { + return injector.getInstance(Key.get(type, Names.named(name))); + } + }); + } + catch (Exception e) { + throw new ProvisionException("Unable to bind configuration to " + clazz.getName(), e); + } } - } + }); } } \ No newline at end of file diff --git a/archaius2-guice/src/test/java/com/netflix/archaius/guice/ConfigurationInjectingListenerTest.java b/archaius2-guice/src/test/java/com/netflix/archaius/guice/ConfigurationInjectingListenerTest.java index 6112a73c9..1677255a4 100644 --- a/archaius2-guice/src/test/java/com/netflix/archaius/guice/ConfigurationInjectingListenerTest.java +++ b/archaius2-guice/src/test/java/com/netflix/archaius/guice/ConfigurationInjectingListenerTest.java @@ -1,7 +1,10 @@ package com.netflix.archaius.guice; +import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; +import com.google.inject.Provides; +import com.google.inject.Singleton; import com.netflix.archaius.api.Config; import com.netflix.archaius.api.annotations.ConfigurationSource; import com.netflix.archaius.visitor.PrintStreamVisitor; @@ -25,4 +28,34 @@ public void confirmLoadOrder() { config.accept(new PrintStreamVisitor()); Assert.assertEquals("prod", config.getString("moduleTest.value")); } + + public static class Bar { + + } + + public static class BarModule extends AbstractModule { + + @Override + protected void configure() { + + } + + @ConfigurationSource({"moduleTest"}) + @Provides + @Singleton + public Bar getBar() { + return new Bar(); + } + } + + @Test + public void configProvidesMethodConfigLoaded() { + Injector injector = Guice.createInjector(new ArchaiusModule(), new BarModule()); + injector.getInstance(Bar.class); + + Config config = injector.getInstance(Config.class); + config.accept(new PrintStreamVisitor()); + Assert.assertEquals("true", config.getString("moduleTest.loaded")); + } + }