Add support for configuring Jackson's ConstructorDetector

Closes gh-27178
This commit is contained in:
Stephane Nicoll 2021-08-17 13:34:12 +02:00
parent 60e57f7a3f
commit 456d741706
5 changed files with 146 additions and 0 deletions

View File

@ -34,6 +34,7 @@ import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.ConstructorDetector;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.springframework.beans.BeanUtils;
@ -41,6 +42,7 @@ import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jackson.JacksonProperties.ConstructorDetectorStrategy;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jackson.JsonComponentModule;
import org.springframework.context.ApplicationContext;
@ -188,6 +190,7 @@ public class JacksonAutoConfiguration {
configureModules(builder);
configureLocale(builder);
configureDefaultLeniency(builder);
configureConstructorDetector(builder);
}
private void configureFeatures(Jackson2ObjectMapperBuilder builder, Map<?, Boolean> features) {
@ -297,6 +300,27 @@ public class JacksonAutoConfiguration {
}
}
private void configureConstructorDetector(Jackson2ObjectMapperBuilder builder) {
ConstructorDetectorStrategy strategy = this.jacksonProperties.getConstructorDetector();
if (strategy != null) {
builder.postConfigurer((objectMapper) -> {
switch (strategy) {
case USE_PROPERTIES_BASED:
objectMapper.setConstructorDetector(ConstructorDetector.USE_PROPERTIES_BASED);
break;
case USE_DELEGATING:
objectMapper.setConstructorDetector(ConstructorDetector.USE_DELEGATING);
break;
case EXPLICIT_ONLY:
objectMapper.setConstructorDetector(ConstructorDetector.EXPLICIT_ONLY);
break;
default:
objectMapper.setConstructorDetector(ConstructorDetector.DEFAULT);
}
});
}
}
private static <T> Collection<T> getBeans(ListableBeanFactory beanFactory, Class<T> type) {
return BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, type).values();
}

View File

@ -97,6 +97,12 @@ public class JacksonProperties {
*/
private Boolean defaultLeniency;
/**
* Strategy to use to to auto-detect constructor, and in particular behavior with
* single-argument constructors.
*/
private ConstructorDetectorStrategy constructorDetector;
/**
* Time zone used when formatting dates. For instance, "America/Los_Angeles" or
* "GMT+10".
@ -164,6 +170,14 @@ public class JacksonProperties {
this.defaultLeniency = defaultLeniency;
}
public ConstructorDetectorStrategy getConstructorDetector() {
return this.constructorDetector;
}
public void setConstructorDetector(ConstructorDetectorStrategy constructorDetector) {
this.constructorDetector = constructorDetector;
}
public TimeZone getTimeZone() {
return this.timeZone;
}
@ -180,4 +194,29 @@ public class JacksonProperties {
this.locale = locale;
}
public enum ConstructorDetectorStrategy {
/**
* Use heuristics to see if "properties" mode is to be used.
*/
DEFAULT,
/**
* Assume "properties" mode if not explicitly annotated otherwise.
*/
USE_PROPERTIES_BASED,
/**
* Assume "delegating" mode if not explicitly annotated otherwise.
*/
USE_DELEGATING,
/**
* Refuse to decide implicit mode and instead throw an InvalidDefinitionException
* for ambiguous cases.
*/
EXPLICIT_ONLY;
}
}

View File

@ -1048,6 +1048,10 @@
"name": "spring.integration.jdbc.initialize-schema",
"defaultValue": "embedded"
},
{
"name": "spring.jackson.constructor-detector",
"defaultValue": "default"
},
{
"name": "spring.jackson.joda-date-time-format",
"type": "java.lang.String",

View File

@ -54,4 +54,25 @@ public class Jackson211AutoConfigurationTests extends JacksonAutoConfigurationTe
});
}
// ConstructorDetector only available as of Jackson 2.12
@Override
void constructorDetectorWithNoStrategyUseDefault() {
}
@Override
void constructorDetectorWithDefaultStrategy() {
}
@Override
void constructorDetectorWithUsePropertiesBasedStrategy() {
}
@Override
void constructorDetectorWithUseDelegatingStrategy() {
}
@Override
void constructorDetectorWithExplicitOnlyStrategy() {
}
}

View File

@ -41,6 +41,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.cfg.ConstructorDetector;
import com.fasterxml.jackson.databind.cfg.ConstructorDetector.SingleArgConstructor;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.util.StdDateFormat;
@ -323,6 +325,62 @@ class JacksonAutoConfigurationTests {
});
}
@Test
void constructorDetectorWithNoStrategyUseDefault() {
this.contextRunner.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.HEURISTIC);
assertThat(cd.requireCtorAnnotation()).isFalse();
assertThat(cd.allowJDKTypeConstructors()).isFalse();
});
}
@Test
void constructorDetectorWithDefaultStrategy() {
this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=default").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.HEURISTIC);
assertThat(cd.requireCtorAnnotation()).isFalse();
assertThat(cd.allowJDKTypeConstructors()).isFalse();
});
}
@Test
void constructorDetectorWithUsePropertiesBasedStrategy() {
this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=use-properties-based")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.PROPERTIES);
assertThat(cd.requireCtorAnnotation()).isFalse();
assertThat(cd.allowJDKTypeConstructors()).isFalse();
});
}
@Test
void constructorDetectorWithUseDelegatingStrategy() {
this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=use-delegating").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.DELEGATING);
assertThat(cd.requireCtorAnnotation()).isFalse();
assertThat(cd.allowJDKTypeConstructors()).isFalse();
});
}
@Test
void constructorDetectorWithExplicitOnlyStrategy() {
this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=explicit-only").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.REQUIRE_MODE);
assertThat(cd.requireCtorAnnotation()).isFalse();
assertThat(cd.allowJDKTypeConstructors()).isFalse();
});
}
@Test
void additionalJacksonBuilderCustomization() {
this.contextRunner.withUserConfiguration(ObjectMapperBuilderCustomConfig.class).run((context) -> {