Commit 2aea4375 authored by chao.chang's avatar chao.chang Committed by Stephane Nicoll

Make Actuator dedicated ConversionService configurable

See gh-16449
parent 38a42a86
...@@ -16,14 +16,26 @@ ...@@ -16,14 +16,26 @@
package org.springframework.boot.actuate.autoconfigure.endpoint; package org.springframework.boot.actuate.autoconfigure.endpoint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor; import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
/** /**
...@@ -32,6 +44,7 @@ import org.springframework.core.env.Environment; ...@@ -32,6 +44,7 @@ import org.springframework.core.env.Environment;
* *
* @author Phillip Webb * @author Phillip Webb
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Chao Chang
* @since 2.0.0 * @since 2.0.0
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
...@@ -39,8 +52,10 @@ public class EndpointAutoConfiguration { ...@@ -39,8 +52,10 @@ public class EndpointAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public ParameterValueMapper endpointOperationParameterMapper() { public ParameterValueMapper endpointOperationParameterMapper(
return new ConversionServiceParameterValueMapper(); ApplicationContext applicationContext) {
return new ConversionServiceParameterValueMapper(
new Factory(applicationContext.getAutowireCapableBeanFactory()).create());
} }
@Bean @Bean
...@@ -49,4 +64,48 @@ public class EndpointAutoConfiguration { ...@@ -49,4 +64,48 @@ public class EndpointAutoConfiguration {
return new CachingOperationInvokerAdvisor(new EndpointIdTimeToLivePropertyFunction(environment)); return new CachingOperationInvokerAdvisor(new EndpointIdTimeToLivePropertyFunction(environment));
} }
private static class Factory {
@SuppressWarnings("rawtypes")
private final List<Converter> converters;
private final List<GenericConverter> genericConverters;
Factory(BeanFactory beanFactory) {
this.converters = beans(beanFactory, Converter.class,
EndpointConverter.VALUE);
this.genericConverters = beans(beanFactory, GenericConverter.class,
EndpointConverter.VALUE);
}
private <T> List<T> beans(BeanFactory beanFactory, Class<T> type,
String qualifier) {
if (beanFactory instanceof ListableBeanFactory) {
return beans(type, qualifier, (ListableBeanFactory) beanFactory);
}
return Collections.emptyList();
}
private <T> List<T> beans(Class<T> type, String qualifier,
ListableBeanFactory beanFactory) {
return new ArrayList<>(BeanFactoryAnnotationUtils
.qualifiedBeansOfType(beanFactory, type, qualifier).values());
}
public ConversionService create() {
if (this.converters.isEmpty() && this.genericConverters.isEmpty()) {
return ApplicationConversionService.getSharedInstance();
}
ApplicationConversionService conversionService = new ApplicationConversionService();
for (Converter<?, ?> converter : this.converters) {
conversionService.addConverter(converter);
}
for (GenericConverter genericConverter : this.genericConverters) {
conversionService.addConverter(genericConverter);
}
return conversionService;
}
}
} }
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.endpoint;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
/**
* Qualifier for beans that are needed to be converters for {@link Endpoint}.
*
* @author Chao Chang
*/
@Qualifier(EndpointConverter.VALUE)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EndpointConverter {
/**
* Concrete value for the {@link Qualifier @Qualifier}.
*/
String VALUE = "org.springframework.boot.actuate.autoconfigure.endpoint.EndpointConverter";
}
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.endpoint;
import java.util.Collections;
import java.util.Set;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
import org.springframework.boot.actuate.endpoint.invoke.ParameterMappingException;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link EndpointAutoConfiguration}.
*
* @author Chao Chang
*/
public class EndpointAutoConfigurationTests {
private static final AutoConfigurations CONFIGURATIONS = AutoConfigurations
.of(EndpointAutoConfiguration.class);
private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(CONFIGURATIONS);
@Test
public void mapShouldUseConfigurationConverter() {
this.contextRunner.withUserConfiguration(ConverterConfiguration.class)
.run((context) -> {
ParameterValueMapper parameterValueMapper = context
.getBean(ParameterValueMapper.class);
Object paramValue = parameterValueMapper.mapParameterValue(
new TestOperationParameter(Person.class), "John Smith");
assertThat(paramValue).isInstanceOf(Person.class);
Person person = (Person) paramValue;
assertThat(person.firstName).isEqualTo("John");
assertThat(person.lastName).isEqualTo("Smith");
});
}
@Test
public void mapWhenConfigurationConverterIsNotQualifiedShouldNotConvert() {
assertThatExceptionOfType(ParameterMappingException.class).isThrownBy(() -> {
this.contextRunner
.withUserConfiguration(NonQualifiedConverterConfiguration.class)
.run((context) -> {
ParameterValueMapper parameterValueMapper = context
.getBean(ParameterValueMapper.class);
parameterValueMapper.mapParameterValue(
new TestOperationParameter(Person.class), "John Smith");
});
}).withCauseInstanceOf(ConverterNotFoundException.class);
}
@Test
public void mapShouldUseGenericConfigurationConverter() {
this.contextRunner.withUserConfiguration(GenericConverterConfiguration.class)
.run((context) -> {
ParameterValueMapper parameterValueMapper = context
.getBean(ParameterValueMapper.class);
Object paramValue = parameterValueMapper.mapParameterValue(
new TestOperationParameter(Person.class), "John Smith");
assertThat(paramValue).isInstanceOf(Person.class);
Person person = (Person) paramValue;
assertThat(person.firstName).isEqualTo("John");
assertThat(person.lastName).isEqualTo("Smith");
});
}
@Test
public void mapWhenGenericConfigurationConverterIsNotQualifiedShouldNotConvert() {
assertThatExceptionOfType(ParameterMappingException.class).isThrownBy(() -> {
this.contextRunner
.withUserConfiguration(
NonQualifiedGenericConverterConfiguration.class)
.run((context) -> {
ParameterValueMapper parameterValueMapper = context
.getBean(ParameterValueMapper.class);
parameterValueMapper.mapParameterValue(
new TestOperationParameter(Person.class), "John Smith");
});
}).withCauseInstanceOf(ConverterNotFoundException.class);
}
static class PersonConverter implements Converter<String, Person> {
@Override
public Person convert(String source) {
String[] content = StringUtils.split(source, " ");
return new Person(content[0], content[1]);
}
}
static class GenericPersonConverter implements GenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(String.class, Person.class));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
String[] content = StringUtils.split((String) source, " ");
return new Person(content[0], content[1]);
}
}
@Configuration(proxyBeanMethods = false)
static class ConverterConfiguration {
@Bean
@EndpointConverter
public Converter<String, Person> personConverter() {
return new PersonConverter();
}
}
@Configuration(proxyBeanMethods = false)
static class NonQualifiedConverterConfiguration {
@Bean
public Converter<String, Person> personConverter() {
return new PersonConverter();
}
}
@Configuration(proxyBeanMethods = false)
static class GenericConverterConfiguration {
@Bean
@EndpointConverter
public GenericConverter genericPersonConverter() {
return new GenericPersonConverter();
}
}
@Configuration(proxyBeanMethods = false)
static class NonQualifiedGenericConverterConfiguration {
@Bean
public GenericConverter genericPersonConverter() {
return new GenericPersonConverter();
}
}
static class Person {
private final String firstName;
private final String lastName;
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
}
private static class TestOperationParameter implements OperationParameter {
private final Class<?> type;
TestOperationParameter(Class<?> type) {
this.type = type;
}
@Override
public String getName() {
return "test";
}
@Override
public Class<?> getType() {
return this.type;
}
@Override
public boolean isMandatory() {
return false;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment