From 2aea437536c63c4b4787661424298aef05590d6a Mon Sep 17 00:00:00 2001 From: "chao.chang" Date: Thu, 4 Apr 2019 13:48:55 +0900 Subject: [PATCH] Make Actuator dedicated ConversionService configurable See gh-16449 --- .../endpoint/EndpointAutoConfiguration.java | 63 ++++- .../endpoint/EndpointConverter.java | 44 ++++ .../EndpointAutoConfigurationTests.java | 230 ++++++++++++++++++ 3 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointConverter.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java index 07c42def21..212f968945 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java @@ -16,14 +16,26 @@ 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.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 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.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; /** @@ -32,6 +44,7 @@ import org.springframework.core.env.Environment; * * @author Phillip Webb * @author Stephane Nicoll + * @author Chao Chang * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) @@ -39,8 +52,10 @@ public class EndpointAutoConfiguration { @Bean @ConditionalOnMissingBean - public ParameterValueMapper endpointOperationParameterMapper() { - return new ConversionServiceParameterValueMapper(); + public ParameterValueMapper endpointOperationParameterMapper( + ApplicationContext applicationContext) { + return new ConversionServiceParameterValueMapper( + new Factory(applicationContext.getAutowireCapableBeanFactory()).create()); } @Bean @@ -49,4 +64,48 @@ public class EndpointAutoConfiguration { return new CachingOperationInvokerAdvisor(new EndpointIdTimeToLivePropertyFunction(environment)); } + private static class Factory { + + @SuppressWarnings("rawtypes") + private final List converters; + + private final List genericConverters; + + Factory(BeanFactory beanFactory) { + this.converters = beans(beanFactory, Converter.class, + EndpointConverter.VALUE); + this.genericConverters = beans(beanFactory, GenericConverter.class, + EndpointConverter.VALUE); + } + + private List beans(BeanFactory beanFactory, Class type, + String qualifier) { + if (beanFactory instanceof ListableBeanFactory) { + return beans(type, qualifier, (ListableBeanFactory) beanFactory); + } + return Collections.emptyList(); + } + + private List beans(Class 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; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointConverter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointConverter.java new file mode 100644 index 0000000000..29a5226b69 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointConverter.java @@ -0,0 +1,44 @@ +/* + * 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"; + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfigurationTests.java new file mode 100644 index 0000000000..cba2d1d5a8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfigurationTests.java @@ -0,0 +1,230 @@ +/* + * 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 { + + @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 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 personConverter() { + return new PersonConverter(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class NonQualifiedConverterConfiguration { + + @Bean + public Converter 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; + } + + } + +}