diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index fabb226604..03bad1cb07 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -69,6 +69,7 @@ dependencies { } testCompile("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${jackson2Version}") testCompile("com.fasterxml.jackson.datatype:jackson-datatype-joda:${jackson2Version}") + testCompile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jackson2Version}") testCompile("com.fasterxml.jackson.module:jackson-module-kotlin:${jackson2Version}") testCompile("org.apache.tomcat:tomcat-util:${tomcatVersion}") testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}") diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index c18cb5fcc6..7dc83aab8f 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -632,21 +632,24 @@ public class Jackson2ObjectMapperBuilder { public void configure(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); + Map modulesToRegister = new LinkedHashMap<>(); if (this.findModulesViaServiceLoader) { - objectMapper.registerModules(ObjectMapper.findModules(this.moduleClassLoader)); + ObjectMapper.findModules(this.moduleClassLoader).forEach(module -> modulesToRegister.put(module.getTypeId(), module)); } else if (this.findWellKnownModules) { - registerWellKnownModulesIfAvailable(objectMapper); + registerWellKnownModulesIfAvailable(modulesToRegister); } if (this.modules != null) { - objectMapper.registerModules(this.modules); + this.modules.forEach(module -> modulesToRegister.put(module.getTypeId(), module)); } if (this.moduleClasses != null) { - for (Class module : this.moduleClasses) { - objectMapper.registerModule(BeanUtils.instantiateClass(module)); + for (Class moduleClass : this.moduleClasses) { + Module module = BeanUtils.instantiateClass(moduleClass); + modulesToRegister.put(module.getTypeId(), module); } } + objectMapper.registerModules(modulesToRegister.values()); if (this.dateFormat != null) { objectMapper.setDateFormat(this.dateFormat); @@ -744,20 +747,22 @@ public class Jackson2ObjectMapperBuilder { } @SuppressWarnings("unchecked") - private void registerWellKnownModulesIfAvailable(ObjectMapper objectMapper) { + private void registerWellKnownModulesIfAvailable(Map modulesToRegister) { try { - Class jdk8Module = (Class) + Class jdk8ModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiateClass(jdk8Module)); + Module jdk8Module = BeanUtils.instantiateClass(jdk8ModuleClass); + modulesToRegister.put(jdk8Module.getTypeId(), jdk8Module); } catch (ClassNotFoundException ex) { // jackson-datatype-jdk8 not available } try { - Class javaTimeModule = (Class) + Class javaTimeModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiateClass(javaTimeModule)); + Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass); + modulesToRegister.put(javaTimeModule.getTypeId(), javaTimeModule); } catch (ClassNotFoundException ex) { // jackson-datatype-jsr310 not available @@ -766,9 +771,10 @@ public class Jackson2ObjectMapperBuilder { // Joda-Time present? if (ClassUtils.isPresent("org.joda.time.LocalDate", this.moduleClassLoader)) { try { - Class jodaModule = (Class) + Class jodaModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.datatype.joda.JodaModule", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiateClass(jodaModule)); + Module jodaModule = BeanUtils.instantiateClass(jodaModuleClass); + modulesToRegister.put(jodaModule.getTypeId(), jodaModule); } catch (ClassNotFoundException ex) { // jackson-datatype-joda not available @@ -778,9 +784,10 @@ public class Jackson2ObjectMapperBuilder { // Kotlin present? if (KotlinDetector.isKotlinPresent()) { try { - Class kotlinModule = (Class) + Class kotlinModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiateClass(kotlinModule)); + Module kotlinModule = BeanUtils.instantiateClass(kotlinModuleClass); + modulesToRegister.put(kotlinModule.getTypeId(), kotlinModule); } catch (ClassNotFoundException ex) { if (!kotlinWarningLogged) { diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java index 2452c8ac27..a0db5be18c 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -21,6 +21,8 @@ import java.io.UnsupportedEncodingException; import java.nio.file.Path; import java.nio.file.Paths; import java.text.SimpleDateFormat; +import java.time.OffsetDateTime; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -40,6 +42,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonMappingException; @@ -50,6 +53,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.cfg.DeserializerFactoryConfig; import com.fasterxml.jackson.databind.cfg.SerializerFactoryConfig; import com.fasterxml.jackson.databind.deser.BasicDeserializerFactory; @@ -68,12 +72,14 @@ import com.fasterxml.jackson.databind.type.SimpleType; import com.fasterxml.jackson.dataformat.cbor.CBORFactory; import com.fasterxml.jackson.dataformat.smile.SmileFactory; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import kotlin.ranges.IntRange; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.junit.Test; import org.springframework.beans.FatalBeanException; +import org.springframework.util.StringUtils; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; @@ -96,6 +102,8 @@ public class Jackson2ObjectMapperBuilderTests { private static final String DATE_FORMAT = "yyyy-MM-dd"; + private static final String DATA = "{\"offsetDateTime\": \"2020-01-01T00:00:00\"}"; + @Test(expected = FatalBeanException.class) public void unknownFeature() { @@ -308,6 +316,18 @@ public class Jackson2ObjectMapperBuilderTests { assertThat(new String(objectMapper.writeValueAsBytes(new Integer(4)), "UTF-8"), containsString("customid")); } + @Test // gh-22576 + public void overrideWellKnownModuleWithModule() throws IOException { + Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addDeserializer(OffsetDateTime.class, new OffsetDateTimeDeserializer()); + builder.modulesToInstall(javaTimeModule); + builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + ObjectMapper objectMapper = builder.build(); + DemoPojo demoPojo = objectMapper.readValue(DATA, DemoPojo.class); + assertNotNull(demoPojo.getOffsetDateTime()); + } + private static SerializerFactoryConfig getSerializerFactoryConfig(ObjectMapper objectMapper) { return ((BasicSerializerFactory) objectMapper.getSerializerFactory()).getFactoryConfig(); @@ -613,4 +633,38 @@ public class Jackson2ObjectMapperBuilderTests { } + static class OffsetDateTimeDeserializer extends JsonDeserializer { + + private static final String CURRENT_ZONE_OFFSET = OffsetDateTime.now().getOffset().toString(); + + @Override + public OffsetDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + final String value = jsonParser.getValueAsString(); + if (StringUtils.isEmpty(value)) { + return null; + } + try { + return OffsetDateTime.parse(value); + + } catch (DateTimeParseException exception) { + return OffsetDateTime.parse(value + CURRENT_ZONE_OFFSET); + } + } + } + + @JsonDeserialize + static class DemoPojo { + + private OffsetDateTime offsetDateTime; + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public void setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + } + + } + }