I am upgrading our application from Spring Boot 2.7.10 to 3.0.5
I run it from IDE on embedded Tomcat and external Tomcat (version 9 for Spring Boot 2.7.x) as well. Now I use Tomcat 10.1.7 and Jackson 2.14.2
It still runs on embedded Tomcat, but not on external Tomcat 10. I get:
Failed to bind properties under 'my-configuration.object-mapper.serializer-provider.generator.write-capabilities' to com.fasterxml.jackson.core.util.JacksonFeatureSet<com.fasterxml.jackson.core.StreamWriteCapability>:
Reason: org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'my-configuration.object-mapper.serializer-provider.generator.write-capabilities' to com.fasterxml.jackson.core.util.JacksonFeatureSet<com.fasterxml.jackson.core.StreamWriteCapability>
Using
@ConfigurationProperties(prefix = "my-configuration")
public class MyConfiguration {
private ObjectMapper objectMapper;
}
I am able to suppress the error by specifying `ignoreInvalidFields```, but to sure whether it is the correct solution.
@ConfigurationProperties(prefix = "my-configuration", ignoreInvalidFields = true)
It is no doubt that I have to refactor our legacy configuration classes but still, I would like to understand what is going on and why is failing since Spring Boot 3 and external Tomcat only.
I expect that if it is completely wrong, it should fail on embedded Tomcat as well.
The minimal project is attached. If you enable the maven profile embedded it runs on the embedded Tomcat. If you deploy it on external Tomcat 10.1.7, it fails.
Stacktrace
Caused by: org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'my-configuration.object-mapper.serializer-provider.generator.write-capabilities' to com.fasterxml.jackson.core.util.JacksonFeatureSet<com.fasterxml.jackson.core.StreamWriteCapability>
at org.springframework.boot.context.properties.bind.Binder.handleBindError(Binder.java:387)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:347)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$4(Binder.java:472)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:98)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:86)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:62)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:476)
at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:590)
at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:576)
at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:474)
at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:414)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$4(Binder.java:472)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:98)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:86)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:62)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:476)
at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:590)
at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:576)
at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:474)
at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:414)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$4(Binder.java:472)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:98)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:86)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:62)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:476)
at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:590)
at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:576)
at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:474)
at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:414)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$4(Binder.java:472)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:98)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:86)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:62)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:476)
at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:590)
at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:576)
at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:474)
at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:414)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$4(Binder.java:472)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:98)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:86)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:62)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:476)
at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:590)
at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:576)
at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:474)
at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:414)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:332)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:262)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:249)
at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bind(ConfigurationPropertiesBinder.java:93)
at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:96)
... 121 more
Caused by: java.lang.IllegalStateException: Unable to get value for property write-capabilities
at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty.lambda$getValue$0(JavaBeanBinder.java:360)
at org.springframework.boot.context.properties.bind.DefaultBindConstructorProvider.getBindConstructor(DefaultBindConstructorProvider.java:46)
at org.springframework.boot.context.properties.bind.ValueObjectBinder$ValueObject.get(ValueObjectBinder.java:190)
at org.springframework.boot.context.properties.bind.ValueObjectBinder.bind(ValueObjectBinder.java:67)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindDataObject$5(Binder.java:476)
at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:590)
at org.springframework.boot.context.properties.bind.Binder$Context.withDataObject(Binder.java:576)
at org.springframework.boot.context.properties.bind.Binder.bindDataObject(Binder.java:474)
at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:414)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:343)
... 176 more
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.fasterxml.jackson.core.JsonGenerator]: Is it an abstract class?
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:215)
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:153)
at org.springframework.boot.context.properties.bind.JavaBeanBinder$Bean.lambda$getSupplier$0(JavaBeanBinder.java:217)
at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanSupplier.get(JavaBeanBinder.java:279)
at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty.lambda$getValue$0(JavaBeanBinder.java:357)
... 185 more
Caused by: java.lang.InstantiationException
at java.base/jdk.internal.reflect.InstantiationExceptionConstructorAccessorImpl.newInstance(InstantiationExceptionConstructorAccessorImpl.java:48)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:198)
... 189 more
Comment From: wilkinsona
ObjectMapper isn't really a suitable target for configuration property binding. This becomes evident when you deploy to Tomcat as it has JNDI enabled. JNDI affects property binding as it's used as a PropertySource. JNDI doesn't allow us to enumerate all of the properties that it contains so, instead of finding all the properties and binding them to the targets, we instead have to ask all of the targets what properties they have and then try to bind them. This is when ObjectMapper causes a problem.
You can reproduce the problem with embedded Tomcat by enabling JNDI:
package com.example.demo;
import org.apache.catalina.startup.Tomcat;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableConfigurationProperties(MyConfiguration.class)
public class DemoApplication {
public static void main(String[] args) {
System.setProperty("java.naming.factory.initial", "org.apache.naming.java.javaURLContextFactory");
SpringApplication.run(DemoApplication.class, args);
}
@Bean
TomcatServletWebServerFactory tomcat() {
return new TomcatServletWebServerFactory() {
@Override
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
tomcat.enableNaming();
return super.getTomcatWebServer(tomcat);
}
};
}
}
I would recommend that you resolve the problem by not binding configuration properties directly to ObjectMapper. We don't do so in Boot, binding instead to org.springframework.boot.autoconfigure.jackson.JacksonProperties and then applying those to a builder that then creates the ObjectMapper. Alternatively, if you want to continue trying to bind ObjectMapper directly, you could disable Framework's JNDI support which will take JndiPropertySource out of the picture. To do so, either set the spring.jndi.ignore system property to false or add a spring.properties file to src/main/resources that contains spring.jndi.ignore=false.