From 9816426bbaec23d404e2ac3094a8f8f079c6e250 Mon Sep 17 00:00:00 2001 From: Joel Leitch Date: Sun, 10 Jan 2010 00:32:21 +0000 Subject: [PATCH] Add new Cache interface and LRU Cache implementation to cache field annotations per "Class" (rather than per instance). This results in a significant speed-up (approx. 3X) when serializing/deserializing the same classes over and over again. --- gson/src/main/java/com/google/gson/Cache.java | 61 ++++++++++++++++ .../java/com/google/gson/FieldAttributes.java | 25 +++++-- .../LowerCamelCaseSeparatorNamingPolicy.java | 2 +- .../main/java/com/google/gson/LruCache.java | 64 ++++++++++++++++ .../java/com/google/gson/ObjectNavigator.java | 2 +- .../DisjunctionExclusionStrategyTest.java | 2 +- ...nDeserializationExclusionStrategyTest.java | 8 +- ...ionSerializationExclusionStrategyTest.java | 8 +- .../com/google/gson/FieldAttributesTest.java | 6 +- .../gson/InnerClassExclusionStrategyTest.java | 4 +- .../gson/JavaFieldNamingPolicyTest.java | 2 +- .../java/com/google/gson/LruCacheTest.java | 73 +++++++++++++++++++ .../gson/NullExclusionStrategyTest.java | 2 +- ...nnotationInterceptingNamingPolicyTest.java | 6 +- .../gson/VersionExclusionStrategyTest.java | 6 +- 15 files changed, 241 insertions(+), 30 deletions(-) create mode 100644 gson/src/main/java/com/google/gson/Cache.java create mode 100644 gson/src/main/java/com/google/gson/LruCache.java create mode 100644 gson/src/test/java/com/google/gson/LruCacheTest.java diff --git a/gson/src/main/java/com/google/gson/Cache.java b/gson/src/main/java/com/google/gson/Cache.java new file mode 100644 index 00000000..32e1f8c8 --- /dev/null +++ b/gson/src/main/java/com/google/gson/Cache.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson; + +/** + * Defines generic cache interface. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +interface Cache { + + /** + * Adds the new value object into the cache for the given key. If the key already + * exists, then this method will override the value for the key. + * + * @param key the key identifier for the {@code value} object + * @param value the value object to store in the cache + */ + void addElement(K key, V value); + + /** + * Retrieve the cached value for the given {@code key}. + * + * @param key the key identifying the value + * @return the cached value for the given {@code key} + */ + V getElement(K key); + + /** + * Removes the value from the cache for the given key. + * + * @param key the key identifying the value to remove + * @return the value for the given {@code key} + */ + V removeElement(K key); + + /** + * Removes everything from this cache. + */ + void clear(); + + /** + * @return the number of objects in this cache + */ + int size(); +} diff --git a/gson/src/main/java/com/google/gson/FieldAttributes.java b/gson/src/main/java/com/google/gson/FieldAttributes.java index 9ad239b9..870b168d 100644 --- a/gson/src/main/java/com/google/gson/FieldAttributes.java +++ b/gson/src/main/java/com/google/gson/FieldAttributes.java @@ -34,6 +34,11 @@ import java.util.Collections; * @since 1.4 */ public final class FieldAttributes { + // TODO(Joel): Fix how we configure this cache in a follow-up CL. + private static final Cache, String>, Collection> ANNOTATION_CACHE = + new LruCache,String>, Collection>(1500); + + private final Class parentClazz; private final Field field; private final Class declaredType; private final boolean isSynthetic; @@ -49,13 +54,14 @@ public final class FieldAttributes { * * @param f the field to pull attributes from */ - FieldAttributes(final Field f) { - Preconditions.checkNotNull(f); - field = f; - name = field.getName(); + FieldAttributes(final Class parentClazz, final Field f) { + Preconditions.checkNotNull(parentClazz); + this.parentClazz = parentClazz; + name = f.getName(); declaredType = f.getType(); isSynthetic = f.isSynthetic(); - modifiers = field.getModifiers(); + modifiers = f.getModifiers(); + field = f; } /** @@ -127,8 +133,13 @@ public final class FieldAttributes { */ public Collection getAnnotations() { if (annotations == null) { - annotations = Collections.unmodifiableCollection( - Arrays.asList(field.getAnnotations())); + Pair, String> key = new Pair, String>(parentClazz, name); + annotations = ANNOTATION_CACHE.getElement(key); + if (annotations == null) { + annotations = Collections.unmodifiableCollection( + Arrays.asList(field.getAnnotations())); + ANNOTATION_CACHE.addElement(key, annotations); + } } return annotations; } diff --git a/gson/src/main/java/com/google/gson/LowerCamelCaseSeparatorNamingPolicy.java b/gson/src/main/java/com/google/gson/LowerCamelCaseSeparatorNamingPolicy.java index 475291bf..cac7eab9 100644 --- a/gson/src/main/java/com/google/gson/LowerCamelCaseSeparatorNamingPolicy.java +++ b/gson/src/main/java/com/google/gson/LowerCamelCaseSeparatorNamingPolicy.java @@ -17,7 +17,7 @@ package com.google.gson; /** - * A {@link FieldNamingStrategy} that ensures the JSON field names consist of only + * A {@link FieldNamingStrategy2} that ensures the JSON field names consist of only * lower case letters and are separated by a particular {@code separatorString}. * *

The following is an example:

diff --git a/gson/src/main/java/com/google/gson/LruCache.java b/gson/src/main/java/com/google/gson/LruCache.java new file mode 100644 index 00000000..e4e73d5e --- /dev/null +++ b/gson/src/main/java/com/google/gson/LruCache.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * An implementation of the {@link Cache} interface that evict objects from the cache using an + * LRU (least recently used) algorithm. Object start getting evicted from the cache once the + * {@code maxCapacity} is reached. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +class LruCache extends LinkedHashMap implements Cache { + private static final long serialVersionUID = 1L; + + private final int maxCapacity; + + LruCache(int maxCapacity) { + super(maxCapacity, 0.7F, true); + this.maxCapacity = maxCapacity; + } + + public void addElement(K key, V value) { + put(key, value); + } + + public void clear() { + super.clear(); + } + + public V getElement(K key) { + return get(key); + } + + public V removeElement(K key) { + return remove(key); + } + + public int size() { + return super.size(); + } + + @Override + protected boolean removeEldestEntry(Map.Entry entry) { + return size() > maxCapacity; + } +} diff --git a/gson/src/main/java/com/google/gson/ObjectNavigator.java b/gson/src/main/java/com/google/gson/ObjectNavigator.java index c72ffa7f..91f6d9c3 100644 --- a/gson/src/main/java/com/google/gson/ObjectNavigator.java +++ b/gson/src/main/java/com/google/gson/ObjectNavigator.java @@ -148,7 +148,7 @@ final class ObjectNavigator { Field[] fields = clazz.getDeclaredFields(); AccessibleObject.setAccessible(fields, true); for (Field f : fields) { - FieldAttributes fieldAttributes = new FieldAttributes(f); + FieldAttributes fieldAttributes = new FieldAttributes(clazz, f); if (exclusionStrategy.shouldSkipField(fieldAttributes) || exclusionStrategy.shouldSkipClass(fieldAttributes.getDeclaredClass())) { continue; // skip diff --git a/gson/src/test/java/com/google/gson/DisjunctionExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/DisjunctionExclusionStrategyTest.java index b8c528ad..34fcccb8 100644 --- a/gson/src/test/java/com/google/gson/DisjunctionExclusionStrategyTest.java +++ b/gson/src/test/java/com/google/gson/DisjunctionExclusionStrategyTest.java @@ -31,7 +31,7 @@ public class DisjunctionExclusionStrategyTest extends TestCase { private static final ExclusionStrategy FALSE_STRATEGY = new MockExclusionStrategy(false, false); private static final ExclusionStrategy TRUE_STRATEGY = new MockExclusionStrategy(true, true); private static final Class CLAZZ = String.class; - private static final FieldAttributes FIELD = new FieldAttributes(CLAZZ.getFields()[0]); + private static final FieldAttributes FIELD = new FieldAttributes(CLAZZ, CLAZZ.getFields()[0]); public void testBadInstantiation() throws Exception { try { diff --git a/gson/src/test/java/com/google/gson/ExposeAnnotationDeserializationExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/ExposeAnnotationDeserializationExclusionStrategyTest.java index 6de26ba3..8fcdb577 100644 --- a/gson/src/test/java/com/google/gson/ExposeAnnotationDeserializationExclusionStrategyTest.java +++ b/gson/src/test/java/com/google/gson/ExposeAnnotationDeserializationExclusionStrategyTest.java @@ -42,22 +42,22 @@ public class ExposeAnnotationDeserializationExclusionStrategyTest extends TestCa public void testSkipNonAnnotatedFields() throws Exception { Field f = MockObject.class.getField("hiddenField"); - assertTrue(strategy.shouldSkipField(new FieldAttributes(f))); + assertTrue(strategy.shouldSkipField(new FieldAttributes(MockObject.class, f))); } public void testSkipExplicitlySkippedFields() throws Exception { Field f = MockObject.class.getField("explicitlyHiddenField"); - assertTrue(strategy.shouldSkipField(new FieldAttributes(f))); + assertTrue(strategy.shouldSkipField(new FieldAttributes(MockObject.class, f))); } public void testNeverSkipExposedAnnotatedFields() throws Exception { Field f = MockObject.class.getField("exposedField"); - assertFalse(strategy.shouldSkipField(new FieldAttributes(f))); + assertFalse(strategy.shouldSkipField(new FieldAttributes(MockObject.class, f))); } public void testNeverSkipExplicitlyExposedAnnotatedFields() throws Exception { Field f = MockObject.class.getField("explicitlyExposedField"); - assertFalse(strategy.shouldSkipField(new FieldAttributes(f))); + assertFalse(strategy.shouldSkipField(new FieldAttributes(MockObject.class, f))); } @SuppressWarnings("unused") diff --git a/gson/src/test/java/com/google/gson/ExposeAnnotationSerializationExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/ExposeAnnotationSerializationExclusionStrategyTest.java index 4974719f..08ead177 100644 --- a/gson/src/test/java/com/google/gson/ExposeAnnotationSerializationExclusionStrategyTest.java +++ b/gson/src/test/java/com/google/gson/ExposeAnnotationSerializationExclusionStrategyTest.java @@ -42,22 +42,22 @@ public class ExposeAnnotationSerializationExclusionStrategyTest extends TestCase public void testSkipNonAnnotatedFields() throws Exception { Field f = MockObject.class.getField("hiddenField"); - assertTrue(strategy.shouldSkipField(new FieldAttributes(f))); + assertTrue(strategy.shouldSkipField(new FieldAttributes(MockObject.class, f))); } public void testSkipExplicitlySkippedFields() throws Exception { Field f = MockObject.class.getField("explicitlyHiddenField"); - assertTrue(strategy.shouldSkipField(new FieldAttributes(f))); + assertTrue(strategy.shouldSkipField(new FieldAttributes(MockObject.class, f))); } public void testNeverSkipExposedAnnotatedFields() throws Exception { Field f = MockObject.class.getField("exposedField"); - assertFalse(strategy.shouldSkipField(new FieldAttributes(f))); + assertFalse(strategy.shouldSkipField(new FieldAttributes(MockObject.class, f))); } public void testNeverSkipExplicitlyExposedAnnotatedFields() throws Exception { Field f = MockObject.class.getField("explicitlyExposedField"); - assertFalse(strategy.shouldSkipField(new FieldAttributes(f))); + assertFalse(strategy.shouldSkipField(new FieldAttributes(MockObject.class, f))); } @SuppressWarnings("unused") diff --git a/gson/src/test/java/com/google/gson/FieldAttributesTest.java b/gson/src/test/java/com/google/gson/FieldAttributesTest.java index e1e18ec3..77cabef8 100644 --- a/gson/src/test/java/com/google/gson/FieldAttributesTest.java +++ b/gson/src/test/java/com/google/gson/FieldAttributesTest.java @@ -36,14 +36,14 @@ public class FieldAttributesTest extends TestCase { @Override protected void setUp() throws Exception { super.setUp(); - fieldAttributes = new FieldAttributes(Foo.class.getField("bar")); + fieldAttributes = new FieldAttributes(Foo.class, Foo.class.getField("bar")); } public void testNullField() throws Exception { try { - new FieldAttributes(null); + new FieldAttributes(Foo.class, null); fail("Field parameter can not be null"); - } catch (IllegalArgumentException expected) { } + } catch (NullPointerException expected) { } } public void testModifiers() throws Exception { diff --git a/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java index 4abfeaae..4f6c4aad 100644 --- a/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java +++ b/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java @@ -46,7 +46,7 @@ public class InnerClassExclusionStrategyTest extends TestCase { public void testExcludeInnerClassField() throws Exception { Field f = getClass().getField("innerClass"); - assertTrue(strategy.shouldSkipField(new FieldAttributes(f))); + assertTrue(strategy.shouldSkipField(new FieldAttributes(getClass(), f))); } public void testIncludeStaticNestedClassObject() throws Exception { @@ -56,7 +56,7 @@ public class InnerClassExclusionStrategyTest extends TestCase { public void testIncludeStaticNestedClassField() throws Exception { Field f = getClass().getField("staticNestedClass"); - assertFalse(strategy.shouldSkipField(new FieldAttributes(f))); + assertFalse(strategy.shouldSkipField(new FieldAttributes(getClass(), f))); } class InnerClass { diff --git a/gson/src/test/java/com/google/gson/JavaFieldNamingPolicyTest.java b/gson/src/test/java/com/google/gson/JavaFieldNamingPolicyTest.java index 1836bdf2..65a8563a 100644 --- a/gson/src/test/java/com/google/gson/JavaFieldNamingPolicyTest.java +++ b/gson/src/test/java/com/google/gson/JavaFieldNamingPolicyTest.java @@ -34,7 +34,7 @@ public class JavaFieldNamingPolicyTest extends TestCase { } public void testFieldNamingPolicy() throws Exception { - FieldAttributes f = new FieldAttributes(String.class.getFields()[0]); + FieldAttributes f = new FieldAttributes(String.class, String.class.getFields()[0]); assertEquals(f.getName(), namingPolicy.translateName(f)); } diff --git a/gson/src/test/java/com/google/gson/LruCacheTest.java b/gson/src/test/java/com/google/gson/LruCacheTest.java new file mode 100644 index 00000000..4725db9a --- /dev/null +++ b/gson/src/test/java/com/google/gson/LruCacheTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson; + +import junit.framework.TestCase; + +/** + * Unit test for the {@link LruCache} class. + * + * @author Inderjeet Singh + * @author Joel Leitch + */ +public class LruCacheTest extends TestCase { + + public void testCacheHitAndMiss() throws Exception { + Cache cache = new LruCache(3); + + String key = "key1"; + assertNull(cache.getElement(key)); + cache.addElement(key, 1); + assertEquals(1, cache.getElement(key).intValue()); + + String key2 = "key2"; + cache.addElement(key2, 2); + assertEquals(1, cache.getElement(key).intValue()); + assertEquals(2, cache.getElement(key2).intValue()); + } + + public void testCacheKeyOverwrite() throws Exception { + Cache cache = new LruCache(3); + + String key = "key1"; + assertNull(cache.getElement(key)); + cache.addElement(key, 1); + assertEquals(1, cache.getElement(key).intValue()); + + cache.addElement(key, 5); + assertEquals(5, cache.getElement(key).intValue()); + } + + public void testCacheEviction() throws Exception { + Cache cache = new LruCache(3); + + cache.addElement("key1", 1); + cache.addElement("key2", 2); + cache.addElement("key3", 3); + assertEquals(1, cache.getElement("key1").intValue()); + assertEquals(2, cache.getElement("key2").intValue()); + assertEquals(3, cache.getElement("key3").intValue()); + + // Access key1 to show key2 will be evicted (shows not a FIFO cache) + cache.getElement("key1"); + cache.addElement("key4", 4); + assertEquals(1, cache.getElement("key1").intValue()); + assertNull(cache.getElement("key2")); + assertEquals(3, cache.getElement("key3").intValue()); + assertEquals(4, cache.getElement("key4").intValue()); + } +} diff --git a/gson/src/test/java/com/google/gson/NullExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/NullExclusionStrategyTest.java index bc0b8ef6..7710db19 100644 --- a/gson/src/test/java/com/google/gson/NullExclusionStrategyTest.java +++ b/gson/src/test/java/com/google/gson/NullExclusionStrategyTest.java @@ -38,6 +38,6 @@ public class NullExclusionStrategyTest extends TestCase { public void testNeverSkipsField() throws Exception { assertFalse(strategy.shouldSkipField( - new FieldAttributes(String.class.getFields()[0]))); + new FieldAttributes(String.class, String.class.getFields()[0]))); } } diff --git a/gson/src/test/java/com/google/gson/SerializedNameAnnotationInterceptingNamingPolicyTest.java b/gson/src/test/java/com/google/gson/SerializedNameAnnotationInterceptingNamingPolicyTest.java index 7894663b..57711028 100644 --- a/gson/src/test/java/com/google/gson/SerializedNameAnnotationInterceptingNamingPolicyTest.java +++ b/gson/src/test/java/com/google/gson/SerializedNameAnnotationInterceptingNamingPolicyTest.java @@ -38,7 +38,8 @@ public class SerializedNameAnnotationInterceptingNamingPolicyTest extends TestCa public void testFieldWithAnnotation() throws Exception { String fieldName = "fieldWithAnnotation"; - FieldAttributes f = new FieldAttributes(SomeObject.class.getField(fieldName)); + FieldAttributes f = new FieldAttributes( + SomeObject.class, SomeObject.class.getField(fieldName)); assertFalse(ANNOTATED_FIELD_NAME.equals(fieldName)); assertEquals(ANNOTATED_FIELD_NAME, policy.translateName(f)); @@ -46,7 +47,8 @@ public class SerializedNameAnnotationInterceptingNamingPolicyTest extends TestCa public void testFieldWithoutAnnotation() throws Exception { String fieldName = "fieldWithoutAnnotation"; - FieldAttributes f = new FieldAttributes(SomeObject.class.getField(fieldName)); + FieldAttributes f = new FieldAttributes( + SomeObject.class, SomeObject.class.getField(fieldName)); assertEquals(fieldName, policy.translateName(f)); } diff --git a/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java index 2034b4eb..26263816 100644 --- a/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java +++ b/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java @@ -43,7 +43,7 @@ public class VersionExclusionStrategyTest extends TestCase { VersionExclusionStrategy strategy = new VersionExclusionStrategy(VERSION); assertFalse(strategy.shouldSkipClass(clazz)); - assertFalse(strategy.shouldSkipField(new FieldAttributes(f))); + assertFalse(strategy.shouldSkipField(new FieldAttributes(clazz, f))); } public void testClassAndFieldAreBehindInVersion() throws Exception { @@ -52,7 +52,7 @@ public class VersionExclusionStrategyTest extends TestCase { VersionExclusionStrategy strategy = new VersionExclusionStrategy(VERSION + 1); assertFalse(strategy.shouldSkipClass(clazz)); - assertFalse(strategy.shouldSkipField(new FieldAttributes(f))); + assertFalse(strategy.shouldSkipField(new FieldAttributes(clazz, f))); } public void testClassAndFieldAreAheadInVersion() throws Exception { @@ -61,7 +61,7 @@ public class VersionExclusionStrategyTest extends TestCase { VersionExclusionStrategy strategy = new VersionExclusionStrategy(VERSION - 1); assertTrue(strategy.shouldSkipClass(clazz)); - assertTrue(strategy.shouldSkipField(new FieldAttributes(f))); + assertTrue(strategy.shouldSkipField(new FieldAttributes(clazz, f))); } @Since(VERSION)