From ace471b80d927c20b4d145cd586d6cf2a973dc76 Mon Sep 17 00:00:00 2001 From: Taylor Jones Date: Fri, 25 Nov 2016 15:08:40 -0500 Subject: [PATCH] Moving memoized code to separate class --- json-view/pom.xml | 16 +++- .../monitorjbl/json/JsonViewSerializer.java | 84 ++++------------ .../main/java/com/monitorjbl/json/Match.java | 29 +++++- .../java/com/monitorjbl/json/Memoizer.java | 95 +++++++++++++++++++ .../JsonViewSerializerPerformanceTest.java | 64 +++++++------ .../java/com/monitorjbl/json/WriterTest.java | 12 ++- json-view/src/test/resources/log4j.properties | 8 ++ spring-json-view/pom.xml | 4 +- .../json/JsonViewMessageConverter.java | 40 ++++---- .../json/JsonViewSupportFactoryBean.java | 12 +-- 10 files changed, 227 insertions(+), 137 deletions(-) create mode 100644 json-view/src/main/java/com/monitorjbl/json/Memoizer.java create mode 100644 json-view/src/test/resources/log4j.properties diff --git a/json-view/pom.xml b/json-view/pom.xml index 734c8cb..d44f21d 100644 --- a/json-view/pom.xml +++ b/json-view/pom.xml @@ -41,8 +41,8 @@ maven-compiler-plugin 3.2 - 1.7 - 1.7 + 1.8 + 1.8 @@ -167,5 +167,17 @@ 1.6.2 test + + org.slf4j + slf4j-api + 1.7.21 + test + + + org.slf4j + slf4j-log4j12 + 1.7.21 + test + diff --git a/json-view/src/main/java/com/monitorjbl/json/JsonViewSerializer.java b/json-view/src/main/java/com/monitorjbl/json/JsonViewSerializer.java index d721318..e6bd2a3 100644 --- a/json-view/src/main/java/com/monitorjbl/json/JsonViewSerializer.java +++ b/json-view/src/main/java/com/monitorjbl/json/JsonViewSerializer.java @@ -32,13 +32,9 @@ public class JsonViewSerializer extends JsonSerializer { - private final int maxCacheSize; - private final Map[]> interfaceCache = new HashMap<>(); - private final Map classAnnotationCache = new HashMap<>(); - private final Map classFieldsCache = new HashMap<>(); - private final Map fieldAnnotationCache = new HashMap<>(); - private final Map, Map>> matchingCache = new HashMap<>(); - private final Map annotatedWithIgnoreCache = new HashMap<>(); + private final Memoizer memoizer; + // private final Map, Map>> matchingCache = new HashMap<>(); +// private final Map annotatedWithIgnoreCache = new HashMap<>(); /** * Map of custom serializers to take into account when serializing fields. @@ -50,7 +46,7 @@ public JsonViewSerializer() { } public JsonViewSerializer(int maxCacheSize) { - this.maxCacheSize = maxCacheSize; + this.memoizer = new Memoizer(maxCacheSize); } /** @@ -89,7 +85,7 @@ public void registerCustomSerializer(Class cls, JsonSerializer forType * * @param cls The class type the serializer was registered for */ - public void unregisterCustomSerializer(Class cls) { + public void unregisterCustomSerializer(Class cls) { if(customSerializersMap != null) { customSerializersMap.remove(cls); } @@ -447,17 +443,9 @@ E readClassAnnotation(Class cls, Class annotationType, String methodName) { * *

* This method is memoized to speed up execution time - * - * @param values - * @param pattern - * @return */ - int containsMatchingPattern(List values, String pattern, boolean matchPrefix) { - Map> l1 = matchingCache.get(values); - Map l2 = l1 == null ? null : l1.get(pattern); - if(l1 != null && l2 != null) { - return l2.get(matchPrefix); - } else { + int containsMatchingPattern(Set values, String pattern, boolean matchPrefix) { + return memoizer.matches(values, pattern, matchPrefix, () -> { int match = -1; for(String val : values) { String replaced = val.replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*"); @@ -466,31 +454,16 @@ int containsMatchingPattern(List values, String pattern, boolean matchPr break; } } - - //save result to avoid having to do an expensive regex check every time - synchronized (matchingCache) { - Map> first = matchingCache.containsKey(values) ? matchingCache.get(values) : new HashMap>(); - Map second = first.containsKey(pattern) ? first.get(pattern) : new HashMap(); - - second.put(matchPrefix, match); - first.put(pattern, second); - matchingCache.put(values, first); - } - return match; - } + }); } /** * Returns a boolean indicating whether the provided field is annotated with * some form of ignore. This method is memoized to speed up execution time - * - * @param f - * @return */ boolean annotatedWithIgnore(Field f) { - boolean annotated; - if(!annotatedWithIgnoreCache.containsKey(f)) { + return memoizer.ignoreAnnotations(f, () -> { JsonIgnore jsonIgnore = getAnnotation(f, JsonIgnore.class); JsonIgnoreProperties classIgnoreProperties = getAnnotation(f.getDeclaringClass(), JsonIgnoreProperties.class); JsonIgnoreProperties fieldIgnoreProperties = null; @@ -512,52 +485,27 @@ boolean annotatedWithIgnore(Field f) { } } - annotated = (jsonIgnore != null && jsonIgnore.value()) || + return (jsonIgnore != null && jsonIgnore.value()) || (classIgnoreProperties != null && Arrays.asList(classIgnoreProperties.value()).contains(f.getName())) || (fieldIgnoreProperties != null && Arrays.asList(fieldIgnoreProperties.value()).contains(f.getName())) || backReferenced; - fitToMaxSize(annotatedWithIgnoreCache).put(f, annotated); - } else { - annotated = annotatedWithIgnoreCache.get(f); - } - - return annotated; + }); } private Class[] getInterfaces(Class cls) { - Class[] interfaces = interfaceCache.get(cls); - if(interfaces == null) { - interfaces = cls.getInterfaces(); - fitToMaxSize(interfaceCache).put(cls, interfaces); - } - return interfaces; + return cls.getInterfaces(); } private Field[] getDeclaredFields(Class cls) { - Field[] fields = classFieldsCache.get(cls); - if(fields == null) { - fields = cls.getDeclaredFields(); - fitToMaxSize(classFieldsCache).put(cls, fields); - } - return fields; + return cls.getDeclaredFields(); } private Annotation[] getAnnotations(Class cls) { - Annotation[] annotations = classAnnotationCache.get(cls); - if(annotations == null) { - annotations = cls.getAnnotations(); - fitToMaxSize(classAnnotationCache).put(cls, annotations); - } - return annotations; + return cls.getAnnotations(); } private Annotation[] getAnnotations(Field field) { - Annotation[] annotations = fieldAnnotationCache.get(field); - if(annotations == null) { - annotations = field.getAnnotations(); - fitToMaxSize(fieldAnnotationCache).put(field, annotations); - } - return annotations; + return field.getAnnotations(); } @SuppressWarnings("unchecked") @@ -589,7 +537,7 @@ private T getAnnotation(Field field, Class annotation) //synchronizes on the provided map to provide threadsafety private Map fitToMaxSize(Map map) { synchronized (map) { - if(map.size() > maxCacheSize) { + if(map.size() > 1) { map.remove(map.keySet().iterator().next()); } } diff --git a/json-view/src/main/java/com/monitorjbl/json/Match.java b/json-view/src/main/java/com/monitorjbl/json/Match.java index ab4def4..0c1b41f 100644 --- a/json-view/src/main/java/com/monitorjbl/json/Match.java +++ b/json-view/src/main/java/com/monitorjbl/json/Match.java @@ -2,11 +2,13 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; public class Match { - private final List includes = new ArrayList<>(); - private final List excludes = new ArrayList<>(); + private final Set includes = new HashSet<>(); + private final Set excludes = new HashSet<>(); Match() { @@ -26,11 +28,11 @@ public Match exclude(String... fields) { return this; } - List getIncludes() { + Set getIncludes() { return includes; } - List getExcludes() { + Set getExcludes() { return excludes; } @@ -45,4 +47,23 @@ public String toString() { ", excludes=" + excludes + '}'; } + + @Override + public boolean equals(Object o) { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + + Match match = (Match) o; + + if(includes != null ? !includes.equals(match.includes) : match.includes != null) return false; + return excludes != null ? excludes.equals(match.excludes) : match.excludes == null; + + } + + @Override + public int hashCode() { + int result = includes != null ? includes.hashCode() : 0; + result = 31 * result + (excludes != null ? excludes.hashCode() : 0); + return result; + } } diff --git a/json-view/src/main/java/com/monitorjbl/json/Memoizer.java b/json-view/src/main/java/com/monitorjbl/json/Memoizer.java new file mode 100644 index 0000000..04cd7a0 --- /dev/null +++ b/json-view/src/main/java/com/monitorjbl/json/Memoizer.java @@ -0,0 +1,95 @@ +package com.monitorjbl.json; + +import java.lang.reflect.Field; +import java.util.EnumMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import static com.monitorjbl.json.Memoizer.FunctionCache.IGNORE_ANNOTATIONS; +import static com.monitorjbl.json.Memoizer.FunctionCache.MATCHES; + +@SuppressWarnings("unchecked") +class Memoizer { + private final int maxCacheSize; + private final Map> cache = new EnumMap<>(FunctionCache.class); + + public Memoizer(int maxCacheSize) { + this.maxCacheSize = maxCacheSize; + for(FunctionCache key : FunctionCache.class.getEnumConstants()) { + cache.put(key, new ConcurrentHashMap<>()); + } + } + + public T ignoreAnnotations(Field f, Supplier compute) { + return (T) fitToMaxSize(IGNORE_ANNOTATIONS).computeIfAbsent(new MonoArg(f), (k) -> compute.get()); + } + + public T matches(Set values, String pattern, boolean matchPrefix, Supplier compute) { + return (T) fitToMaxSize(MATCHES).computeIfAbsent(new TriArg(values, pattern, matchPrefix), (k) -> compute.get()); + } + + private Map fitToMaxSize(FunctionCache key) { + Map map = cache.get(key); + if(map.size() > maxCacheSize) { + map.remove(map.keySet().iterator().next()); + } + return map; + } + + enum FunctionCache { + IGNORE_ANNOTATIONS, MATCHES + } + + private interface Arg {} + + private class MonoArg implements Arg { + private final Object arg1; + + public MonoArg(Object arg1) { + this.arg1 = arg1; + } + + @Override + public boolean equals(Object o) { + MonoArg monoArg = (MonoArg) o; + + return arg1 != null ? arg1.equals(monoArg.arg1) : monoArg.arg1 == null; + } + + @Override + public int hashCode() { + return arg1 != null ? arg1.hashCode() : 0; + } + } + + private class TriArg implements Arg { + private final Object arg1; + private final Object arg2; + private final Object arg3; + + public TriArg(Object arg1, Object arg2, Object arg3) { + this.arg1 = arg1; + this.arg2 = arg2; + this.arg3 = arg3; + } + + @Override + public boolean equals(Object o) { + TriArg triArg = (TriArg) o; + + if(arg1 != null ? !arg1.equals(triArg.arg1) : triArg.arg1 != null) return false; + if(arg2 != null ? !arg2.equals(triArg.arg2) : triArg.arg2 != null) return false; + return arg3 != null ? arg3.equals(triArg.arg3) : triArg.arg3 == null; + } + + @Override + public int hashCode() { + int result = arg1 != null ? arg1.hashCode() : 0; + result = 31 * result + (arg2 != null ? arg2.hashCode() : 0); + result = 31 * result + (arg3 != null ? arg3.hashCode() : 0); + return result; + } + } +} diff --git a/json-view/src/test/java/com/monitorjbl/json/JsonViewSerializerPerformanceTest.java b/json-view/src/test/java/com/monitorjbl/json/JsonViewSerializerPerformanceTest.java index c78f7b6..a318baa 100644 --- a/json-view/src/test/java/com/monitorjbl/json/JsonViewSerializerPerformanceTest.java +++ b/json-view/src/test/java/com/monitorjbl/json/JsonViewSerializerPerformanceTest.java @@ -11,7 +11,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.math.RoundingMode; +import java.text.DecimalFormat; import java.util.Arrays; import java.util.Collection; @@ -20,6 +24,7 @@ @RunWith(Parameterized.class) public class JsonViewSerializerPerformanceTest { + private static final Logger log = LoggerFactory.getLogger(JsonViewSerializerPerformanceTest.class); private int repetitions; private JsonViewSerializer serializer = new JsonViewSerializer(); private ObjectMapper sut; @@ -28,7 +33,7 @@ public class JsonViewSerializerPerformanceTest { @Parameterized.Parameters public static Collection data() { return Arrays.asList(new Object[][]{ - {500}, {1000}, {10000}, {100000}, {1000000} + {1000}, {10000}, {100000}, {1000000} }); } @@ -46,44 +51,33 @@ public void setup() { @Test public void comparePerformance() throws Exception { - long baselineTimes = baselineRandomSingleObjectPerformance(); - long jsonViewTimes = jsonViewRandomSingleObjectPerformance(); - long difference = (long) (((double) jsonViewTimes) / (double) baselineTimes) * 100L; + long baselineTimes = randomSingleObjectPerformance(() -> + compare.writeValueAsString(testObject())); + long jsonViewTimes = randomSingleObjectPerformance(() -> + sut.writeValueAsString(JsonView.with(testObject()).onClass(TestObject.class, match().exclude("int1")))); + String difference = divide(jsonViewTimes * 100L, baselineTimes); System.out.printf("[%-8s]: | Baseline: %-8s | JsonView: %-8s | Difference: %-6s |\n", - repetitions, (baselineTimes / 1000000) + "ms", (jsonViewTimes / 1000000) + "ms", difference + "%"); + repetitions, divide(baselineTimes, 1000000L) + "ms", divide(jsonViewTimes, 1000000L) + "ms", difference + "%"); } - public long jsonViewRandomSingleObjectPerformance() throws Exception { - long times = 0; - for(int i = 0; i < repetitions; i++) { - TestObject ref = testObject(); - + public long randomSingleObjectPerformance(UncheckedRunnable mapper) throws Exception { + long totalTime = 0; + long chunkTime = 0; + for(int i = 1; i <= repetitions; i++) { long time = System.nanoTime(); - sut.writeValueAsString(JsonView.with(ref).onClass(TestObject.class, match() - .exclude("int1"))); + mapper.run(); time = System.nanoTime() - time; - if(i > 100) { - times += time; - } - } - return times; - } - - public long baselineRandomSingleObjectPerformance() throws Exception { - long times = 0; - for(int i = 0; i < repetitions; i++) { - TestObject ref = testObject(); - - long time = System.nanoTime(); - compare.writeValueAsString(ref); - time = System.nanoTime() - time; - if(i > 100) { - times += time; + totalTime += time; + chunkTime += time; + if(i % 100000 == 0) { + log.trace("Time per 100k entries: " + ((double) chunkTime / 1000000.0) + "ms"); + chunkTime = 0; } } - return times; + + return totalTime; } TestObject testObject() { @@ -99,4 +93,14 @@ TestObject testObject() { ref.setListOfObjects(newArrayList(sub)); return ref; } + + String divide(long numerator, long denominator) { + DecimalFormat df = new DecimalFormat("#.###"); + df.setRoundingMode(RoundingMode.CEILING); + return df.format((double) numerator / (double) denominator); + } + + interface UncheckedRunnable { + public void run() throws Exception; + } } \ No newline at end of file diff --git a/json-view/src/test/java/com/monitorjbl/json/WriterTest.java b/json-view/src/test/java/com/monitorjbl/json/WriterTest.java index 308ecee..e0dbf2b 100644 --- a/json-view/src/test/java/com/monitorjbl/json/WriterTest.java +++ b/json-view/src/test/java/com/monitorjbl/json/WriterTest.java @@ -17,8 +17,10 @@ import java.net.URL; import java.util.Date; import java.util.List; +import java.util.Set; import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Sets.newHashSet; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -46,7 +48,7 @@ public void setup() { @Test public void testContainsMatchingPattern_basic() { - List patterns = newArrayList("field1", "field2"); + Set patterns = newHashSet("field1", "field2"); assertEquals(1, sut.containsMatchingPattern(patterns, "field1", true)); assertEquals(1, sut.containsMatchingPattern(patterns, "field2", true)); assertEquals(-1, sut.containsMatchingPattern(patterns, "field3", true)); @@ -54,7 +56,7 @@ public void testContainsMatchingPattern_basic() { @Test public void testContainsMatchingPattern_wildcard() { - List patterns = newArrayList("field*"); + Set patterns = newHashSet("field*"); assertEquals(0, sut.containsMatchingPattern(patterns, "field1", true)); assertEquals(0, sut.containsMatchingPattern(patterns, "field2", true)); assertEquals(-1, sut.containsMatchingPattern(patterns, "val1", true)); @@ -62,7 +64,7 @@ public void testContainsMatchingPattern_wildcard() { @Test public void testContainsMatchingPattern_wildcardAll() { - List patterns = newArrayList("*"); + Set patterns = newHashSet("*"); assertEquals(0, sut.containsMatchingPattern(patterns, "field1", true)); assertEquals(0, sut.containsMatchingPattern(patterns, "field2", true)); assertEquals(0, sut.containsMatchingPattern(patterns, "val1", true)); @@ -70,14 +72,14 @@ public void testContainsMatchingPattern_wildcardAll() { @Test public void testContainsMatchingPattern_wildcardInChildPath() { - List patterns = newArrayList("*.green"); + Set patterns = newHashSet("*.green"); assertEquals(0, sut.containsMatchingPattern(patterns, "field1.green", true)); assertEquals(-1, sut.containsMatchingPattern(patterns, "field2.blue", true)); } @Test public void testContainsMatchingPattern_wildcardInComplexPath() { - List patterns = newArrayList("*.green.*"); + Set patterns = newHashSet("*.green.*"); assertEquals(-1, sut.containsMatchingPattern(patterns, "field1.green", true)); assertEquals(-1, sut.containsMatchingPattern(patterns, "field2.blue", true)); assertEquals(0, sut.containsMatchingPattern(patterns, "field1.green.id", true)); diff --git a/json-view/src/test/resources/log4j.properties b/json-view/src/test/resources/log4j.properties new file mode 100644 index 0000000..71255bb --- /dev/null +++ b/json-view/src/test/resources/log4j.properties @@ -0,0 +1,8 @@ +# Root logger option +log4j.rootLogger=DEBUG, stdout + +# Direct log messages to stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n \ No newline at end of file diff --git a/spring-json-view/pom.xml b/spring-json-view/pom.xml index 178900c..8d62587 100644 --- a/spring-json-view/pom.xml +++ b/spring-json-view/pom.xml @@ -41,8 +41,8 @@ maven-compiler-plugin 3.2 - 1.7 - 1.7 + 1.8 + 1.8 diff --git a/spring-json-view/src/main/java/com/monitorjbl/json/JsonViewMessageConverter.java b/spring-json-view/src/main/java/com/monitorjbl/json/JsonViewMessageConverter.java index b17b696..af5650c 100644 --- a/spring-json-view/src/main/java/com/monitorjbl/json/JsonViewMessageConverter.java +++ b/spring-json-view/src/main/java/com/monitorjbl/json/JsonViewMessageConverter.java @@ -1,19 +1,18 @@ package com.monitorjbl.json; -import java.io.IOException; - +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import org.springframework.http.HttpOutputMessage; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; public class JsonViewMessageConverter extends MappingJackson2HttpMessageConverter { - private JsonViewSerializer serializer=new JsonViewSerializer(); - + private JsonViewSerializer serializer = new JsonViewSerializer(); + public JsonViewMessageConverter() { super(); ObjectMapper defaultMapper = new ObjectMapper(); @@ -37,27 +36,28 @@ public JsonViewMessageConverter(ObjectMapper mapper) { * Thus, when JSonView find a field of that type (DateTime), it will delegate the serialization to the serializer specified.
* Example:
* - * JsonViewSupportFactoryBean bean = new JsonViewSupportFactoryBean( mapper ); - * bean.registerCustomSerializer( DateTime.class, new DateTimeSerializer() ); + * JsonViewSupportFactoryBean bean = new JsonViewSupportFactoryBean( mapper ); + * bean.registerCustomSerializer( DateTime.class, new DateTimeSerializer() ); * - * @param Type class of the serializer - * @param class1 {@link Class} the class type you want to add a custom serializer + * + * @param Type class of the serializer + * @param class1 {@link Class} the class type you want to add a custom serializer * @param forType {@link JsonSerializer} the serializer you want to apply for that type */ - public void registerCustomSerializer( Class class1, JsonSerializer forType ) - { - this.serializer.registerCustomSerializer(class1, forType ); + public void registerCustomSerializer(Class class1, JsonSerializer forType) { + this.serializer.registerCustomSerializer(class1, forType); } - + /** * Unregister a previously registtered serializer. @see registerCustomSerializer - * @param class1 + * + * @param Type class of the serializer + * @param class1 {@link Class} the class type for which you want to remove a custom serializer */ - public void unregisterCustomSerializer( Class class1 ) - { - this.serializer.unregisterCustomSerializer(class1); + public void unregisterCustomSerializer(Class class1) { + this.serializer.unregisterCustomSerializer(class1); } - + @Override protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { super.writeInternal(object, outputMessage); diff --git a/spring-json-view/src/main/java/com/monitorjbl/json/JsonViewSupportFactoryBean.java b/spring-json-view/src/main/java/com/monitorjbl/json/JsonViewSupportFactoryBean.java index b185ea8..7e08452 100644 --- a/spring-json-view/src/main/java/com/monitorjbl/json/JsonViewSupportFactoryBean.java +++ b/spring-json-view/src/main/java/com/monitorjbl/json/JsonViewSupportFactoryBean.java @@ -98,22 +98,22 @@ private void decorateHandlers(List handlers) { * bean.registerCustomSerializer( DateTime.class, new DateTimeSerializer() ); * * @param Type class of the serializer - * @param class1 {@link Class} the class type you want to add a custom serializer + * @param cls {@link Class} the class type you want to add a custom serializer * @param forType {@link JsonSerializer} the serializer you want to apply for that type */ - public void registerCustomSerializer( Class class1, JsonSerializer forType ) + public void registerCustomSerializer( Class cls, JsonSerializer forType ) { - this.converter.registerCustomSerializer( class1, forType ); + this.converter.registerCustomSerializer( cls, forType ); } /** * Unregister a previously registtered serializer. @see registerCustomSerializer - * @param class1 + * @param cls The class type the serializer was registered for */ - public void unregisterCustomSerializer( Class class1 ) + public void unregisterCustomSerializer( Class cls ) { - this.converter.unregisterCustomSerializer(class1); + this.converter.unregisterCustomSerializer(cls); } } \ No newline at end of file