Skip to content

Commit

Permalink
Merge pull request #695 from fatroom/type_reference_resolving
Browse files Browse the repository at this point in the history
Added resolving beans by type
  • Loading branch information
Willi Schönborn committed Jul 16, 2019
2 parents 8db97b9 + 7d4a1b2 commit e04558e
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 11 deletions.
6 changes: 6 additions & 0 deletions MIGRATION.md
Expand Up @@ -79,6 +79,12 @@ The Apache HTTP client specific `GzipHttpRequestInterceptor` has been replaced w

## Spring Boot Auto Configuration

### Changed resolution of dependency beans

Riptide 2.x was looking for specific beans named `meterRegistry`, `logbook` or `tracer` during the construction
of appropriate plugins.
This behaviour changed to resolution be type.

### Added `enabled` properties

:warning: **All nested configurations now have an `enabled` flag which is disabled by default**
Expand Down
@@ -0,0 +1,114 @@
package org.zalando.riptide.autoconfigure;

import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.MeterRegistry;
import io.opentracing.Tracer;
import io.opentracing.contrib.concurrent.TracedExecutorService;
import io.opentracing.contrib.concurrent.TracedScheduledExecutorService;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.zalando.logbook.Logbook;
import org.zalando.riptide.failsafe.CircuitBreakerListener;
import org.zalando.riptide.failsafe.RetryListener;
import org.zalando.riptide.logbook.LogbookPlugin;
import org.zalando.riptide.micrometer.MicrometerPlugin;
import org.zalando.riptide.opentracing.OpenTracingPlugin;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static org.zalando.riptide.autoconfigure.ValueConstants.LOGBOOK_REF;
import static org.zalando.riptide.autoconfigure.ValueConstants.METER_REGISTRY_REF;
import static org.zalando.riptide.autoconfigure.ValueConstants.TRACER_REF;

@AllArgsConstructor
class DefaultRiptideConfigurer {
private final ConfigurableListableBeanFactory beanFactory;
private final RiptideProperties properties;

void register() {
properties.getClients().forEach(this::configure);
}

private void configure(final String id, final RiptideProperties.Client client) {
if (client.getTracing().getEnabled()) {
final BeanDefinition tracerRef = getBeanRef(Tracer.class, "tracer");

findBeanDefinition(id, TracedExecutorService.class)
.ifPresent(bd -> replaceConstructorArgumentWithBean(bd, TRACER_REF, tracerRef));

findBeanDefinition(id, OpenTracingPlugin.class)
.ifPresent(bd -> replaceConstructorArgumentWithBean(bd, TRACER_REF, tracerRef));

findBeanDefinition(id, TracedScheduledExecutorService.class)
.ifPresent(bd -> replaceConstructorArgumentWithBean(bd, TRACER_REF, tracerRef));
}

if (client.getLogging().getEnabled()) {
final BeanDefinition logbookRef = getBeanRef(Logbook.class, "logbook");

findBeanDefinition(id, LogbookPlugin.class)
.ifPresent(bd -> replaceConstructorArgumentWithBean(bd, LOGBOOK_REF, logbookRef));
}

if (client.getMetrics().getEnabled()) {
final BeanDefinition meterRegistryRef = getBeanRef(MeterRegistry.class, "meterRegistry");

findBeanDefinition(id, MicrometerPlugin.class)
.ifPresent(bd -> replaceConstructorArgumentWithBean(bd, METER_REGISTRY_REF, meterRegistryRef));

findBeanDefinition(id, RetryListener.class)
.ifPresent(bd -> replaceConstructorArgumentWithBean(bd, METER_REGISTRY_REF, meterRegistryRef));

findBeanDefinition(id, CircuitBreakerListener.class)
.ifPresent(bd -> replaceConstructorArgumentWithBean(bd, METER_REGISTRY_REF, meterRegistryRef));
}
}

@VisibleForTesting
BeanDefinition getBeanRef(Class type, String argName) {
Map<String, BeanDefinition> definitions = new HashMap<>();
// search primary bean definition
for (String beanName : beanFactory.getBeanNamesForType(type)) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
if (beanDefinition.isPrimary()) {
return beanDefinition;
}
definitions.put(beanName, beanDefinition);
}

// resolve by name
BeanDefinition beanDefinition = definitions.get(argName);
if (beanDefinition != null) {
return beanDefinition;
}

// if only one candidate present use it
if (definitions.size() == 1) {
return definitions.values().iterator().next();
}

throw new NoSuchBeanDefinitionException(type);
}

private Optional<BeanDefinition> findBeanDefinition(final String id, final Class<?> type) {
try {
final Name name = Name.name(id, type);
return Optional.of(beanFactory.getBeanDefinition(name.toNormalizedString()));
} catch (NoSuchBeanDefinitionException e) {
return Optional.empty();
}
}

private void replaceConstructorArgumentWithBean(final BeanDefinition bd, final String arg,
final BeanDefinition ref) {
bd.getConstructorArgumentValues()
.getIndexedArgumentValues()
.values().stream()
.filter(holder -> arg.equals(holder.getValue()))
.forEach(holder -> holder.setValue(ref));
}
}
Expand Up @@ -89,6 +89,9 @@
import static org.zalando.riptide.autoconfigure.RiptideProperties.Chaos.ErrorResponses;
import static org.zalando.riptide.autoconfigure.RiptideProperties.Chaos.Exceptions;
import static org.zalando.riptide.autoconfigure.RiptideProperties.Chaos.Latency;
import static org.zalando.riptide.autoconfigure.ValueConstants.LOGBOOK_REF;
import static org.zalando.riptide.autoconfigure.ValueConstants.METER_REGISTRY_REF;
import static org.zalando.riptide.autoconfigure.ValueConstants.TRACER_REF;

@Slf4j
@AllArgsConstructor
Expand Down Expand Up @@ -171,7 +174,7 @@ private String registerExecutor(final String id, final Client client) {
return registry.registerIfAbsent(id, TracedExecutorService.class, () ->
genericBeanDefinition(TracedExecutorService.class)
.addConstructorArgReference(executorId)
.addConstructorArgReference("tracer"));
.addConstructorArgValue(TRACER_REF));
}

return executorId;
Expand Down Expand Up @@ -329,7 +332,7 @@ private Optional<String> registerMicrometerPlugin(final String id, final Client
log.debug("Client [{}]: Registering [{}]", id, MicrometerPlugin.class.getSimpleName());
return genericBeanDefinition(MicrometerPluginFactory.class)
.setFactoryMethod("createMicrometerPlugin")
.addConstructorArgReference("meterRegistry")
.addConstructorArgValue(METER_REGISTRY_REF)
.addConstructorArgValue(ImmutableList.of(clientId(id)));
});

Expand All @@ -343,7 +346,7 @@ private Optional<String> registerLogbookPlugin(final String id, final Client cli
final String pluginId = registry.registerIfAbsent(id, LogbookPlugin.class, () -> {
log.debug("Client [{}]: Registering [{}]", id, LogbookPlugin.class.getSimpleName());
return genericBeanDefinition(LogbookPlugin.class)
.addConstructorArgReference("logbook");
.addConstructorArgValue(LOGBOOK_REF);
});

return Optional.of(pluginId);
Expand Down Expand Up @@ -381,7 +384,7 @@ private Optional<String> registerOpenTracingPlugin(final String id, final Client
log.debug("Client [{}]: Registering [{}]", id, OpenTracingPlugin.class.getSimpleName());
return genericBeanDefinition(OpenTracingPluginFactory.class)
.setFactoryMethod("create")
.addConstructorArgReference("tracer")
.addConstructorArgValue(TRACER_REF)
.addConstructorArgValue(client)
.addConstructorArgValue(registry.findRef(id, SpanDecorator.class).orElse(null));
});
Expand Down Expand Up @@ -497,7 +500,7 @@ private String registerScheduler(final String id, final Client client) {
return registry.registerIfAbsent(id, TracedScheduledExecutorService.class, () ->
genericBeanDefinition(TracedScheduledExecutorService.class)
.addConstructorArgReference(executorId)
.addConstructorArgReference("tracer"));
.addConstructorArgValue(TRACER_REF));
}

return executorId;
Expand Down Expand Up @@ -531,7 +534,7 @@ private String registerRetryListener(final String id, final Client client) {
if (client.getMetrics().getEnabled()) {
return genericBeanDefinition(MicrometerPluginFactory.class)
.setFactoryMethod("createRetryListener")
.addConstructorArgReference("meterRegistry")
.addConstructorArgValue(METER_REGISTRY_REF)
.addConstructorArgValue(ImmutableList.of(clientId(id)));
} else {
return genericBeanDefinition(MicrometerPluginFactory.class)
Expand All @@ -545,7 +548,7 @@ private String registerCircuitBreakerListener(final String id, final Client clie
if (client.getMetrics().getEnabled()) {
return genericBeanDefinition(MicrometerPluginFactory.class)
.setFactoryMethod("createCircuitBreakerListener")
.addConstructorArgReference("meterRegistry")
.addConstructorArgValue(METER_REGISTRY_REF)
.addConstructorArgValue(ImmutableList.of(clientId(id), clientName(id, client)));
} else {
return genericBeanDefinition(MicrometerPluginFactory.class)
Expand Down
Expand Up @@ -29,18 +29,19 @@ public void setEnvironment(final Environment environment) {
from(((ConfigurableEnvironment) environment).getPropertySources());
final Binder binder = new Binder(sources);

this.properties = binder.bind("riptide", RiptideProperties.class).orElseCreate(RiptideProperties.class);
this.properties = Defaulting.withDefaults(binder.bind("riptide", RiptideProperties.class).orElseCreate(RiptideProperties.class));
}

@Override
public void postProcessBeanDefinitionRegistry(final BeanDefinitionRegistry registry) {
final RiptideRegistrar registrar = registrarFactory.apply(new Registry(registry), Defaulting.withDefaults(properties));
final RiptideRegistrar registrar = registrarFactory.apply(new Registry(registry), properties);
registrar.register();
}

@Override
public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) throws BeansException {
// nothing to do
final DefaultRiptideConfigurer configurer = new DefaultRiptideConfigurer(beanFactory, properties);
configurer.register();
}

}
@@ -0,0 +1,17 @@
package org.zalando.riptide.autoconfigure;

import com.google.gag.annotation.remark.Hack;

/**
* Constants referring to beans that need to be looked up by types during Riptide clients constructions.
* <p>This is an artificial arrangement of 16 unicode characters, with its sole purpose
* being to never match user-declared values.
*/
@Hack("To allow search of the parameters that need to be replaced with Bean references")
class ValueConstants {
private ValueConstants() {}

static final String METER_REGISTRY_REF = "\n\t\t\n\t\t\n\uE000\uE001\uE001\n\t\t\t\t\n";
static final String TRACER_REF = "\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n";
static final String LOGBOOK_REF = "\n\t\t\n\t\t\n\uE000\uE001\uE003\n\t\t\t\t\n";
}
@@ -0,0 +1,96 @@
package org.zalando.riptide.autoconfigure;

import io.opentracing.Tracer;
import io.opentracing.noop.NoopTracerFactory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

final class DefaultRiptideConfigurerTest {
@TestConfiguration
static class PrimaryTracerConfiguration {
@Bean
@Primary
Tracer primaryTracer() {
return NoopTracerFactory.create();
}

@Bean
Tracer secondaryTracer() {
return NoopTracerFactory.create();
}
}

@TestConfiguration
static class SingleTracerConfiguration {
@Bean
Tracer opentracingTracer() {
return NoopTracerFactory.create();
}
}

@TestConfiguration
static class DoubleTracerConfiguration {
@Bean
Tracer tracer() {
return NoopTracerFactory.create();
}

@Bean
Tracer secondaryTracer() {
return NoopTracerFactory.create();
}
}

@Test
void shouldFindPrimaryBeanDefinitionIfAvailable() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(PrimaryTracerConfiguration.class);
context.refresh();
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
DefaultRiptideConfigurer configurer = new DefaultRiptideConfigurer(beanFactory, null);
BeanDefinition bd = configurer.getBeanRef(Tracer.class, "tracer");
assertThat(bd.isPrimary()).isTrue();
}

@Test
void shouldBeanDefinitionIfSingleBeanRegisteredForType() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(SingleTracerConfiguration.class);
context.refresh();
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
DefaultRiptideConfigurer configurer = new DefaultRiptideConfigurer(beanFactory, null);
BeanDefinition bd = configurer.getBeanRef(Tracer.class, "tracer");
assertThat(bd.getFactoryMethodName()).isEqualTo("opentracingTracer");
}

@Test
void shouldFindBeanDefinitionByNameIfNoPrimaryBeanAvailable() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(DoubleTracerConfiguration.class);
context.refresh();
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
DefaultRiptideConfigurer configurer = new DefaultRiptideConfigurer(beanFactory, null);
BeanDefinition bd = configurer.getBeanRef(Tracer.class, "tracer");
assertThat(bd.getFactoryMethodName()).isEqualTo("tracer");
}

@Test
void shouldFailIfMultipleBeanFoundWithoutCorrespondingName() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(DoubleTracerConfiguration.class);
context.refresh();
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
DefaultRiptideConfigurer configurer = new DefaultRiptideConfigurer(beanFactory, null);
assertThrows(NoSuchBeanDefinitionException.class,
() -> configurer.getBeanRef(Tracer.class, "opentracingTracer"));
}
}
Expand Up @@ -12,7 +12,7 @@
public class OpenTracingTestAutoConfiguration {

@Bean
public Tracer tracer() {
public Tracer sampleTracer() {
return new MockTracer();
}

Expand Down

0 comments on commit e04558e

Please sign in to comment.