Skip to content

Commit

Permalink
Merge pull request #77 from zalando-stups/typed-configs
Browse files Browse the repository at this point in the history
Enable structured types as config values
  • Loading branch information
lukasniemeier-zalando authored Nov 24, 2023
2 parents 4ff9c60 + 44922c2 commit 4c0735c
Show file tree
Hide file tree
Showing 25 changed files with 686 additions and 259 deletions.
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,22 @@ And the _@ConfigurationServiceScan_ annotation hints the Baigan registrar to loo
@BaiganConfig
public interface ExpressFeature {

public Boolean enabled();
Boolean enabled();

public String serviceUrl();
String serviceUrl();

SomeStructuredConfigClass complexConfiguration();

List<String> configList();

Map<UUID, List<SomeConfigObject>> nestedGenericConfiguration();
}
```

The individual methods may have arbitrary classes as return types, in particular complex structured types are supported, including Generics.

**Note**: Primitives are not supported as return types as they cannot be null and therefore cannot express a missing configuration value.

The above example code enables the application to inject _ExpressFeature_ spring bean into any other Spring bean:

```Java
Expand All @@ -84,13 +93,20 @@ This is done using the Spring Bean of type `RepositoryFactory`, which allows cre
types. The following example shows how to configure a filesystem based repository.

```Java
@Bean
public ConfigurationRepository configurationRepository(RepositoryFactory factory) {
return factory.fileSystemConfigurationRepository()
.fileName("configs.json");
@Configuration
public class ApplicationConfiguration {

@Bean
public ConfigurationRepository configurationRepository(RepositoryFactory factory){
return factory.fileSystemConfigurationRepository()
.fileName("configs.json");
}
}
```

Check the documentation of the builders for details on how to configure the repositories. In particular, all
repositories can be configured with a Jackson `ObjectMapper` used to deserialize the configuration.

### Creating configurations
Baigan configurations follow a specific schema and can be stored on any of the supported repositories.

Expand Down
21 changes: 21 additions & 0 deletions src/main/java/org/zalando/baigan/model/In.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package org.zalando.baigan.model;

import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
Expand Down Expand Up @@ -45,4 +47,23 @@ public boolean eval(final String forValue) {
return inValue.contains(forValue);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
In in = (In) o;
return Objects.equals(inValue, in.inValue);
}

@Override
public int hashCode() {
return Objects.hash(inValue);
}

@Override
public String toString() {
return new StringJoiner(", ", In.class.getSimpleName() + "[", "]")
.add("inValue=" + inValue)
.toString();
}
}
23 changes: 23 additions & 0 deletions src/main/java/org/zalando/baigan/proxy/BaiganConfigClasses.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.zalando.baigan.proxy;

import java.lang.reflect.Type;
import java.util.Map;

public class BaiganConfigClasses {
private Map<String, Type> configTypesByKey;

public BaiganConfigClasses() {}

public void setConfigTypesByKey(Map<String, Type> configTypesByKey) {
configTypesByKey.forEach((key, value) -> {
if (value.getClass().isPrimitive()) {
throw new IllegalArgumentException("Config " + key + " has an illegal return type " + value + ". Primitives are not supported as return type.");
}
});
this.configTypesByKey = configTypesByKey;
}

public Map<String, Type> getConfigTypesByKey() {
return configTypesByKey;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,25 @@
import org.zalando.baigan.annotation.BaiganConfig;
import org.zalando.baigan.annotation.ConfigurationServiceScan;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toMap;
import static org.zalando.baigan.proxy.ProxyUtils.createKey;

/**
* ImportBeanDefinitionRegistrar implementation that finds the
* {@link ConfigurationServiceScan} annotations, delegates the scanning of
* packages and proxy bean creation further down to the corresponding
* implementations.
*
* @see ConfigurationServiceBeanFactory
*
* @author mchand
*
* @see ConfigurationServiceBeanFactory
*/
public class ConfigurationBeanDefinitionRegistrar
implements ImportBeanDefinitionRegistrar {
Expand Down Expand Up @@ -83,38 +87,49 @@ private void createAndRegisterBeanDefinitions(final Set<String> packages,
final ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateInterfaceProvider();
scanner.addIncludeFilter(new AnnotationTypeFilter(BaiganConfig.class));

List<Class<?>> baiganConfigClasses = new ArrayList<>();

for (final String singlePackage : packages) {
final Set<BeanDefinition> candidates = scanner.findCandidateComponents(singlePackage);
for (BeanDefinition definition : candidates) {
if (definition instanceof GenericBeanDefinition) {
final GenericBeanDefinition genericDefinition = (GenericBeanDefinition) definition;
registerAsBean(registry, genericDefinition);
final Class<?> baiganConfigClass = registerAsBean(registry, genericDefinition);
baiganConfigClasses.add(baiganConfigClass);
} else {
throw new IllegalStateException(
String.format(
"Unable to read required metadata of configuration candidate [%s]",
definition
)
String.format(
"Unable to read required metadata of configuration candidate [%s]",
definition
)
);
}
}
}
Map<String, Type> configTypesByKey = baiganConfigClasses.stream().flatMap(clazz ->
Arrays.stream(clazz.getMethods()).map(method -> new ConfigType(createKey(clazz, method), method.getGenericReturnType()))
).collect(toMap(c -> c.key, c -> c.type));
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(BaiganConfigClasses.class);
beanDefinition.getPropertyValues().add("configTypesByKey", configTypesByKey);
registry.registerBeanDefinition("baiganConfigClasses", beanDefinition);
}

private void registerAsBean(final BeanDefinitionRegistry registry, final GenericBeanDefinition genericDefinition) {
private Class<?> registerAsBean(final BeanDefinitionRegistry registry, final GenericBeanDefinition genericDefinition) {
try {
final Class<?> interfaceToImplement = genericDefinition.resolveBeanClass(
registry.getClass().getClassLoader()
registry.getClass().getClassLoader()
);
registerAsBean(registry, interfaceToImplement);
return interfaceToImplement;
} catch (final ClassNotFoundException e) {
throw new IllegalStateException("Unable to register annotated interface as configuration bean", e);
}
}

private void registerAsBean(final BeanDefinitionRegistry registry, final Class<?> interfaceToImplement) {
final BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
ConfigurationServiceBeanFactory.class
ConfigurationServiceBeanFactory.class
);
builder.addPropertyValue("candidateInterface", interfaceToImplement);

Expand All @@ -123,7 +138,7 @@ private void registerAsBean(final BeanDefinitionRegistry registry, final Class<?
}

private static class ClassPathScanningCandidateInterfaceProvider
extends ClassPathScanningCandidateComponentProvider {
extends ClassPathScanningCandidateComponentProvider {

public ClassPathScanningCandidateInterfaceProvider() {
super(false);
Expand All @@ -136,4 +151,13 @@ protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
}
}

private static class ConfigType {
private final String key;
private final Type type;

public ConfigType(String key, Type type) {
this.key = key;
this.type = type;
}
}
}
12 changes: 11 additions & 1 deletion src/main/java/org/zalando/baigan/proxy/ProxyUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import com.google.common.base.CaseFormat;
import com.google.common.base.Strings;

import java.lang.reflect.Method;

/**
* The class to contain utility methods used in proxying configuration beans.
*
Expand All @@ -28,7 +30,15 @@
public class ProxyUtils {
private static final String NAMESPACE_SEPARATOR = ".";

public static String dottify(final String text) {
public static String createKey(final Class<?> clazz, Method method) {
final String methodName = method.getName();
final String nameSpace = clazz.getSimpleName();

return ProxyUtils.dottify(nameSpace) + "."
+ ProxyUtils.dottify(methodName);
}

private static String dottify(final String text) {

if (Strings.isNullOrEmpty(text)) {
return NAMESPACE_SEPARATOR;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.zalando.baigan.proxy.handler;

import com.google.common.base.Supplier;
import com.google.common.primitives.Primitives;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
Expand All @@ -12,19 +11,17 @@
import org.zalando.baigan.context.ContextProviderRetriever;
import org.zalando.baigan.model.Configuration;
import org.zalando.baigan.context.ContextProvider;
import org.zalando.baigan.proxy.ProxyUtils;
import org.zalando.baigan.repository.ConfigurationRepository;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Suppliers.memoize;
import static org.zalando.baigan.proxy.ProxyUtils.createKey;

/**
* This class provides a concrete implementation for the Method invocation
Expand Down Expand Up @@ -58,58 +55,25 @@ public void setBeanFactory(final BeanFactory beanFactory) throws BeansException
}

@Override
protected Object handleInvocation(Object proxy, Method method,
Object[] args) throws Throwable {
final String methodName = method.getName();
final String nameSpace = getNamespace(proxy);

final String key = ProxyUtils.dottify(nameSpace) + "."
+ ProxyUtils.dottify(methodName);
protected Object handleInvocation(Object proxy, Method method, Object[] args) {
final String key = createKey(getClass(proxy), method);
final Object result = getConfig(key);
if (result == null) {
LOG.warn("Configuration not found for key: {}", key);
return null;
}

final Class<?> declaredReturnType = method.getReturnType();

try {

Constructor<?> constructor;
if (declaredReturnType.isInstance(result)) {
return result;
} else if (declaredReturnType.isPrimitive()) {
final Class<?> resultClass = result.getClass();
constructor = Primitives.wrap(declaredReturnType)
.getDeclaredConstructor(resultClass);
} else if (declaredReturnType.isEnum()) {
for (Object t : Arrays
.asList(declaredReturnType.getEnumConstants())) {
if (result.toString().equalsIgnoreCase(t.toString())) {
return t;
}
}
LOG.warn("Unable to map [{}] to enum type [{}].", result, declaredReturnType.getName());
return null;
} else {
constructor = declaredReturnType
.getDeclaredConstructor(result.getClass());
}
return constructor.newInstance(result);
} catch (Exception exception) {
LOG.warn(
"Wrong or Incompatible configuration. Cannot find a constructor to create object of type "
+ declaredReturnType
+ " for value of the configuration key " + key,
exception);
if (!method.getReturnType().isInstance(result)) {
LOG.error("Configuration repository returned object of wrong type. Expected: {}, actual: {}", method.getReturnType(), result.getClass());
return null;
}
return null;

return result;
}

private String getNamespace(final Object proxy) {
private Class<?> getClass(final Object proxy) {
final Class<?>[] interfaces = proxy.getClass().getInterfaces();
checkState(interfaces.length == 1, "Expected exactly one interface on proxy object.");
return interfaces[0].getSimpleName();
return interfaces[0];
}

private Object getConfig(final String key) {
Expand Down

This file was deleted.

Loading

0 comments on commit 4c0735c

Please sign in to comment.