-
Notifications
You must be signed in to change notification settings - Fork 41.2k
Custom converters cannot be used when creating endpoint-related beans due to eager initialization triggered by ServletEndpointRegistrar #20714
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Thank you for the sample. I have reproduced the problem. The problem is caused by some eager initialization that's triggered by |
The following will reproduce the problem when added to @Test
void endpointThatRequiresCustomConversionForItsConstruction() {
new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(ServletWebServerFactoryAutoConfiguration.class,
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
ServletEndpointManagementContextConfiguration.class, DispatcherServletAutoConfiguration.class))
.withUserConfiguration(CustomConversionEndpoint.class, CustomConversionConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(CustomConversionEndpoint.class);
});
}
@Component
@Endpoint(id = "customconversion")
static class CustomConversionEndpoint {
CustomConversionEndpoint(@Value("custom.type") CustomType customType) {
}
}
@Configuration(proxyBeanMethods = false)
static class CustomConversionConfiguration {
@Bean
public ConversionService conversionService() {
FormattingConversionService conversionService = new DefaultFormattingConversionService();
conversionService.addConverter(new StringToCustomTypeConverter());
return conversionService;
}
}
static class CustomType {
}
static class StringToCustomTypeConverter implements Converter<String, CustomType> {
@Override
public CustomType convert(String source) {
return new CustomType();
}
} The test passes if the creation of |
@wilkinsona Thanks for your analysis. Yes, the We have currently a workaround by manually setting the custom DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
conversionService.addConverter(myCustomConverter);
beanFactory.setConversionService(conversionService); This is working for us but it's is a bit "hacky" and we would be happy to remove this workaround. :-) |
The scope of the problem can be narrowed considerably by changing diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java
index bbdf544f66..b4756df202 100644
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java
+++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java
@@ -16,6 +16,7 @@
package org.springframework.boot.actuate.endpoint.annotation;
+import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@@ -127,7 +128,7 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
private Collection<EndpointBean> createEndpointBeans() {
Map<EndpointId, EndpointBean> byId = new LinkedHashMap<>();
String[] beanNames = BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.applicationContext,
- Endpoint.class);
+ getEndpointAnnotation());
for (String beanName : beanNames) {
if (!ScopedProxyUtils.isScopedTarget(beanName)) {
EndpointBean endpointBean = createEndpointBean(beanName);
@@ -261,6 +262,10 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
return true;
}
+ protected Class<? extends Annotation> getEndpointAnnotation() {
+ return Endpoint.class;
+ }
+
private boolean isEndpointFiltered(EndpointBean endpointBean) {
for (EndpointFilter<E> filter : this.filters) {
if (!isFilterMatch(filter, endpointBean)) {
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscoverer.java
index 5c7dc7bd67..cb1508f57d 100644
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscoverer.java
+++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscoverer.java
@@ -16,6 +16,7 @@
package org.springframework.boot.actuate.endpoint.web.annotation;
+import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -80,4 +81,9 @@ public class ServletEndpointDiscoverer extends EndpointDiscoverer<ExposableServl
throw new IllegalStateException("ServletEndpoints must not declare operations");
}
+ @Override
+ protected Class<? extends Annotation> getEndpointAnnotation() {
+ return ServletEndpoint.class;
+ }
+
} |
This is a nasty one. I've tried approach here where we defer creating the endpoint beans as late as possible. |
Uh oh!
There was an error while loading. Please reload this page.
This is related to the issues #6222 and #12148
It affects Spring Boot version 2.2.x
The behavior of configuration and value binding is still not completly consistent. The use of a custom
Converter
registered as bean or inside aconversionService
bean only works in the following cases:@ConfigurationProperties
@Value
annotation if the bean is created after complete refresh of theApplicationContext
.It does not work, if the configuration value if injected into a bean with
@Value
if the bean is created during "refresh phase" of theApplicationContext
. This applies for example to all beans created during the initialization of the Tomcat server in aServletWebServerApplicationContext
I provided a test project which contains multiple test cases showcasing the inconsistent behavior:
Test scenario 1:
HealthIndicator
bean created during refresh with configuration object => SUCCESSTest scenario 2:
HealthIndicator
bean created during refresh with constructor injection via@Value
=> FAILTest scenario 3:
@Service
bean created after refresh with constructor injection via@Value
=> SUCCESSspring-boot-converter-bug.zip
Please also note that the failure of scenario 2 can only be detected in an integration test if a "real"
webEnvironment
, e.g.RANDOM_PORT
, is used. In a mocked web environment, i.e. without actually starting a Tomcat, the problem will not occur.The text was updated successfully, but these errors were encountered: