Skip to content

Commit 20bbc94

Browse files
Singletone ObjectMapper (#71)
* Use a Singleton for the ObjectMapper * cleanup up and add JavaDoc * add test * final class for the singleton
1 parent 754838d commit 20bbc94

File tree

4 files changed

+81
-31
lines changed

4 files changed

+81
-31
lines changed

src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package dev.toonformat.jtoon.decoder;
22

3-
import com.fasterxml.jackson.annotation.JsonInclude;
43
import dev.toonformat.jtoon.DecodeOptions;
4+
import dev.toonformat.jtoon.util.ObjectMapperSingleton;
55
import tools.jackson.databind.ObjectMapper;
6-
import tools.jackson.databind.json.JsonMapper;
7-
import tools.jackson.module.afterburner.AfterburnerModule;
86

97
import java.util.LinkedHashMap;
10-
import java.util.TimeZone;
118
import java.util.regex.Matcher;
129

1310
import static dev.toonformat.jtoon.util.Headers.KEYED_ARRAY_PATTERN;
@@ -33,17 +30,7 @@
3330
*/
3431
public final class ValueDecoder {
3532

36-
private static final ObjectMapper OBJECT_MAPPER;
37-
38-
static {
39-
OBJECT_MAPPER = JsonMapper.builder()
40-
.changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.ALWAYS))
41-
.addModule(new AfterburnerModule().setUseValueClassLoader(true)) // Speeds up Jackson by 20–40% in most real-world cases
42-
// .disable(MapperFeature.DEFAULT_VIEW_INCLUSION) in Jackson 3 this is default disabled
43-
// .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) in Jackson 3 this is default disabled
44-
.defaultTimeZone(TimeZone.getTimeZone("UTC")) // set a default timezone for dates
45-
.build();
46-
}
33+
private static final ObjectMapper MAPPER = ObjectMapperSingleton.getInstance();
4734

4835
private ValueDecoder() {
4936
throw new UnsupportedOperationException("Utility class cannot be instantiated");
@@ -132,7 +119,7 @@ public static Object decode(String toon, DecodeOptions options) {
132119
public static String decodeToJson(String toon, DecodeOptions options) {
133120
try {
134121
Object decoded = decode(toon, options);
135-
return OBJECT_MAPPER.writeValueAsString(decoded);
122+
return MAPPER.writeValueAsString(decoded);
136123
} catch (Exception e) {
137124
throw new IllegalArgumentException("Failed to convert decoded value to JSON: " + e.getMessage(), e);
138125
}

src/main/java/dev/toonformat/jtoon/normalizer/JsonNormalizer.java

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package dev.toonformat.jtoon.normalizer;
22

3-
import com.fasterxml.jackson.annotation.JsonInclude;
3+
import dev.toonformat.jtoon.util.ObjectMapperSingleton;
44
import tools.jackson.databind.JsonNode;
55
import tools.jackson.databind.ObjectMapper;
6-
import tools.jackson.databind.json.JsonMapper;
76
import tools.jackson.databind.node.ArrayNode;
87
import tools.jackson.databind.node.BooleanNode;
98
import tools.jackson.databind.node.DecimalNode;
@@ -15,7 +14,6 @@
1514
import tools.jackson.databind.node.ObjectNode;
1615
import tools.jackson.databind.node.ShortNode;
1716
import tools.jackson.databind.node.StringNode;
18-
import tools.jackson.module.afterburner.AfterburnerModule;
1917

2018
import java.math.BigDecimal;
2119
import java.math.BigInteger;
@@ -33,7 +31,6 @@
3331
import java.util.Map;
3432
import java.util.Objects;
3533
import java.util.Optional;
36-
import java.util.TimeZone;
3734
import java.util.function.Function;
3835
import java.util.function.IntFunction;
3936
import java.util.stream.Stream;
@@ -43,20 +40,11 @@
4340
* Handles Java-specific types like LocalDateTime, Optional, Stream, etc.
4441
*/
4542
public final class JsonNormalizer {
43+
4644
/**
4745
* Shared ObjectMapper instance configured for JSON normalization.
4846
*/
49-
public static final ObjectMapper MAPPER;
50-
51-
static {
52-
MAPPER = JsonMapper.builder()
53-
.changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.ALWAYS))
54-
.addModule(new AfterburnerModule().setUseValueClassLoader(true)) // Speeds up Jackson by 20–40% in most real-world cases
55-
// .disable(MapperFeature.DEFAULT_VIEW_INCLUSION) in Jackson 3 this is default disabled
56-
// .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) in Jackson 3 this is default disabled
57-
.defaultTimeZone(TimeZone.getTimeZone("UTC")) // set a default timezone for dates
58-
.build();
59-
}
47+
public static final ObjectMapper MAPPER = ObjectMapperSingleton.getInstance();
6048

6149
private static final List<Function<Object, JsonNode>> NORMALIZERS = List.of(
6250
JsonNormalizer::tryNormalizePrimitive,
@@ -69,6 +57,7 @@ private JsonNormalizer() {
6957
throw new UnsupportedOperationException("Utility class cannot be instantiated");
7058
}
7159

60+
7261
/**
7362
* Parses a JSON string into a JsonNode using the shared ObjectMapper.
7463
* <p>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package dev.toonformat.jtoon.util;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import tools.jackson.databind.ObjectMapper;
5+
import tools.jackson.databind.json.JsonMapper;
6+
import tools.jackson.module.afterburner.AfterburnerModule;
7+
8+
import java.util.TimeZone;
9+
10+
/**
11+
* Provides a singleton ObjectMapper instance.
12+
*/
13+
public final class ObjectMapperSingleton {
14+
/**
15+
* Holds the singleton ObjectMapper.
16+
*/
17+
private static ObjectMapper INSTANCE;
18+
19+
private ObjectMapperSingleton() {
20+
throw new UnsupportedOperationException("Utility class cannot be instantiated");
21+
}
22+
23+
/**
24+
* Returns the singleton ObjectMapper.
25+
*
26+
* @return ObjectMapper
27+
*/
28+
public static ObjectMapper getInstance() {
29+
if (INSTANCE == null) {
30+
INSTANCE = JsonMapper.builder()
31+
.changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.ALWAYS))
32+
.addModule(new AfterburnerModule()) // Speeds up Jackson by 20–40% in most real-world cases
33+
// .disable(MapperFeature.DEFAULT_VIEW_INCLUSION) in Jackson 3 this is default disabled
34+
// .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) in Jackson 3 this is default disabled
35+
// .configure(SerializationFeature.INDENT_OUTPUT, false) in Jackson 3 this is default false
36+
.defaultTimeZone(TimeZone.getTimeZone("UTC")) // set a default timezone for dates
37+
.build();
38+
}
39+
return INSTANCE;
40+
}
41+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package dev.toonformat.jtoon.util;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Tag;
5+
import org.junit.jupiter.api.Test;
6+
7+
import java.lang.reflect.Constructor;
8+
import java.lang.reflect.InvocationTargetException;
9+
10+
import static org.junit.jupiter.api.Assertions.assertEquals;
11+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
12+
import static org.junit.jupiter.api.Assertions.assertThrows;
13+
14+
/**
15+
* JUnit 5 test class for ObjectMapperSingleton.
16+
*/
17+
@Tag("unit")
18+
class ObjectMapperSingletonTest {
19+
20+
@Test
21+
@DisplayName("throws unsupported Operation Exception for calling the constructor")
22+
void throwsOnConstructor() throws NoSuchMethodException {
23+
final Constructor<ObjectMapperSingleton> constructor = ObjectMapperSingleton.class.getDeclaredConstructor();
24+
constructor.setAccessible(true);
25+
26+
final InvocationTargetException thrown =
27+
assertThrows(InvocationTargetException.class, constructor::newInstance);
28+
29+
final Throwable cause = thrown.getCause();
30+
assertInstanceOf(UnsupportedOperationException.class, cause);
31+
assertEquals("Utility class cannot be instantiated", cause.getMessage());
32+
}
33+
}

0 commit comments

Comments
 (0)