Updates Jackson usage to use immutable ObjectReader/Writer instead of ObjectMapper

Jackson 2.10+ recommend using `ObjectReader` and `ObjectWriter` as opposed to `ObjectMapper`
https://cowtowncoder.medium.com/jackson-3-0-immutability-w-builders-d9c532860d88
This commit is contained in:
Brian Demers 2021-07-12 15:45:30 -04:00
parent 9007ae7c98
commit 0c55bfce67
No known key found for this signature in database
GPG Key ID: 78A3F0D70AE5308C
6 changed files with 29 additions and 21 deletions

View File

@ -5,7 +5,8 @@
This patch release: This patch release:
* Adds additional handling for rare JSON parsing exceptions and wraps them in a `JwtException` to allow the application to handle these conditions as JWT concerns. * Adds additional handling for rare JSON parsing exceptions and wraps them in a `JwtException` to allow the application to handle these conditions as JWT concerns.
* Upgrades the `jjwt-jackson` module's Jackson dependency to `2.9.10.7`. * Upgrades the `jjwt-jackson` module's Jackson dependency to `2.12.4`.
* Updates Jackson usage (in `jjwt-jackson`) to use immutable classes instead of using `ObjectMapper` directly.
### 0.11.2 ### 0.11.2

View File

@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer; import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.module.SimpleModule;
import io.jsonwebtoken.io.DeserializationException; import io.jsonwebtoken.io.DeserializationException;
@ -35,7 +36,7 @@ import java.util.Map;
public class JacksonDeserializer<T> implements Deserializer<T> { public class JacksonDeserializer<T> implements Deserializer<T> {
private final Class<T> returnType; private final Class<T> returnType;
private final ObjectMapper objectMapper; private final ObjectReader objectReader;
@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator @SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
public JacksonDeserializer() { public JacksonDeserializer() {
@ -68,14 +69,7 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
* @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type * @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type
*/ */
public JacksonDeserializer(Map<String, Class> claimTypeMap) { public JacksonDeserializer(Map<String, Class> claimTypeMap) {
// DO NOT reuse JacksonSerializer.DEFAULT_OBJECT_MAPPER as this could result in sharing the custom deserializer this(objectMapperWithMappedTypes(claimTypeMap));
// between instances
this(new ObjectMapper());
Assert.notNull(claimTypeMap, "Claim type map cannot be null.");
// register a new Deserializer
SimpleModule module = new SimpleModule();
module.addDeserializer(Object.class, new MappedTypeDeserializer(Collections.unmodifiableMap(claimTypeMap)));
objectMapper.registerModule(module);
} }
@SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom ObjectMapper @SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom ObjectMapper
@ -86,7 +80,7 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
private JacksonDeserializer(ObjectMapper objectMapper, Class<T> returnType) { private JacksonDeserializer(ObjectMapper objectMapper, Class<T> returnType) {
Assert.notNull(objectMapper, "ObjectMapper cannot be null."); Assert.notNull(objectMapper, "ObjectMapper cannot be null.");
Assert.notNull(returnType, "Return type cannot be null."); Assert.notNull(returnType, "Return type cannot be null.");
this.objectMapper = objectMapper; this.objectReader = objectMapper.reader();
this.returnType = returnType; this.returnType = returnType;
} }
@ -101,7 +95,19 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
} }
protected T readValue(byte[] bytes) throws IOException { protected T readValue(byte[] bytes) throws IOException {
return objectMapper.readValue(bytes, returnType); return objectReader.readValue(bytes, returnType);
}
private static ObjectMapper objectMapperWithMappedTypes(Map<String, Class> claimTypeMap) {
// DO NOT reuse JacksonSerializer.DEFAULT_OBJECT_MAPPER as this could result in sharing the custom deserializer
// between instances
Assert.notNull(claimTypeMap, "Claim type map cannot be null.");
ObjectMapper objectMapper = new ObjectMapper();
// register a new Deserializer
SimpleModule module = new SimpleModule();
module.addDeserializer(Object.class, new MappedTypeDeserializer(Collections.unmodifiableMap(claimTypeMap)));
objectMapper.registerModule(module);
return objectMapper;
} }
/** /**
@ -122,7 +128,7 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
// check if the current claim key is mapped, if so traverse it's value // check if the current claim key is mapped, if so traverse it's value
String name = parser.currentName(); String name = parser.currentName();
if (claimTypeMap != null && name != null && claimTypeMap.containsKey(name)) { if (claimTypeMap != null && name != null && claimTypeMap.containsKey(name)) {
Class type = claimTypeMap.get(name); Class<?> type = claimTypeMap.get(name);
return parser.readValueAsTree().traverse(parser.getCodec()).readValueAs(type); return parser.readValueAsTree().traverse(parser.getCodec()).readValueAs(type);
} }
// otherwise default to super // otherwise default to super

View File

@ -17,6 +17,7 @@ package io.jsonwebtoken.jackson.io;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import io.jsonwebtoken.io.SerializationException; import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer; import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
@ -28,7 +29,7 @@ public class JacksonSerializer<T> implements Serializer<T> {
static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper(); static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper();
private final ObjectMapper objectMapper; private final ObjectWriter objectWriter;
@SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator @SuppressWarnings("unused") //used via reflection by RuntimeClasspathDeserializerLocator
public JacksonSerializer() { public JacksonSerializer() {
@ -38,7 +39,7 @@ public class JacksonSerializer<T> implements Serializer<T> {
@SuppressWarnings("WeakerAccess") //intended for end-users to use when providing a custom ObjectMapper @SuppressWarnings("WeakerAccess") //intended for end-users to use when providing a custom ObjectMapper
public JacksonSerializer(ObjectMapper objectMapper) { public JacksonSerializer(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper cannot be null."); Assert.notNull(objectMapper, "ObjectMapper cannot be null.");
this.objectMapper = objectMapper; this.objectWriter = objectMapper.writer();
} }
@Override @Override
@ -54,6 +55,6 @@ public class JacksonSerializer<T> implements Serializer<T> {
@SuppressWarnings("WeakerAccess") //for testing @SuppressWarnings("WeakerAccess") //for testing
protected byte[] writeValueAsBytes(T t) throws JsonProcessingException { protected byte[] writeValueAsBytes(T t) throws JsonProcessingException {
return this.objectMapper.writeValueAsBytes(t); return this.objectWriter.writeValueAsBytes(t);
} }
} }

View File

@ -38,14 +38,14 @@ class JacksonDeserializerTest {
@Test @Test
void testDefaultConstructor() { void testDefaultConstructor() {
def deserializer = new JacksonDeserializer() def deserializer = new JacksonDeserializer()
assertNotNull deserializer.objectMapper assertNotNull deserializer.objectReader
} }
@Test @Test
void testObjectMapperConstructor() { void testObjectMapperConstructor() {
def customOM = new ObjectMapper() def customOM = new ObjectMapper()
def deserializer = new JacksonDeserializer(customOM) def deserializer = new JacksonDeserializer(customOM)
assertSame customOM, deserializer.objectMapper assertSame customOM.getDeserializationConfig(), deserializer.objectReader.config
} }
@Test(expected = IllegalArgumentException) @Test(expected = IllegalArgumentException)

View File

@ -37,14 +37,14 @@ class JacksonSerializerTest {
@Test @Test
void testDefaultConstructor() { void testDefaultConstructor() {
def serializer = new JacksonSerializer() def serializer = new JacksonSerializer()
assertNotNull serializer.objectMapper assertNotNull serializer.objectWriter
} }
@Test @Test
void testObjectMapperConstructor() { void testObjectMapperConstructor() {
def customOM = new ObjectMapper() def customOM = new ObjectMapper()
def serializer = new JacksonSerializer<>(customOM) def serializer = new JacksonSerializer<>(customOM)
assertSame customOM, serializer.objectMapper assertSame customOM.getSerializationConfig(), serializer.objectWriter.config
} }
@Test(expected = IllegalArgumentException) @Test(expected = IllegalArgumentException)

View File

@ -74,7 +74,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<buildNumber>${user.name}-${maven.build.timestamp}</buildNumber> <buildNumber>${user.name}-${maven.build.timestamp}</buildNumber>
<jackson.version>2.9.10.7</jackson.version> <jackson.version>2.12.4</jackson.version>
<orgjson.version>20180130</orgjson.version> <orgjson.version>20180130</orgjson.version>
<gson.version>2.8.5</gson.version> <gson.version>2.8.5</gson.version>