Skip to content

Commit

Permalink
Load properties for @ConfigurationSource on @provides methods
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
elandau committed Jun 27, 2019
1 parent dc55e24 commit b923dcd
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
* @author elandau
*
*/
@Target(ElementType.TYPE)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ConfigurationSource {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand All @@ -43,7 +50,11 @@ public class ConfigurationInjectingListener implements ProvisionListener {

@com.google.inject.Inject(optional = true)
private CascadeStrategy cascadeStrategy;


private Map<Binding<?>, ConfigurationSourceHolder> bindingToHolder = new ConcurrentHashMap<>();

private ConfigMapper mapper = new ConfigMapper();

@Inject
public static void init(ConfigurationInjectingListener listener) {
LOG.info("Initializing ConfigurationInjectingListener");
Expand All @@ -52,63 +63,114 @@ public static void init(ConfigurationInjectingListener listener) {
CascadeStrategy getCascadeStrategy() {
return cascadeStrategy != null ? cascadeStrategy : NoCascadeStrategy.INSTANCE;
}

private ConfigMapper mapper = new ConfigMapper();

@Override
public <T> void onProvision(ProvisionInvocation<T> 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<String> 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<ConfigurationSource, Configuration> 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<Object, ConfigurationSourceHolder>() {
@Override
protected ConfigurationSourceHolder visitOther(Binding binding) {
final Class<?> clazz = binding.getKey().getTypeLiteral().getRawType();
return new ConfigurationSourceHolder(
clazz.getDeclaredAnnotation(ConfigurationSource.class),
clazz.getDeclaredAnnotation(Configuration.class)
);
}

@Override
public ConfigurationSourceHolder visit(ProviderInstanceBinding providerInstanceBinding) {
final Provider provider = providerInstanceBinding.getUserSuppliedProvider();
if (provider instanceof ProvidesMethodBinding) {
final ProvidesMethodBinding providesMethodBinding = (ProvidesMethodBinding)provider;
ConfigurationSource configurationSource = providesMethodBinding.getMethod().getAnnotation(ConfigurationSource.class);
if (configurationSource != null) {
return new ConfigurationSourceHolder(
configurationSource,
null);
}
}

return super.visit(providerInstanceBinding);
}

try {
mapper.mapConfig(provision.provision(), config, new IoCContainer() {
@Override
public <S> S getInstance(String name, Class<S> type) {
return injector.getInstance(Key.get(type, Names.named(name)));
});
}

@Override
public <T> void onProvision(ProvisionInvocation<T> 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<String> 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> S getInstance(String name, Class<S> type) {
return injector.getInstance(Key.get(type, Names.named(name)));
}
});
}
catch (Exception e) {
throw new ProvisionException("Unable to bind configuration to " + clazz.getName(), e);
}
}
}
});
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ default String getDefault() {
}
}

public static interface MySubConfig {
public interface MySubConfig {
@DefaultValue("0")
int getInteger();
}

@Configuration(prefix="foo")
public static interface MyConfigWithPrefix {
public interface MyConfigWithPrefix {
@DefaultValue("0")
int getInteger();

Expand Down Expand Up @@ -107,9 +107,9 @@ public MyConfig getMyConfig(ConfigProxyFactory factory) {

@Configuration(prefix="prefix-${env}", allowFields=true)
@ConfigurationSource(value={"moduleTest"}, cascading=MyCascadingStrategy.class)
public static interface ModuleTestConfig {
public Boolean isLoaded();
public String getProp1();
public interface ModuleTestConfig {
Boolean isLoaded();
String getProp1();
}

@Test
Expand All @@ -130,7 +130,7 @@ public ModuleTestConfig getMyConfig(ConfigProxyFactory factory) {
injector.getInstance(Config.class).accept(new PrintStreamVisitor());
}

public static interface DefaultMethodWithAnnotation {
public interface DefaultMethodWithAnnotation {
@DefaultValue("fromAnnotation")
default String getValue() {
return "fromDefault";
Expand Down

0 comments on commit b923dcd

Please sign in to comment.