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 07c42def213..d32af3609f9 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,23 @@ package org.springframework.boot.actuate.autoconfigure.endpoint; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.EndpointConverter; 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.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 +41,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 +49,24 @@ public class EndpointAutoConfiguration { @Bean @ConditionalOnMissingBean - public ParameterValueMapper endpointOperationParameterMapper() { - return new ConversionServiceParameterValueMapper(); + public ParameterValueMapper endpointOperationParameterMapper( + @EndpointConverter ObjectProvider> converters, + @EndpointConverter ObjectProvider genericConverters) { + ConversionService conversionService = createConversionService( + converters.orderedStream().collect(Collectors.toList()), + genericConverters.orderedStream().collect(Collectors.toList())); + return new ConversionServiceParameterValueMapper(conversionService); + } + + private ConversionService createConversionService(List> converters, + List genericConverters) { + if (genericConverters.isEmpty() && converters.isEmpty()) { + return ApplicationConversionService.getSharedInstance(); + } + ApplicationConversionService conversionService = new ApplicationConversionService(); + converters.forEach(conversionService::addConverter); + genericConverters.forEach(conversionService::addConverter); + return conversionService; } @Bean 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 00000000000..f3c0643fa5b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfigurationTests.java @@ -0,0 +1,205 @@ +/* + * Copyright 2012-2019 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.jupiter.api.Test; + +import org.springframework.boot.actuate.endpoint.annotation.EndpointConverter; +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.ApplicationContextRunner; +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 + */ +class EndpointAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class)); + + @Test + 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 + 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 + 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 + 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 + Converter personConverter() { + return new PersonConverter(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class NonQualifiedConverterConfiguration { + + @Bean + Converter personConverter() { + return new PersonConverter(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericConverterConfiguration { + + @Bean + @EndpointConverter + GenericConverter genericPersonConverter() { + return new GenericPersonConverter(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class NonQualifiedGenericConverterConfiguration { + + @Bean + 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; + } + + } + + 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; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointConverter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointConverter.java new file mode 100644 index 00000000000..790028a7eab --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointConverter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2019 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.endpoint.annotation; + +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; + +/** + * Qualifier for beans that are needed to convert {@link Endpoint} input parameters. + * + * @author Chao Chang + * @since 2.2.0 + */ +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Qualifier +public @interface EndpointConverter { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index 9d7cc9b492c..36ed2f2c3c9 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -558,8 +558,8 @@ automatically if you are using Spring Boot's Gradle plugin or if you are using M The parameters passed to endpoint operation methods are, if necessary, automatically converted to the required type. Before calling an operation method, the input received via JMX or an HTTP request is converted to the required types using an instance of -`ApplicationConversionService`. - +`ApplicationConversionService` as well as any `Converter` or `GenericConverter` beans +qualified with `@EndpointConverter`. [[production-ready-endpoints-custom-web]]