diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java index 8cecc2be70..c2dd470085 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java @@ -161,21 +161,11 @@ public abstract class DeserializationContext /********************************************************** */ - protected DeserializationContext(DeserializerFactory df) { - this(df, null); - } - protected DeserializationContext(DeserializerFactory df, DeserializerCache cache) { - if (df == null) { - throw new NullPointerException("Cannot pass null DeserializerFactory"); - } - _factory = df; - if (cache == null) { - cache = new DeserializerCache(); - } - _cache = cache; + _factory = Objects.requireNonNull(df); + _cache = Objects.requireNonNull(cache); _featureFlags = 0; _readCapabilities = null; _config = null; @@ -199,6 +189,24 @@ protected DeserializationContext(DeserializationContext src, _attributes = src._attributes; } + /** + * @since 2.16 + */ + protected DeserializationContext(DeserializationContext src, + DeserializerCache cache) + { + _cache = cache; + _factory = src._factory; + + _config = src._config; + _featureFlags = src._featureFlags; + _readCapabilities = src._readCapabilities; + _view = src._view; + _parser = src._parser; + _injectableValues = src._injectableValues; + _attributes = src._attributes; + } + /** * Constructor used for creating actual per-call instances. */ @@ -243,17 +251,19 @@ protected DeserializationContext(DeserializationContext src, } /** - * Copy-constructor for use with copy() by {@link ObjectMapper#copy()} + * Copy-constructor for use with copy() by {@link ObjectMapper#copy()}. + * Only called on blueprint objects. */ protected DeserializationContext(DeserializationContext src) { - _cache = new DeserializerCache(); + _cache = src._cache.emptyCopy(); _factory = src._factory; _config = src._config; _featureFlags = src._featureFlags; _readCapabilities = src._readCapabilities; _view = src._view; - _injectableValues = null; + _injectableValues = src._injectableValues; + _attributes = null; } /* diff --git a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java index 4489cab9d1..a9ccb1bb81 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java +++ b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java @@ -2282,6 +2282,7 @@ public ObjectMapper setCacheProvider(CacheProvider cacheProvider) { _assertNotNull("cacheProvider", cacheProvider); _deserializationConfig = _deserializationConfig.with(cacheProvider); _serializationConfig = _serializationConfig.with(cacheProvider); + _deserializationContext = _deserializationContext.withCaches(cacheProvider); return this; } diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/CacheProvider.java b/src/main/java/com/fasterxml/jackson/databind/cfg/CacheProvider.java index d9cebf5964..d1a8487dbc 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/CacheProvider.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/CacheProvider.java @@ -1,5 +1,11 @@ package com.fasterxml.jackson.databind.cfg; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.DeserializerCache; +import com.fasterxml.jackson.databind.util.LookupCache; + /** * Interface that defines API Jackson uses for constructing various internal * caches. This allows configuring custom caches and cache configurations. @@ -11,5 +17,11 @@ public interface CacheProvider extends java.io.Serializable { - // !!! TODO: add methods + /** + * Method to provide a {@link LookupCache} instance for constructing {@link DeserializerCache}. + * + * @return {@link LookupCache} instance for constructing {@link DeserializerCache}. + */ + LookupCache> forDeserializerCache(DeserializationConfig config); + } diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/DefaultCacheProvider.java b/src/main/java/com/fasterxml/jackson/databind/cfg/DefaultCacheProvider.java index 9b16755383..6859889725 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/DefaultCacheProvider.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/DefaultCacheProvider.java @@ -1,8 +1,18 @@ package com.fasterxml.jackson.databind.cfg; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.DeserializerCache; +import com.fasterxml.jackson.databind.util.LRUMap; +import com.fasterxml.jackson.databind.util.LookupCache; + /** - * Default implementation of {@link CacheProvider}. - * + * The default implementation of {@link CacheProvider}. + * Configuration is builder-based via {@link DefaultCacheProvider.Builder}. + *

+ * Users can either use this class or create their own {@link CacheProvider} implementation. + * * @since 2.16 */ public class DefaultCacheProvider @@ -10,10 +20,123 @@ public class DefaultCacheProvider { private static final long serialVersionUID = 1L; - public static DefaultCacheProvider defaultInstance() { - return new DefaultCacheProvider(); - } + private final static DefaultCacheProvider DEFAULT + = new DefaultCacheProvider(DeserializerCache.DEFAULT_MAX_CACHE_SIZE); + + /** + * Maximum size of the {@link LookupCache} instance constructed by {@link #forDeserializerCache(DeserializationConfig)}. + * + * @see Builder#maxDeserializerCacheSize(int) + */ + protected final int _maxDeserializerCacheSize; - // To implement! + /* + /********************************************************************** + /* Life cycle + /********************************************************************** + */ + + protected DefaultCacheProvider(int deserializerCache) + { + _maxDeserializerCacheSize = deserializerCache; + } + + /* + /********************************************************************** + /* Defaults + /********************************************************************** + */ + + /** + * @return Default {@link DefaultCacheProvider} instance using default configuration values. + */ + public static CacheProvider defaultInstance() { + return DEFAULT; + } + + /* + /********************************************************** + /* API implementation + /********************************************************** + */ + + /** + * Method to provide a {@link LookupCache} instance for constructing {@link DeserializerCache}. + * Implementation should match {@link DeserializerCache#DeserializerCache(int)}. + * + * @return {@link LookupCache} instance for constructing {@link DeserializerCache}. + */ + @Override + public LookupCache> forDeserializerCache(DeserializationConfig config) { + return _buildCache(_maxDeserializerCacheSize); + } + /* + /********************************************************** + /* Overridable factory methods + /********************************************************** + */ + + protected LookupCache _buildCache(int maxSize) + { + // Use 1/4 of maximum size (but at most 64) for initial size + final int initialSize = Math.min(64, maxSize >> 2); + return new LRUMap<>(initialSize, maxSize); + } + + /* + /********************************************************** + /* Builder Config + /********************************************************** + */ + + /** + * @return {@link Builder} instance for configuration. + */ + public static DefaultCacheProvider.Builder builder() { + return new Builder(); + } + + /** + * Builder offering fluent factory methods to configure {@link DefaultCacheProvider}, keeping it immutable. + */ + public static class Builder { + + /** + * Maximum Size of the {@link LookupCache} instance created by {@link #forDeserializerCache(DeserializationConfig)}. + * Corresponds to {@link DefaultCacheProvider#_maxDeserializerCacheSize}. + */ + private int _maxDeserializerCacheSize; + + Builder() { } + + /** + * Define the maximum size of the {@link LookupCache} instance constructed by {@link #forDeserializerCache(DeserializationConfig)}. + * The cache is instantiated as: + *

+         *     return new LRUMap<>(Math.min(64, maxSize >> 2), maxSize);
+         * 
+ * + * @param maxDeserializerCacheSize Size for the {@link LookupCache} to use within {@link DeserializerCache} + * @return this builder + * @throws IllegalArgumentException if {@code maxDeserializerCacheSize} is negative + * @since 2.16 + */ + public Builder maxDeserializerCacheSize(int maxDeserializerCacheSize) { + if (maxDeserializerCacheSize < 0) { + throw new IllegalArgumentException("Cannot set maxDeserializerCacheSize to a negative value"); + } + _maxDeserializerCacheSize = maxDeserializerCacheSize; + return this; + } + + /** + * Constructs a {@link DefaultCacheProvider} with the provided configuration values, using defaults where not specified. + * + * @return A {@link DefaultCacheProvider} instance with the specified configuration + */ + public DefaultCacheProvider build() { + return new DefaultCacheProvider(_maxDeserializerCacheSize); + } + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/DefaultDeserializationContext.java b/src/main/java/com/fasterxml/jackson/databind/deser/DefaultDeserializationContext.java index 0e57e3ccb0..1fdc8bdafb 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/DefaultDeserializationContext.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/DefaultDeserializationContext.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.cfg.CacheProvider; import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; import com.fasterxml.jackson.databind.deser.impl.ReadableObjectId; import com.fasterxml.jackson.databind.deser.impl.ReadableObjectId.Referring; @@ -62,6 +63,16 @@ protected DefaultDeserializationContext(DefaultDeserializationContext src, } /** + * @since 2.16 + */ + protected DefaultDeserializationContext(DefaultDeserializationContext src, + DeserializerCache cache) { + super(src, cache); + } + + /** + * Copy-constructor + * * @since 2.4.4 */ protected DefaultDeserializationContext(DefaultDeserializationContext src) { @@ -296,6 +307,13 @@ public final KeyDeserializer keyDeserializerInstance(Annotated ann, Object deser */ public abstract DefaultDeserializationContext with(DeserializerFactory factory); + /** + * Fluent factory method used for constructing a new instance with cache instances provided by {@link CacheProvider}. + * + * @since 2.16 + */ + public abstract DefaultDeserializationContext withCaches(CacheProvider cacheProvider); + /** * Method called to create actual usable per-deserialization * context instance. @@ -384,7 +402,9 @@ public final static class Impl extends DefaultDeserializationContext * {@link DeserializerCache}, given factory. */ public Impl(DeserializerFactory df) { - super(df, null); + // 04-Sep-2023, tatu: Not ideal (wrt not going via CacheProvider) but + // has to do for backwards compatibility: + super(df, new DeserializerCache()); } private Impl(Impl src, @@ -402,6 +422,13 @@ private Impl(Impl src, DeserializationConfig config) { super(src, config); } + /** + * @since 2.16 + */ + private Impl(Impl src, DeserializerCache deserializerCache) { + super(src, deserializerCache); + } + @Override public DefaultDeserializationContext copy() { ClassUtil.verifyMustOverride(Impl.class, this, "copy"); @@ -424,5 +451,11 @@ public DefaultDeserializationContext createDummyInstance(DeserializationConfig c public DefaultDeserializationContext with(DeserializerFactory factory) { return new Impl(this, factory); } + + @Override // Since 2.16 + public DefaultDeserializationContext withCaches(CacheProvider cacheProvider) { + return new Impl(this, + new DeserializerCache(cacheProvider.forDeserializerCache(_config))); + } } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java b/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java index 9c067e43ab..e4ce196714 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java @@ -73,6 +73,13 @@ public DeserializerCache(LookupCache> cache) _cachedDeserializers = cache; } + /** + * @since 2.16 + */ + public DeserializerCache emptyCopy() { + return new DeserializerCache(_cachedDeserializers.emptyCopy()); + } + /* /********************************************************** /* JDK serialization handling diff --git a/src/test/java/com/fasterxml/jackson/databind/cfg/CacheProviderTest.java b/src/test/java/com/fasterxml/jackson/databind/cfg/CacheProviderTest.java new file mode 100644 index 0000000000..a9c0a6738a --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/cfg/CacheProviderTest.java @@ -0,0 +1,180 @@ +package com.fasterxml.jackson.databind.cfg; + +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.util.LookupCache; +import org.junit.Test; + +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * + * [databind#2502] Test for adding a way to configure Caches Jackson uses + * + * @since 2.16 + */ +public class CacheProviderTest +{ + + static class RandomBean { + public int point; + } + + static class AnotherBean { + public int height; + } + + static class SimpleTestCache implements LookupCache> { + + final HashMap> _cachedDeserializers; + + boolean invokedAtLeastOnce = false; + + public SimpleTestCache(int cacheSize) { + _cachedDeserializers = new HashMap<>(cacheSize); + } + + @Override + public int size() { + return _cachedDeserializers.size(); + } + + @Override + public JsonDeserializer get(Object key) { + invokedAtLeastOnce = true; + return _cachedDeserializers.get(key); + } + + @Override + public JsonDeserializer put(JavaType key, JsonDeserializer value) { + invokedAtLeastOnce = true; + return _cachedDeserializers.put(key, value); + } + + @Override + public JsonDeserializer putIfAbsent(JavaType key, JsonDeserializer value) { + invokedAtLeastOnce = true; + return _cachedDeserializers.putIfAbsent(key, value); + } + + @Override + public void clear() { + _cachedDeserializers.clear(); + } + + boolean isInvokedAtLeastOnce() { + return invokedAtLeastOnce; + } + } + + static class CustomCacheProvider implements CacheProvider { + + final SimpleTestCache _cache; + int createCacheCount = 0; + + public CustomCacheProvider(SimpleTestCache cache) { + _cache = cache; + } + + @Override + public LookupCache> forDeserializerCache(DeserializationConfig config) { + createCacheCount++; + return _cache; + } + + int createCacheCount() { + return createCacheCount; + } + } + + /* + /********************************************************************** + /* Unit tests + /********************************************************************** + */ + + @Test + public void testDefaultCacheProviderConfigDeserializerCache() throws Exception + { + CacheProvider cacheProvider = DefaultCacheProvider.builder() + .maxDeserializerCacheSize(1234) + .build(); + ObjectMapper mapper = JsonMapper.builder() + .cacheProvider(cacheProvider).build(); + + assertNotNull(mapper.readValue("{\"point\":24}", RandomBean.class)); + } + + @Test + public void testDefaultCacheProviderConfigDeserializerCacheSizeZero() throws Exception + { + CacheProvider cacheProvider = DefaultCacheProvider.builder() + .maxDeserializerCacheSize(0) + .build(); + ObjectMapper mapper = JsonMapper.builder() + .cacheProvider(cacheProvider) + .build(); + + assertNotNull(mapper.readValue("{\"point\":24}", RandomBean.class)); + } + + @Test + public void testBuilderNullCheckingForDeserializerCacheConfig() throws Exception + { + try { + DefaultCacheProvider.builder() + .maxDeserializerCacheSize(-1); + fail("Should not reach here"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Cannot set maxDeserializerCacheSize to a negative value")); + } + } + + @Test + public void testCustomCacheProviderConfig() throws Exception + { + SimpleTestCache cache = new SimpleTestCache(123); + ObjectMapper mapper = JsonMapper.builder() + .cacheProvider(new CustomCacheProvider(cache)) + .build(); + + assertNotNull(mapper.readValue("{\"point\":24}", RandomBean.class)); + assertTrue(cache.isInvokedAtLeastOnce()); + } + + @Test + public void testDefaultCacheProviderSharesCache() throws Exception + { + // Arrange + // 1. shared CacheProvider + CustomCacheProvider cacheProvider = new CustomCacheProvider(new SimpleTestCache(123)); + // 2. two different mapper instances + ObjectMapper mapper1 = JsonMapper.builder() + .cacheProvider(cacheProvider) + .build(); + ObjectMapper mapper2 = JsonMapper.builder() + .cacheProvider(cacheProvider) + .build(); + + // Act + // 3. Add two different types to each mapper cache + mapper1.readValue("{\"point\":24}", RandomBean.class); + mapper2.readValue("{\"height\":24}", AnotherBean.class); + + // Assert + // 4. Should have created two cache instance + assertEquals(2, cacheProvider.createCacheCount()); + } + + @Test + public void testBuilderBuildWithDefaults() throws Exception + { + // does not throw + DefaultCacheProvider.builder().build(); + } +}