From a597d3e1f7f77f7745fc01752dbe26568417b307 Mon Sep 17 00:00:00 2001 From: JavaNo0b <98101954+JavaNo0b@users.noreply.github.com> Date: Tue, 6 May 2025 19:26:47 +0900 Subject: [PATCH 01/10] Add performance warning to RedisOperations#keys() Javadoc Signed-off-by: JavaNo0b <98101954+JavaNo0b@users.noreply.github.com> --- .../data/redis/core/RedisOperations.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/core/RedisOperations.java b/src/main/java/org/springframework/data/redis/core/RedisOperations.java index 4ea682d900..cadfa9c2de 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/RedisOperations.java @@ -262,11 +262,14 @@ T execute(RedisScript script, RedisSerializer argsSerializer, RedisSer DataType type(K key); /** - * Find all keys matching the given {@code pattern}. - * - * @param pattern must not be {@literal null}. - * @return {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: KEYS + * Retrieve keys matching the given pattern via {@code KEYS} command. + *

+ * Note: This command scans the entire keyspace and may cause performance issues + * in production environments. Prefer using {@link #scan(ScanOptions)} for large datasets. + * + * @param pattern key pattern + * @return set of matching keys, or {@literal null} when used in pipeline / transaction + * @see Redis KEYS command */ @Nullable Set keys(K pattern); From 2ae6ab869d11cb1854abe6a7fe9ab32caf4055fe Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 30 May 2025 07:59:22 +0200 Subject: [PATCH 02/10] Update javadoc. Original Pull Request: #3142 --- .../data/redis/connection/ReactiveKeyCommands.java | 7 ++++--- .../data/redis/connection/RedisKeyCommands.java | 5 ++++- .../data/redis/connection/StringRedisConnection.java | 5 ++++- .../springframework/data/redis/core/ClusterOperations.java | 5 ++++- .../data/redis/core/ReactiveRedisOperations.java | 7 ++++--- .../springframework/data/redis/core/RedisOperations.java | 6 +++--- 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java index 3354cf9af1..877a787f5e 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java @@ -244,9 +244,10 @@ default Mono touch(Collection keys) { Flux, Long>> touch(Publisher> keys); /** - * Find all keys matching the given {@literal pattern}.
- * It is recommended to use {@link #scan(ScanOptions)} to iterate over the keyspace as {@link #keys(ByteBuffer)} is a - * non-interruptible and expensive Redis operation. + * Retrieve all keys matching the given pattern via {@code KEYS} command. + *

+ * IMPORTANT: This command is non-interruptible and scans the entire keyspace which may cause + * performance issues. Consider {@link #scan(ScanOptions)} for large datasets. * * @param pattern must not be {@literal null}. * @return diff --git a/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java index 4319dd8705..e79cf2c0f8 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java @@ -122,7 +122,10 @@ default Boolean exists(byte[] key) { Long touch(byte[]... keys); /** - * Find all keys matching the given {@code pattern}. + * Retrieve all keys matching the given pattern. + *

+ * IMPORTANT: The {@literal KEYS} command is non-interruptible and scans the entire keyspace which + * may cause performance issues. Consider {@link #scan(ScanOptions)} for large datasets. * * @param pattern must not be {@literal null}. * @return empty {@link Set} if no match found. {@literal null} when used in pipeline / transaction. diff --git a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index dfcc585130..dd483bdf89 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -185,7 +185,10 @@ interface StringTuple extends Tuple { Long touch(String... keys); /** - * Find all keys matching the given {@code pattern}. + * Retrieve all keys matching the given pattern via {@code KEYS} command. + *

+ * IMPORTANT: This command is non-interruptible and scans the entire keyspace which may cause + * performance issues. Consider {@link #scan(ScanOptions)} for large datasets. * * @param pattern must not be {@literal null}. * @return diff --git a/src/main/java/org/springframework/data/redis/core/ClusterOperations.java b/src/main/java/org/springframework/data/redis/core/ClusterOperations.java index f96c8c0d39..01ebb31246 100644 --- a/src/main/java/org/springframework/data/redis/core/ClusterOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ClusterOperations.java @@ -38,7 +38,10 @@ public interface ClusterOperations { /** - * Get all keys located at given node. + * Retrieve all keys located at given node matching the given pattern. + *

+ * IMPORTANT: The {@literal KEYS} command is non-interruptible and scans the entire keyspace which + * may cause performance issues. * * @param node must not be {@literal null}. * @param pattern diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java index 686277f0df..55f126b636 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java @@ -264,9 +264,10 @@ default Mono>> listenToPatternLater(String... Mono type(K key); /** - * Find all keys matching the given {@code pattern}.
- * IMPORTANT: It is recommended to use {@link #scan()} to iterate over the keyspace as - * {@link #keys(Object)} is a non-interruptible and expensive Redis operation. + * Retrieve all keys matching the given pattern via {@code KEYS} command. + *

+ * IMPORTANT: This command is non-interruptible and scans the entire keyspace which may cause + * performance issues. Consider {@link #scan(ScanOptions)} for large datasets. * * @param pattern must not be {@literal null}. * @return the {@link Flux} emitting matching keys one by one. diff --git a/src/main/java/org/springframework/data/redis/core/RedisOperations.java b/src/main/java/org/springframework/data/redis/core/RedisOperations.java index cadfa9c2de..18783d2c92 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/RedisOperations.java @@ -262,10 +262,10 @@ T execute(RedisScript script, RedisSerializer argsSerializer, RedisSer DataType type(K key); /** - * Retrieve keys matching the given pattern via {@code KEYS} command. + * Retrieve all keys matching the given pattern via {@code KEYS} command. *

- * Note: This command scans the entire keyspace and may cause performance issues - * in production environments. Prefer using {@link #scan(ScanOptions)} for large datasets. + * IMPORTANT: This command is non-interruptible and scans the entire keyspace which may cause + * performance issues. Consider {@link #scan(ScanOptions)} for large datasets. * * @param pattern key pattern * @return set of matching keys, or {@literal null} when used in pipeline / transaction From 65b64a6424e464c6f2e7430f116fd2350d9ea85c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 6 Jun 2025 08:43:39 +0200 Subject: [PATCH 03/10] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b780d1e99c..73fcb03e93 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 4.0.0-SNAPSHOT + 4.0.x-GH-3154-SNAPSHOT Spring Data Redis Spring Data module for Redis From 773a0b044f9e9001c5fa9c5fe9d772c32e441156 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 6 Jun 2025 14:42:41 +0200 Subject: [PATCH 04/10] jackson3 --- pom.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pom.xml b/pom.xml index 73fcb03e93..8b21be569e 100644 --- a/pom.xml +++ b/pom.xml @@ -148,6 +148,21 @@ true + + + tools.jackson.core + jackson-databind + 3.0.0-rc5 + true + + + + com.fasterxml.jackson.core + jackson-annotations + 3.0-rc5 + true + + commons-beanutils commons-beanutils From 463000964751c6d12f753168ecddc3eb0b95a393 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 6 Jun 2025 14:42:57 +0200 Subject: [PATCH 05/10] Hacking: Jackson3 HashMapper --- .../data/redis/hash/Jackson3HashMapper.java | 684 ++++++++++++++++++ ...Jackson3HashMapperFlatteningUnitTests.java | 63 ++ ...kson3HashMapperNonFlatteningUnitTests.java | 63 ++ .../mapping/Jackson3HashMapperUnitTests.java | 511 +++++++++++++ 4 files changed, 1321 insertions(+) create mode 100644 src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java create mode 100644 src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperFlatteningUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperNonFlatteningUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperUnitTests.java diff --git a/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java b/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java new file mode 100644 index 0000000000..282c8d4a83 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java @@ -0,0 +1,684 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.redis.hash; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.Version; +import tools.jackson.databind.DatabindContext; +import tools.jackson.databind.DefaultTyping; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.deser.jdk.JavaUtilCalendarDeserializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator.TypeMatcher; +import tools.jackson.databind.module.SimpleDeserializers; +import tools.jackson.databind.module.SimpleSerializers; +import tools.jackson.databind.ser.Serializers; +import tools.jackson.databind.ser.jdk.JavaUtilCalendarSerializer; +import tools.jackson.databind.ser.jdk.JavaUtilDateSerializer; + +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TimeZone; + +import org.springframework.data.mapping.MappingException; +import org.springframework.data.redis.support.collections.CollectionUtils; +import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +/** + * {@link ObjectMapper} based {@link HashMapper} implementation that allows flattening. Given an entity {@code Person} + * with an {@code Address} like below the flattening will create individual hash entries for all nested properties and + * resolve complex types into simple types, as far as possible. + *

+ * Flattening requires all property names to not interfere with JSON paths. Using dots or brackets in map keys or as + * property names is not supported using flattening. The resulting hash cannot be mapped back into an Object. + *

Example

+ * + *
+ * class Person {
+ * 	String firstname;
+ * 	String lastname;
+ * 	Address address;
+ * 	Date date;
+ * 	LocalDateTime localDateTime;
+ * }
+ *
+ * class Address {
+ * 	String city;
+ * 	String country;
+ * }
+ * 
+ * + *

Normal

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Hash fieldValue
firstnameJon
lastnameSnow
address{ "city" : "Castle Black", "country" : "The North" }
date1561543964015
localDateTime2018-01-02T12:13:14
+ *

Flat

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Hash fieldValue
firstnameJon
lastnameSnow
address.cityCastle Black
address.countryThe North
date1561543964015
localDateTime2018-01-02T12:13:14
+ * + * @author Christoph Strobl + * @author Mark Paluch + * @author John Blum + * @since 1.8 + */ +public class Jackson3HashMapper implements HashMapper { + + private static final boolean SOURCE_VERSION_PRESENT = ClassUtils.isPresent("javax.lang.model.SourceVersion", + Jackson3HashMapper.class.getClassLoader()); + + private final ObjectMapper typingMapper; + private final ObjectMapper untypedMapper; + private final boolean flatten; + + /** + * Creates new {@link Jackson3HashMapper} with a default {@link ObjectMapper}. + * + * @param flatten boolean used to configure whether JSON de/serialized {@link Object} properties will be un/flattened + * using {@literal dot notation}, or whether to retain the hierarchical node structure created by Jackson. + */ + public Jackson3HashMapper(boolean flatten) { + + this(JsonMapper.builder().findAndAddModules().addModules(new HashMapperModule()) + .activateDefaultTypingAsProperty(BasicPolymorphicTypeValidator.builder().allowIfBaseType(Object.class).allowIfSubType(new TypeMatcher() { + @Override + public boolean match(DatabindContext ctxt, Class clazz) { + return true; + } + }).build(), + DefaultTyping.NON_FINAL_AND_ENUMS, "@class") + .configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .changeDefaultPropertyInclusion(value -> value.withValueInclusion(Include.NON_NULL)).build(), false); + + //// this(new Supplier() { + // @Override + // public ObjectMapper get() { + //// return JsonMapper.builder() + //// .findAndAddModules() + //// .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + //// .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + //// .build(); + // return null; + // } + // }.get(), false); + // this(new ObjectMapper() { + // + // @Override + // protected DeserializationContextExt _deserializationContext(JsonParser p) { + // return super._deserializationContext(p); + // } + // + // @Override + // protected TypeResolverBuilder _constructDefaultTypeResolverBuilder(DefaultTyping applicability, + // PolymorphicTypeValidator typeValidator) { + // + // return new DefaultTypeResolverBuilder(applicability, typeValidator, ) { + // + // public boolean useForType(JavaType type) { + // + // if (type.isPrimitive()) { + // return false; + // } + // + // if (flatten && (type.isTypeOrSubTypeOf(Number.class) || type.isEnumType())) { + // return false; + // } + // + // if (EVERYTHING.equals(_appliesFor)) { + // return !TreeNode.class.isAssignableFrom(type.getRawClass()); + // } + // + // return super.useForType(type); + // } + // }; + // } + // }.findAndRegisterModules(), flatten); + // },flatten); + + // this.typingMapper.de.activateDefaultTyping(this.typingMapper.getPolymorphicTypeValidator(), + // DefaultTyping.EVERYTHING, As.PROPERTY); + // this.typingMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); + // + // if(flatten) { + // this.typingMapper.disable(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES); + // } + // + // // Prevent splitting time types into arrays. E + // this.typingMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + // this.typingMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + // this.typingMapper.setSerializationInclusion(Include.NON_NULL); + // this.typingMapper.registerModule(new HashMapperModule()); + } + + /** + * Creates new {@link Jackson3HashMapper} initialized with a custom Jackson {@link ObjectMapper}. + * + * @param mapper Jackson {@link ObjectMapper} used to de/serialize hashed {@link Object objects}; must not be + * {@literal null}. + * @param flatten boolean used to configure whether JSON de/serialized {@link Object} properties will be un/flattened + * using {@literal dot notation}, or whether to retain the hierarchical node structure created by Jackson. + */ + public Jackson3HashMapper(ObjectMapper mapper, boolean flatten) { + + Assert.notNull(mapper, "Mapper must not be null"); + + this.flatten = flatten; + this.typingMapper = mapper; + this.untypedMapper = new ObjectMapper(); + // this.untypedMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); + // this.untypedMapper.setSerializationInclusion(Include.NON_NULL); + // this.untypedMapper.findAndRegisterModules(); + } + + @Override + @SuppressWarnings("unchecked") + public Map toHash(Object source) { + + JsonNode tree = this.typingMapper.valueToTree(source); + + return this.flatten ? flattenMap(tree.properties()) : this.untypedMapper.convertValue(tree, Map.class); + } + + @Override + @SuppressWarnings("all") + public Object fromHash(Map hash) { + + try { + if (this.flatten) { + + Map unflattenedHash = doUnflatten(hash); + byte[] unflattenedHashedBytes = this.untypedMapper.writeValueAsBytes(unflattenedHash); + Object hashedObject = this.typingMapper.reader().forType(Object.class).readValue(unflattenedHashedBytes); + + return hashedObject; + } + + return this.typingMapper.treeToValue(this.untypedMapper.valueToTree(hash), Object.class); + + } catch (Exception ex) { + throw new MappingException(ex.getMessage(), ex); + } + } + + @SuppressWarnings("unchecked") + private Map doUnflatten(Map source) { + + Map result = new LinkedHashMap<>(); + Set treatSeparate = new LinkedHashSet<>(); + + for (Entry entry : source.entrySet()) { + + String key = entry.getKey(); + String[] keyParts = key.split("\\."); + + if (keyParts.length == 1 && isNotIndexed(keyParts[0])) { + result.put(key, entry.getValue()); + } else if (keyParts.length == 1 && isIndexed(keyParts[0])) { + + String indexedKeyName = keyParts[0]; + String nonIndexedKeyName = stripIndex(indexedKeyName); + + int index = getIndex(indexedKeyName); + + if (result.containsKey(nonIndexedKeyName)) { + addValueToTypedListAtIndex((List) result.get(nonIndexedKeyName), index, entry.getValue()); + } else { + result.put(nonIndexedKeyName, createTypedListWithValue(index, entry.getValue())); + } + } else { + treatSeparate.add(keyParts[0]); + } + } + + for (String partial : treatSeparate) { + + Map newSource = new LinkedHashMap<>(); + + // Copies all nested, dot properties from the source Map to the new Map beginning from + // the next nested (dot) property + for (Entry entry : source.entrySet()) { + String key = entry.getKey(); + if (key.startsWith(partial)) { + String keyAfterDot = key.substring(partial.length() + 1); + newSource.put(keyAfterDot, entry.getValue()); + } + } + + if (isNonNestedIndexed(partial)) { + + String nonIndexPartial = stripIndex(partial); + int index = getIndex(partial); + + if (result.containsKey(nonIndexPartial)) { + addValueToTypedListAtIndex((List) result.get(nonIndexPartial), index, doUnflatten(newSource)); + } else { + result.put(nonIndexPartial, createTypedListWithValue(index, doUnflatten(newSource))); + } + } else { + result.put(partial, doUnflatten(newSource)); + } + } + + return result; + } + + private boolean isIndexed(@NonNull String value) { + return value.indexOf('[') > -1; + } + + private boolean isNotIndexed(@NonNull String value) { + return !isIndexed(value); + } + + private boolean isNonNestedIndexed(@NonNull String value) { + return value.endsWith("]"); + } + + private int getIndex(@NonNull String indexedValue) { + return Integer.parseInt(indexedValue.substring(indexedValue.indexOf('[') + 1, indexedValue.length() - 1)); + } + + private @NonNull String stripIndex(@NonNull String indexedValue) { + + int indexOfLeftBracket = indexedValue.indexOf("["); + + return indexOfLeftBracket > -1 ? indexedValue.substring(0, indexOfLeftBracket) : indexedValue; + } + + private Map flattenMap(Set> source) { + + Map resultMap = new HashMap<>(); + doFlatten("", source, resultMap); + return resultMap; + } + + private void doFlatten(String propertyPrefix, Set> inputMap, Map resultMap) { + + if (StringUtils.hasText(propertyPrefix)) { + propertyPrefix = propertyPrefix + "."; + } + + Iterator> entries = inputMap.iterator(); + while (entries.hasNext()) { + Entry entry = entries.next(); + flattenElement(propertyPrefix + entry.getKey(), entry.getValue(), resultMap); + } + } + + private void flattenElement(String propertyPrefix, Object source, Map resultMap) { + + if (!(source instanceof JsonNode element)) { + resultMap.put(propertyPrefix, source); + return; + } + + if (element.isArray()) { + + Iterator nodes = element.values().iterator(); + + while (nodes.hasNext()) { + + JsonNode currentNode = nodes.next(); + + if (currentNode.isArray()) { + flattenCollection(propertyPrefix, currentNode.values().iterator(), resultMap); + } else if (nodes.hasNext() && mightBeJavaType(currentNode)) { + + JsonNode next = nodes.next(); + + if (next.isArray()) { + flattenCollection(propertyPrefix, next.values().iterator(), resultMap); + } + if (currentNode.asText().equals("java.util.Date")) { + resultMap.put(propertyPrefix, next.asText()); + break; + } + if (next.isNumber()) { + resultMap.put(propertyPrefix, next.numberValue()); + break; + } + if (next.isTextual()) { + resultMap.put(propertyPrefix, next.textValue()); + break; + } + if (next.isBoolean()) { + resultMap.put(propertyPrefix, next.booleanValue()); + break; + } + if (next.isBinary()) { + + try { + resultMap.put(propertyPrefix, next.binaryValue()); + } catch (Exception ex) { + throw new IllegalStateException("Cannot read binary value '%s'".formatted(propertyPrefix), ex); + } + + break; + } + } + } + } else if (element.isObject()) { + doFlatten(propertyPrefix, element.properties(), resultMap); + } else { + + switch (element.getNodeType()) { + case STRING -> resultMap.put(propertyPrefix, element.textValue()); + case NUMBER -> resultMap.put(propertyPrefix, element.numberValue()); + case BOOLEAN -> resultMap.put(propertyPrefix, element.booleanValue()); + case BINARY -> { + try { + resultMap.put(propertyPrefix, element.binaryValue()); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + default -> + resultMap.put(propertyPrefix, new DirectFieldAccessFallbackBeanWrapper(element).getPropertyValue("_value")); + } + } + } + + private boolean mightBeJavaType(JsonNode node) { + + String textValue = node.asText(); + + if (!SOURCE_VERSION_PRESENT) { + return Arrays.asList("java.util.Date", "java.math.BigInteger", "java.math.BigDecimal").contains(textValue); + } + + return javax.lang.model.SourceVersion.isName(textValue); + } + + private void flattenCollection(String propertyPrefix, Iterator list, Map resultMap) { + + for (int counter = 0; list.hasNext(); counter++) { + JsonNode element = list.next(); + flattenElement(propertyPrefix + "[" + counter + "]", element, resultMap); + } + } + + @SuppressWarnings("unchecked") + private void addValueToTypedListAtIndex(List listWithTypeHint, int index, Object value) { + + List valueList = (List) listWithTypeHint.get(1); + + if (index >= valueList.size()) { + int initialCapacity = index + 1; + List newValueList = new ArrayList<>(initialCapacity); + Collections.copy(CollectionUtils.initializeList(newValueList, initialCapacity), valueList); + listWithTypeHint.set(1, newValueList); + valueList = newValueList; + } + + valueList.set(index, value); + } + + private List createTypedListWithValue(int index, Object value) { + + int initialCapacity = index + 1; + + List valueList = CollectionUtils.initializeList(new ArrayList<>(initialCapacity), initialCapacity); + valueList.set(index, value); + + List listWithTypeHint = new ArrayList<>(); + listWithTypeHint.add(ArrayList.class.getName()); + listWithTypeHint.add(valueList); + + return listWithTypeHint; + } + + private static class HashMapperModule extends JacksonModule { + + @Override + public String getModuleName() { + return "spring-data-hash-mapper-module"; + } + + @Override + public Version version() { + return new Version(4, 0, 0, null, "org.springframework.data", "spring-data-redis"); + } + + @Override + public void setupModule(SetupContext context) { + + List> valueSerializers = new ArrayList<>(); + valueSerializers.add(new JavaUtilDateSerializer(true, null)); + valueSerializers.add(new UTCCalendarSerializer()); + + Serializers serializers = new SimpleSerializers(valueSerializers); + context.addSerializers(serializers); + + Map, ValueDeserializer> valueDeserializers = new LinkedHashMap<>(); + valueDeserializers.put(GregorianCalendar.class, new UTCCalendarDeserializer()); + + context.addDeserializers(new SimpleDeserializers(valueDeserializers)); + } + } + + static class UTCCalendarSerializer extends JavaUtilCalendarSerializer { + + @Override + public void serialize(Calendar value, JsonGenerator g, SerializationContext provider) throws JacksonException { + + Calendar utc = Calendar.getInstance(); + utc.setTimeInMillis(value.getTimeInMillis()); + utc.setTimeZone(TimeZone.getTimeZone("UTC")); + super.serialize(utc, g, provider); + } + } + + static class UTCCalendarDeserializer extends JavaUtilCalendarDeserializer { + @Override + public Calendar deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + + Calendar cal = super.deserialize(p, ctxt); + + Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + utc.setTimeInMillis(cal.getTimeInMillis()); + utc.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); + + return utc; + } + } + + // /** + // * {@link JsonDeserializer} for {@link Date} objects without considering type hints. + // */ + // private static class UntypedDateDeserializer extends JsonDeserializer { + // + // private final JsonDeserializer delegate = new UntypedObjectDeserializer(null, null); + // + // @Override + // public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) + // throws IOException { + // return deserialize(p, ctxt); + // } + // + // @Override + // public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + // + // Object value = delegate.deserialize(p, ctxt); + // + // if (value instanceof Date) { + // return (Date) value; + // } + // + // try { + // return ctxt.getConfig().getDateFormat().parse(value.toString()); + // } catch (ParseException ignore) { + // return new Date(NumberUtils.parseNumber(value.toString(), Long.class)); + // } + // } + // } + // + // /** + // * {@link JsonDeserializer} for {@link Calendar} objects without considering type hints. + // */ + // private static class UntypedCalendarDeserializer extends JsonDeserializer { + // + // private final UntypedDateDeserializer dateDeserializer = new UntypedDateDeserializer(); + // + // @Override + // public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) + // throws IOException { + // return deserialize(p, ctxt); + // } + // + // @Override + // public Calendar deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + // + // Date date = dateDeserializer.deserialize(p, ctxt); + // + // if (date != null) { + // Calendar calendar = Calendar.getInstance(); + // calendar.setTime(date); + // return calendar; + // } + // + // return null; + // } + // } + // + // /** + // * Untyped {@link JsonSerializer} to serialize plain values without writing JSON type hints. + // * + // * @param + // */ + // private static class UntypedSerializer extends JsonSerializer { + // + // private final JsonSerializer delegate; + // + // UntypedSerializer(JsonSerializer delegate) { + // this.delegate = delegate; + // } + // + // @Override + // public void serializeWithType(T value, JsonGenerator jsonGenerator, SerializerProvider serializers, + // TypeSerializer typeSerializer) throws IOException { + // + // serialize(value, jsonGenerator, serializers); + // } + // + // @Override + // public void serialize(@Nullable T value, JsonGenerator jsonGenerator, SerializerProvider serializers) + // throws IOException { + // + // if (value != null) { + // delegate.serialize(value, jsonGenerator, serializers); + // } else { + // serializers.defaultSerializeNull(jsonGenerator); + // } + // } + // } + // + // private static class DateToTimestampSerializer extends DateSerializer { + // + // // Prevent splitting to array. + // @Override + // protected boolean _asTimestamp(SerializerProvider serializers) { + // return true; + // } + // } + // + // private static class CalendarToTimestampSerializer extends CalendarSerializer { + // + // // Prevent splitting to array. + // @Override + // protected boolean _asTimestamp(SerializerProvider serializers) { + // return true; + // } + // } +} diff --git a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperFlatteningUnitTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperFlatteningUnitTests.java new file mode 100644 index 0000000000..f11ef10ed9 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperFlatteningUnitTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.redis.mapping; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDateTime; +import java.time.Month; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.hash.Jackson3HashMapper; + +/** + * @author Christoph Strobl + * @author John Blum + * @since 2023/06 + */ +public class Jackson3HashMapperFlatteningUnitTests extends Jackson3HashMapperUnitTests { + + Jackson3HashMapperFlatteningUnitTests() { + super(new Jackson3HashMapper(true)); + } + + @Test // GH-2593 + void timestampHandledCorrectly() { + + Map hash = Map.of("@class", Session.class.getName(), "lastAccessed", "2023-06-05T18:36:30"); + + // Map hash = Map.of("lastAccessed", "2023-06-05T18:36:30"); + + Session session = (Session) getMapper().fromHash(hash); + + assertThat(session).isNotNull(); + assertThat(session.lastAccessed).isEqualTo(LocalDateTime.of(2023, Month.JUNE, 5, 18, 36, 30)); + } + + private static class Session { + + private LocalDateTime lastAccessed; + + public LocalDateTime getLastAccessed() { + return lastAccessed; + } + + public void setLastAccessed(LocalDateTime lastAccessed) { + this.lastAccessed = lastAccessed; + } + } +} diff --git a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperNonFlatteningUnitTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperNonFlatteningUnitTests.java new file mode 100644 index 0000000000..791b6fe609 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperNonFlatteningUnitTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.redis.mapping; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDateTime; +import java.time.Month; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.hash.Jackson3HashMapper; + +/** + * @author Christoph Strobl + * @author John Blum + * @since 2023/06 + */ +public class Jackson3HashMapperNonFlatteningUnitTests extends Jackson3HashMapperUnitTests { + + Jackson3HashMapperNonFlatteningUnitTests() { + super(new Jackson3HashMapper(false)); + } + + @Test // GH-2593 + void timestampHandledCorrectly() { + + Session source = new Session(); + source.lastAccessed = LocalDateTime.of(2023, Month.JUNE, 5, 18, 36, 30); + + Map hash = getMapper().toHash(source); + Session session = (Session) getMapper().fromHash(hash); + + assertThat(session).isNotNull(); + assertThat(session.lastAccessed).isEqualTo(LocalDateTime.of(2023, Month.JUNE, 5, 18, 36, 30)); + } + + private static class Session { + + private LocalDateTime lastAccessed; + + public LocalDateTime getLastAccessed() { + return lastAccessed; + } + + public void setLastAccessed(LocalDateTime lastAccessed) { + this.lastAccessed = lastAccessed; + } + } +} diff --git a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperUnitTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperUnitTests.java new file mode 100644 index 0000000000..63f237ade3 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperUnitTests.java @@ -0,0 +1,511 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.redis.mapping; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.Address; +import org.springframework.data.redis.Person; +import org.springframework.data.redis.hash.HashMapper; +import org.springframework.data.redis.hash.Jackson2HashMapper; +import org.springframework.data.redis.hash.Jackson3HashMapper; + +/** + * Unit tests for {@link Jackson2HashMapper}. + * + * @author Christoph Strobl + * @author Mark Paluch + * @author John Blum + */ +public abstract class Jackson3HashMapperUnitTests extends AbstractHashMapperTests { + + private final Jackson3HashMapper mapper; + + public Jackson3HashMapperUnitTests(Jackson3HashMapper mapper) { + this.mapper = mapper; + } + + protected Jackson3HashMapper getMapper() { + return this.mapper; + } + + @Override + @SuppressWarnings("rawtypes") + protected HashMapper mapperFor(Class t) { + return getMapper(); + } + + @Test // DATAREDIS-423 + void shouldMapTypedListOfSimpleType() { + + WithList source = new WithList(); + source.strings = Arrays.asList("spring", "data", "redis"); + assertBackAndForwardMapping(source); + } + + @Test // DATAREDIS-423 + void shouldMapTypedListOfComplexType() { + + WithList source = new WithList(); + + source.persons = Arrays.asList(new Person("jon", "snow", 19), new Person("tyrion", "lannister", 27)); + assertBackAndForwardMapping(source); + } + + @Test // DATAREDIS-423 + void shouldMapTypedListOfComplexObjectWihtNestedElements() { + + WithList source = new WithList(); + + Person jon = new Person("jon", "snow", 19); + Address adr = new Address(); + adr.setStreet("the wall"); + adr.setNumber(100); + jon.setAddress(adr); + + source.persons = Arrays.asList(jon, new Person("tyrion", "lannister", 27)); + assertBackAndForwardMapping(source); + } + + @Test // DATAREDIS-423 + void shouldMapNestedObject() { + + Person jon = new Person("jon", "snow", 19); + Address adr = new Address(); + adr.setStreet("the wall"); + adr.setNumber(100); + jon.setAddress(adr); + + assertBackAndForwardMapping(jon); + } + + @Test // DATAREDIS-423 + void shouldMapUntypedList() { + + WithList source = new WithList(); + source.objects = Arrays.asList(100, "foo", new Person("jon", "snow", 19)); + assertBackAndForwardMapping(source); + } + + @Test // DATAREDIS-423 + void shouldMapTypedMapOfSimpleTypes() { + + WithMap source = new WithMap(); + source.strings = new LinkedHashMap<>(); + source.strings.put("1", "spring"); + source.strings.put("2", "data"); + source.strings.put("3", "redis"); + assertBackAndForwardMapping(source); + } + + @Test // DATAREDIS-423 + void shouldMapTypedMapOfComplexTypes() { + + WithMap source = new WithMap(); + source.persons = new LinkedHashMap<>(); + source.persons.put("1", new Person("jon", "snow", 19)); + source.persons.put("2", new Person("tyrion", "lannister", 19)); + assertBackAndForwardMapping(source); + } + + @Test // DATAREDIS-423 + void shouldMapUntypedMap() { + + WithMap source = new WithMap(); + source.objects = new LinkedHashMap<>(); + source.objects.put("1", "spring"); + source.objects.put("2", 100); + source.objects.put("3", "redis"); + assertBackAndForwardMapping(source); + } + + @Test // DATAREDIS-423 + void nestedStuff() { + + WithList nestedList = new WithList(); + nestedList.objects = new ArrayList<>(); + + WithMap deepNestedMap = new WithMap(); + deepNestedMap.persons = new LinkedHashMap<>(); + deepNestedMap.persons.put("jon", new Person("jon", "snow", 24)); + + nestedList.objects.add(deepNestedMap); + + WithMap outer = new WithMap(); + outer.objects = new LinkedHashMap<>(); + outer.objects.put("1", nestedList); + + assertBackAndForwardMapping(outer); + } + + @Test // DATAREDIS-1001 + void dateValueShouldBeTreatedCorrectly() { + + WithDates source = new WithDates(); + source.string = "id-1"; + source.date = new Date(1561543964015L); + source.calendar = Calendar.getInstance(); + source.localDate = LocalDate.parse("2018-01-02"); + source.localDateTime = LocalDateTime.parse("2018-01-02T12:13:14"); + + assertBackAndForwardMapping(source); + } + + @Test // GH-1566 + @Disabled("Jackson removed default typing for final types") + void mapFinalClass() { + + MeFinal source = new MeFinal(); + source.value = "id-1"; + + assertBackAndForwardMapping(source); + } + + @Test // GH-2365 + void bigIntegerShouldBeTreatedCorrectly() { + + WithBigWhatever source = new WithBigWhatever(); + source.bigI = BigInteger.TEN; + + assertBackAndForwardMapping(source); + } + + @Test // GH-2365 + void bigDecimalShouldBeTreatedCorrectly() { + + WithBigWhatever source = new WithBigWhatever(); + source.bigD = BigDecimal.ONE; + + assertBackAndForwardMapping(source); + } + + @Test // GH-2979 + void enumsShouldBeTreatedCorrectly() { + + WithEnumValue source = new WithEnumValue(); + source.value = SpringDataEnum.REDIS; + + assertBackAndForwardMapping(source); + } + + public static class WithList { + + List strings; + List objects; + List persons; + + public List getStrings() { + return this.strings; + } + + public void setStrings(List strings) { + this.strings = strings; + } + + public List getObjects() { + return this.objects; + } + + public void setObjects(List objects) { + this.objects = objects; + } + + public List getPersons() { + return this.persons; + } + + public void setPersons(List persons) { + this.persons = persons; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof WithList that)) { + return false; + } + + return Objects.equals(this.getObjects(), that.getObjects()) + && Objects.equals(this.getPersons(), that.getPersons()) + && Objects.equals(this.getStrings(), that.getStrings()); + } + + @Override + public int hashCode() { + return Objects.hash(getObjects(), getPersons(), getStrings()); + } + } + + public static class WithMap { + + Map strings; + Map objects; + Map persons; + + public Map getStrings() { + return this.strings; + } + + public void setStrings(Map strings) { + this.strings = strings; + } + + public Map getObjects() { + return this.objects; + } + + public void setObjects(Map objects) { + this.objects = objects; + } + + public Map getPersons() { + return this.persons; + } + + public void setPersons(Map persons) { + this.persons = persons; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof WithMap that)) { + return false; + } + + return Objects.equals(this.getObjects(), that.getObjects()) + && Objects.equals(this.getPersons(), that.getPersons()) + && Objects.equals(this.getStrings(), that.getStrings()); + } + + @Override + public int hashCode() { + return Objects.hash(getObjects(), getPersons(), getStrings()); + } + } + + private static class WithDates { + + private String string; + private Date date; + private Calendar calendar; + private LocalDate localDate; + private LocalDateTime localDateTime; + + public String getString() { + return this.string; + } + + public void setString(String string) { + this.string = string; + } + + public Date getDate() { + return this.date; + } + + public void setDate(Date date) { + this.date = date; + } + + public Calendar getCalendar() { + return this.calendar; + } + + public void setCalendar(Calendar calendar) { + this.calendar = calendar; + } + + public LocalDate getLocalDate() { + return this.localDate; + } + + public void setLocalDate(LocalDate localDate) { + this.localDate = localDate; + } + + public LocalDateTime getLocalDateTime() { + return this.localDateTime; + } + + public void setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof WithDates that)) { + return false; + } + + return Objects.equals(this.getString(), that.getString()) + && Objects.equals(this.getCalendar(), that.getCalendar()) + && Objects.equals(this.getDate(), that.getDate()) + && Objects.equals(this.getLocalDate(), that.getLocalDate()) + && Objects.equals(this.getLocalDateTime(), that.getLocalDateTime()); + } + + @Override + public int hashCode() { + return Objects.hash(getString(), getCalendar(), getDate(), getLocalDate(), getLocalDateTime()); + } + + @Override + public String toString() { + return "WithDates{" + + "string='" + string + '\'' + + ", date=" + date + + ", calendar=" + calendar + + ", localDate=" + localDate + + ", localDateTime=" + localDateTime + + '}'; + } + } + + private static class WithBigWhatever { + + private BigDecimal bigD; + private BigInteger bigI; + + public BigDecimal getBigD() { + return this.bigD; + } + + public void setBigD(BigDecimal bigD) { + this.bigD = bigD; + } + + public BigInteger getBigI() { + return this.bigI; + } + + public void setBigI(BigInteger bigI) { + this.bigI = bigI; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof WithBigWhatever that)) { + return false; + } + + return Objects.equals(this.getBigD(), that.getBigD()) + && Objects.equals(this.getBigI(), that.getBigI()); + } + + @Override + public int hashCode() { + return Objects.hash(getBigD(), getBigI()); + } + } + + public static final class MeFinal { + + private String value; + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof MeFinal that)) { + return false; + } + + return Objects.equals(this.getValue(), that.getValue()); + } + + @Override + public int hashCode() { + return Objects.hash(getValue()); + } + } + + enum SpringDataEnum { + COMMONS, REDIS + } + + static class WithEnumValue { + + SpringDataEnum value; + + public SpringDataEnum getValue() { + return value; + } + + public void setValue(SpringDataEnum value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + WithEnumValue that = (WithEnumValue) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + } +} From 822d2700f50fb91018403e0313b7b0eb01e7e1c5 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 10 Jun 2025 08:53:28 +0200 Subject: [PATCH 06/10] Hacking: Jackson Serializers --- .../Jackson3JsonRedisSerializer.java | 198 ++++++++++++++++++ .../serializer/Jackson3ObjectReader.java | 57 +++++ .../serializer/Jackson3ObjectWriter.java | 54 +++++ .../core/AbstractOperationsTestParams.java | 8 + .../core/ReactiveOperationsTestParams.java | 6 + .../collections/CollectionTestParams.java | 15 ++ .../RedisPropertiesIntegrationTests.java | 11 + 7 files changed, 349 insertions(+) create mode 100644 src/main/java/org/springframework/data/redis/serializer/Jackson3JsonRedisSerializer.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/Jackson3ObjectReader.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/Jackson3ObjectWriter.java diff --git a/src/main/java/org/springframework/data/redis/serializer/Jackson3JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/Jackson3JsonRedisSerializer.java new file mode 100644 index 0000000000..15cd981176 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/Jackson3JsonRedisSerializer.java @@ -0,0 +1,198 @@ +/* + * Copyright 2011-2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.redis.serializer; + +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ser.SerializerFactory; +import tools.jackson.databind.type.TypeFactory; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link RedisSerializer} that can read and write JSON using + * Jackson's and + * Jackson Databind {@link ObjectMapper}. + *

+ * This serializer can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances. + * Note:Null objects are serialized as empty arrays and vice versa. + *

+ * JSON reading and writing can be customized by configuring {@link JacksonObjectReader} respective + * {@link JacksonObjectWriter}. + * + * @author Thomas Darimont + * @author Mark Paluch + * @since 1.2 + */ +public class Jackson3JsonRedisSerializer implements RedisSerializer { + + /** + * @deprecated since 3.0 for removal. + */ + @Deprecated(since = "3.0", forRemoval = true) // + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + private final JavaType javaType; + + private ObjectMapper mapper; + + private final Jackson3ObjectReader reader; + + private final Jackson3ObjectWriter writer; + + /** + * Creates a new {@link Jackson3JsonRedisSerializer} for the given target {@link Class}. + * + * @param type must not be {@literal null}. + */ + public Jackson3JsonRedisSerializer(Class type) { + this(new ObjectMapper(), type); + } + + /** + * Creates a new {@link Jackson3JsonRedisSerializer} for the given target {@link JavaType}. + * + * @param javaType must not be {@literal null}. + */ + public Jackson3JsonRedisSerializer(JavaType javaType) { + this(new ObjectMapper(), javaType); + } + + /** + * Creates a new {@link Jackson3JsonRedisSerializer} for the given target {@link Class}. + * + * @param mapper must not be {@literal null}. + * @param type must not be {@literal null}. + * @since 3.0 + */ + public Jackson3JsonRedisSerializer(ObjectMapper mapper, Class type) { + + Assert.notNull(mapper, "ObjectMapper must not be null"); + Assert.notNull(type, "Java type must not be null"); + + this.javaType = getJavaType(type); + this.mapper = mapper; + this.reader = Jackson3ObjectReader.create(); + this.writer = Jackson3ObjectWriter.create(); + } + + /** + * Creates a new {@link Jackson3JsonRedisSerializer} for the given target {@link JavaType}. + * + * @param mapper must not be {@literal null}. + * @param javaType must not be {@literal null}. + * @since 3.0 + */ + public Jackson3JsonRedisSerializer(ObjectMapper mapper, JavaType javaType) { + this(mapper, javaType, Jackson3ObjectReader.create(), Jackson3ObjectWriter.create()); + } + + /** + * Creates a new {@link Jackson3JsonRedisSerializer} for the given target {@link JavaType}. + * + * @param mapper must not be {@literal null}. + * @param javaType must not be {@literal null}. + * @param reader the {@link JacksonObjectReader} function to read objects using {@link ObjectMapper}. + * @param writer the {@link JacksonObjectWriter} function to write objects using {@link ObjectMapper}. + * @since 3.0 + */ + public Jackson3JsonRedisSerializer(ObjectMapper mapper, JavaType javaType, Jackson3ObjectReader reader, + Jackson3ObjectWriter writer) { + + Assert.notNull(mapper, "ObjectMapper must not be null!"); + Assert.notNull(reader, "Reader must not be null!"); + Assert.notNull(writer, "Writer must not be null!"); + + this.mapper = mapper; + this.reader = reader; + this.writer = writer; + this.javaType = javaType; + } + + /** + * Sets the {@code ObjectMapper} for this view. If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} + * is used. + *

+ * Setting a custom-configured {@code ObjectMapper} is one way to take further control of the JSON serialization + * process. For example, an extended {@link SerializerFactory} can be configured that provides custom serializers for + * specific types. The other option for refining the serialization process is to use Jackson's provided annotations on + * the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary. + * + * @deprecated since 3.0, use {@link #Jackson3JsonRedisSerializer(ObjectMapper, Class) constructor creation} to + * configure the object mapper. + */ + @Deprecated(since = "3.0", forRemoval = true) + public void setObjectMapper(ObjectMapper mapper) { + + Assert.notNull(mapper, "'objectMapper' must not be null"); + this.mapper = mapper; + } + + @Override + public byte[] serialize(@Nullable T value) throws SerializationException { + + if (value == null) { + return SerializationUtils.EMPTY_ARRAY; + } + try { + return this.writer.write(this.mapper, value); + } catch (Exception ex) { + throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex); + } + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public T deserialize(@Nullable byte[] bytes) throws SerializationException { + + if (SerializationUtils.isEmpty(bytes)) { + return null; + } + try { + return (T) this.reader.read(this.mapper, bytes, javaType); + } catch (Exception ex) { + throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex); + } + } + + /** + * Returns the Jackson {@link JavaType} for the specific class. + *

+ * Default implementation returns {@link TypeFactory#constructType(java.lang.reflect.Type)}, but this can be + * overridden in subclasses, to allow for custom generic collection handling. For instance: + * + *

+	 * protected JavaType getJavaType(Class<?> clazz) {
+	 * 	if (List.class.isAssignableFrom(clazz)) {
+	 * 		return TypeFactory.defaultInstance().constructCollectionType(ArrayList.class, MyBean.class);
+	 * 	} else {
+	 * 		return super.getJavaType(clazz);
+	 * 	}
+	 * }
+	 * 
+ * + * @param clazz the class to return the java type for + * @return the java type + */ + protected JavaType getJavaType(Class clazz) { + return TypeFactory.unsafeSimpleType(clazz); + } +} diff --git a/src/main/java/org/springframework/data/redis/serializer/Jackson3ObjectReader.java b/src/main/java/org/springframework/data/redis/serializer/Jackson3ObjectReader.java new file mode 100644 index 0000000000..ee8b880194 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/Jackson3ObjectReader.java @@ -0,0 +1,57 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.redis.serializer; + +import java.io.IOException; +import java.io.InputStream; + +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ObjectMapper; + +/** + * Defines the contract for Object Mapping readers. Implementations of this interface can deserialize a given byte array + * holding JSON to an Object considering the target type. + *

+ * Reader functions can customize how the actual JSON is being deserialized by e.g. obtaining a customized + * {@link com.fasterxml.jackson.databind.ObjectReader} applying serialization features, date formats, or views. + * + * @author Mark Paluch + * @since 3.0 + */ +@FunctionalInterface +public interface Jackson3ObjectReader { + + /** + * Read an object graph from the given root JSON into a Java object considering the {@link JavaType}. + * + * @param mapper the object mapper to use. + * @param source the JSON to deserialize. + * @param type the Java target type + * @return the deserialized Java object. + * @throws IOException if an I/O error or JSON deserialization error occurs. + */ + Object read(ObjectMapper mapper, byte[] source, JavaType type) throws IOException; + + /** + * Create a default {@link Jackson3ObjectReader} delegating to {@link ObjectMapper#readValue(InputStream, JavaType)}. + * + * @return the default {@link Jackson3ObjectReader}. + */ + static Jackson3ObjectReader create() { + return (mapper, source, type) -> mapper.readValue(source, 0, source.length, type); + } + +} diff --git a/src/main/java/org/springframework/data/redis/serializer/Jackson3ObjectWriter.java b/src/main/java/org/springframework/data/redis/serializer/Jackson3ObjectWriter.java new file mode 100644 index 0000000000..f995bcc123 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/Jackson3ObjectWriter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2022-2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.redis.serializer; + +import java.io.IOException; + +import tools.jackson.databind.ObjectMapper; + +/** + * Defines the contract for Object Mapping writers. Implementations of this interface can serialize a given Object to a + * {@code byte[]} containing JSON. + *

+ * Writer functions can customize how the actual JSON is being written by e.g. obtaining a customized + * {@link com.fasterxml.jackson.databind.ObjectWriter} applying serialization features, date formats, or views. + * + * @author Mark Paluch + * @since 3.0 + */ +@FunctionalInterface +public interface Jackson3ObjectWriter { + + /** + * Write the object graph with the given root {@code source} as byte array. + * + * @param mapper the object mapper to use. + * @param source the root of the object graph to marshal. + * @return a byte array containing the serialized object graph. + * @throws IOException if an I/O error or JSON serialization error occurs. + */ + byte[] write(ObjectMapper mapper, Object source) throws IOException; + + /** + * Create a default {@link Jackson3ObjectWriter} delegating to {@link ObjectMapper#writeValueAsBytes(Object)}. + * + * @return the default {@link Jackson3ObjectWriter}. + */ + static Jackson3ObjectWriter create() { + return ObjectMapper::writeValueAsBytes; + } + +} diff --git a/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java b/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java index 485b6b1414..91fb53ec80 100644 --- a/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java +++ b/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java @@ -34,6 +34,7 @@ import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.GenericToStringSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.Jackson3JsonRedisSerializer; import org.springframework.data.redis.serializer.OxmSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.test.XstreamOxmSerializerSingleton; @@ -109,6 +110,12 @@ public static Collection testParams(RedisConnectionFactory connectionF jackson2JsonPersonTemplate.setValueSerializer(jackson2JsonSerializer); jackson2JsonPersonTemplate.afterPropertiesSet(); + Jackson3JsonRedisSerializer jackson3JsonSerializer = new Jackson3JsonRedisSerializer<>(Person.class); + RedisTemplate jackson3JsonPersonTemplate = new RedisTemplate<>(); + jackson3JsonPersonTemplate.setConnectionFactory(connectionFactory); + jackson3JsonPersonTemplate.setValueSerializer(jackson3JsonSerializer); + jackson3JsonPersonTemplate.afterPropertiesSet(); + GenericJackson2JsonRedisSerializer genericJackson2JsonSerializer = new GenericJackson2JsonRedisSerializer(); RedisTemplate genericJackson2JsonPersonTemplate = new RedisTemplate<>(); genericJackson2JsonPersonTemplate.setConnectionFactory(connectionFactory); @@ -124,6 +131,7 @@ public static Collection testParams(RedisConnectionFactory connectionF { xstreamStringTemplate, stringFactory, stringFactory }, // { xstreamPersonTemplate, stringFactory, personFactory }, // { jackson2JsonPersonTemplate, stringFactory, personFactory }, // + { jackson3JsonPersonTemplate, stringFactory, personFactory }, // { genericJackson2JsonPersonTemplate, stringFactory, personFactory } }); } } diff --git a/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java b/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java index 77895e3bb2..250d7975e8 100644 --- a/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java +++ b/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java @@ -34,6 +34,7 @@ import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.GenericToStringSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.Jackson3JsonRedisSerializer; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.OxmSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; @@ -100,6 +101,10 @@ RedisSerializationContext. newSerializationContext(jdkSerializat ReactiveRedisTemplate jackson2JsonPersonTemplate = new ReactiveRedisTemplate( lettuceConnectionFactory, RedisSerializationContext.fromSerializer(jackson2JsonSerializer)); + Jackson3JsonRedisSerializer jackson3JsonSerializer = new Jackson3JsonRedisSerializer<>(Person.class); + ReactiveRedisTemplate jackson3JsonPersonTemplate = new ReactiveRedisTemplate( + lettuceConnectionFactory, RedisSerializationContext.fromSerializer(jackson3JsonSerializer)); + GenericJackson2JsonRedisSerializer genericJackson2JsonSerializer = new GenericJackson2JsonRedisSerializer(); ReactiveRedisTemplate genericJackson2JsonPersonTemplate = new ReactiveRedisTemplate( lettuceConnectionFactory, RedisSerializationContext.fromSerializer(genericJackson2JsonSerializer)); @@ -115,6 +120,7 @@ RedisSerializationContext. newSerializationContext(jdkSerializat new Fixture<>(xstreamStringTemplate, stringFactory, stringFactory, oxmSerializer, "String/OXM"), // new Fixture<>(xstreamPersonTemplate, stringFactory, personFactory, oxmSerializer, "String/Person/OXM"), // new Fixture<>(jackson2JsonPersonTemplate, stringFactory, personFactory, jackson2JsonSerializer, "Jackson2"), // + new Fixture<>(jackson3JsonPersonTemplate, stringFactory, personFactory, jackson2JsonSerializer, "Jackson3"), // new Fixture<>(genericJackson2JsonPersonTemplate, stringFactory, personFactory, genericJackson2JsonSerializer, "Generic Jackson 2")); diff --git a/src/test/java/org/springframework/data/redis/support/collections/CollectionTestParams.java b/src/test/java/org/springframework/data/redis/support/collections/CollectionTestParams.java index 8b9063497f..f43926aeed 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/CollectionTestParams.java +++ b/src/test/java/org/springframework/data/redis/support/collections/CollectionTestParams.java @@ -31,6 +31,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.Jackson3JsonRedisSerializer; import org.springframework.data.redis.serializer.OxmSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.test.XstreamOxmSerializerSingleton; @@ -48,6 +49,7 @@ public static Collection testParams() { OxmSerializer serializer = XstreamOxmSerializerSingleton.getInstance(); Jackson2JsonRedisSerializer jackson2JsonSerializer = new Jackson2JsonRedisSerializer<>(Person.class); + Jackson3JsonRedisSerializer jackson3JsonSerializer = new Jackson3JsonRedisSerializer<>(Person.class); StringRedisSerializer stringSerializer = StringRedisSerializer.UTF_8; // create Jedis Factory @@ -86,6 +88,12 @@ public static Collection testParams() { rawTemplate.setKeySerializer(stringSerializer); rawTemplate.afterPropertiesSet(); + // jackson3 + RedisTemplate jackson3JsonPersonTemplate = new RedisTemplate<>(); + jackson3JsonPersonTemplate.setConnectionFactory(jedisConnFactory); + jackson3JsonPersonTemplate.setValueSerializer(jackson3JsonSerializer); + jackson3JsonPersonTemplate.afterPropertiesSet(); + // Lettuce LettuceConnectionFactory lettuceConnFactory = LettuceConnectionFactoryExtension .getConnectionFactory(RedisStanalone.class); @@ -110,6 +118,11 @@ public static Collection testParams() { jackson2JsonPersonTemplateLtc.setConnectionFactory(lettuceConnFactory); jackson2JsonPersonTemplateLtc.afterPropertiesSet(); + RedisTemplate jackson3JsonPersonTemplateLtc = new RedisTemplate<>(); + jackson3JsonPersonTemplateLtc.setValueSerializer(jackson3JsonSerializer); + jackson3JsonPersonTemplateLtc.setConnectionFactory(lettuceConnFactory); + jackson3JsonPersonTemplateLtc.afterPropertiesSet(); + RedisTemplate rawTemplateLtc = new RedisTemplate<>(); rawTemplateLtc.setConnectionFactory(lettuceConnFactory); rawTemplateLtc.setEnableDefaultSerializer(false); @@ -122,6 +135,7 @@ public static Collection testParams() { { stringFactory, xstreamStringTemplate }, // { personFactory, xstreamPersonTemplate }, // { personFactory, jackson2JsonPersonTemplate }, // + { personFactory, jackson3JsonPersonTemplate }, // { rawFactory, rawTemplate }, // lettuce @@ -132,6 +146,7 @@ public static Collection testParams() { { stringFactory, xstreamStringTemplateLtc }, // { personFactory, xstreamPersonTemplateLtc }, // { personFactory, jackson2JsonPersonTemplateLtc }, // + { personFactory, jackson3JsonPersonTemplateLtc }, // { rawFactory, rawTemplateLtc } }); } } diff --git a/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java index be3e627a28..f241a7c480 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java @@ -39,6 +39,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.Jackson3JsonRedisSerializer; import org.springframework.data.redis.serializer.OxmSerializer; import org.springframework.data.redis.test.XstreamOxmSerializerSingleton; import org.springframework.data.redis.test.extension.RedisStanalone; @@ -192,6 +193,9 @@ public static Collection testParams() { Jackson2JsonRedisSerializer jackson2JsonSerializer = new Jackson2JsonRedisSerializer<>(Person.class); Jackson2JsonRedisSerializer jackson2JsonStringSerializer = new Jackson2JsonRedisSerializer<>( String.class); + Jackson3JsonRedisSerializer jackson3JsonSerializer = new Jackson3JsonRedisSerializer<>(Person.class); + Jackson3JsonRedisSerializer jackson3JsonStringSerializer = new Jackson3JsonRedisSerializer<>( + String.class); // create Jedis Factory ObjectFactory stringFactory = new StringObjectFactory(); @@ -215,6 +219,13 @@ public static Collection testParams() { jackson2JsonPersonTemplate.setHashValueSerializer(jackson2JsonStringSerializer); jackson2JsonPersonTemplate.afterPropertiesSet(); + RedisTemplate jackson3JsonPersonTemplate = new RedisTemplate<>(); + jackson3JsonPersonTemplate.setConnectionFactory(jedisConnFactory); + jackson3JsonPersonTemplate.setDefaultSerializer(jackson3JsonSerializer); + jackson3JsonPersonTemplate.setHashKeySerializer(jackson3JsonSerializer); + jackson3JsonPersonTemplate.setHashValueSerializer(jackson3JsonStringSerializer); + jackson3JsonPersonTemplate.afterPropertiesSet(); + // Lettuce LettuceConnectionFactory lettuceConnFactory = LettuceConnectionFactoryExtension .getConnectionFactory(RedisStanalone.class, false); From 92c49a0558216084657ed4fbee85778ed03fc2d7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 10 Jun 2025 13:14:48 +0200 Subject: [PATCH 07/10] Hacking mapper config --- .../data/redis/hash/Jackson3HashMapper.java | 222 ++---------------- ...Jackson3HashMapperFlatteningUnitTests.java | 3 +- ...kson3HashMapperNonFlatteningUnitTests.java | 3 +- .../mapping/Jackson3HashMapperUnitTests.java | 49 ++-- 4 files changed, 59 insertions(+), 218 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java b/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java index 282c8d4a83..6b61dd2ecd 100644 --- a/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java +++ b/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java @@ -19,7 +19,6 @@ import tools.jackson.core.JsonGenerator; import tools.jackson.core.JsonParser; import tools.jackson.core.Version; -import tools.jackson.databind.DatabindContext; import tools.jackson.databind.DefaultTyping; import tools.jackson.databind.DeserializationContext; import tools.jackson.databind.DeserializationFeature; @@ -29,10 +28,11 @@ import tools.jackson.databind.SerializationContext; import tools.jackson.databind.ValueDeserializer; import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.cfg.MapperBuilder; import tools.jackson.databind.deser.jdk.JavaUtilCalendarDeserializer; import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.json.JsonMapper.Builder; import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; -import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator.TypeMatcher; import tools.jackson.databind.module.SimpleDeserializers; import tools.jackson.databind.module.SimpleSerializers; import tools.jackson.databind.ser.Serializers; @@ -48,12 +48,13 @@ import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TimeZone; +import java.util.function.Consumer; +import java.util.function.Supplier; import org.springframework.data.mapping.MappingException; import org.springframework.data.redis.support.collections.CollectionUtils; @@ -159,87 +160,25 @@ public class Jackson3HashMapper implements HashMapper { Jackson3HashMapper.class.getClassLoader()); private final ObjectMapper typingMapper; - private final ObjectMapper untypedMapper; private final boolean flatten; - /** - * Creates new {@link Jackson3HashMapper} with a default {@link ObjectMapper}. - * - * @param flatten boolean used to configure whether JSON de/serialized {@link Object} properties will be un/flattened - * using {@literal dot notation}, or whether to retain the hierarchical node structure created by Jackson. - */ - public Jackson3HashMapper(boolean flatten) { + public Jackson3HashMapper( + Consumer>> jsonMapperBuilder, + boolean flatten) { + this(((Supplier) () -> { + Builder builder = JsonMapper.builder(); + jsonMapperBuilder.accept(builder); + return builder.build(); + }).get(), flatten); + } - this(JsonMapper.builder().findAndAddModules().addModules(new HashMapperModule()) - .activateDefaultTypingAsProperty(BasicPolymorphicTypeValidator.builder().allowIfBaseType(Object.class).allowIfSubType(new TypeMatcher() { - @Override - public boolean match(DatabindContext ctxt, Class clazz) { - return true; - } - }).build(), - DefaultTyping.NON_FINAL_AND_ENUMS, "@class") - .configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false) + public static void preconfigure(MapperBuilder> builder) { + builder.findAndAddModules().addModules(new HashMapperModule()) + .activateDefaultTypingAsProperty(BasicPolymorphicTypeValidator.builder().allowIfBaseType(Object.class) + .allowIfSubType((ctx, clazz) -> true).build(), DefaultTyping.NON_FINAL_AND_ENUMS, "@class") + .configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .changeDefaultPropertyInclusion(value -> value.withValueInclusion(Include.NON_NULL)).build(), false); - - //// this(new Supplier() { - // @Override - // public ObjectMapper get() { - //// return JsonMapper.builder() - //// .findAndAddModules() - //// .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) - //// .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - //// .build(); - // return null; - // } - // }.get(), false); - // this(new ObjectMapper() { - // - // @Override - // protected DeserializationContextExt _deserializationContext(JsonParser p) { - // return super._deserializationContext(p); - // } - // - // @Override - // protected TypeResolverBuilder _constructDefaultTypeResolverBuilder(DefaultTyping applicability, - // PolymorphicTypeValidator typeValidator) { - // - // return new DefaultTypeResolverBuilder(applicability, typeValidator, ) { - // - // public boolean useForType(JavaType type) { - // - // if (type.isPrimitive()) { - // return false; - // } - // - // if (flatten && (type.isTypeOrSubTypeOf(Number.class) || type.isEnumType())) { - // return false; - // } - // - // if (EVERYTHING.equals(_appliesFor)) { - // return !TreeNode.class.isAssignableFrom(type.getRawClass()); - // } - // - // return super.useForType(type); - // } - // }; - // } - // }.findAndRegisterModules(), flatten); - // },flatten); - - // this.typingMapper.de.activateDefaultTyping(this.typingMapper.getPolymorphicTypeValidator(), - // DefaultTyping.EVERYTHING, As.PROPERTY); - // this.typingMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); - // - // if(flatten) { - // this.typingMapper.disable(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES); - // } - // - // // Prevent splitting time types into arrays. E - // this.typingMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - // this.typingMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - // this.typingMapper.setSerializationInclusion(Include.NON_NULL); - // this.typingMapper.registerModule(new HashMapperModule()); + .changeDefaultPropertyInclusion(value -> value.withValueInclusion(Include.NON_NULL)); } /** @@ -256,10 +195,6 @@ public Jackson3HashMapper(ObjectMapper mapper, boolean flatten) { this.flatten = flatten; this.typingMapper = mapper; - this.untypedMapper = new ObjectMapper(); - // this.untypedMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); - // this.untypedMapper.setSerializationInclusion(Include.NON_NULL); - // this.untypedMapper.findAndRegisterModules(); } @Override @@ -267,8 +202,7 @@ public Jackson3HashMapper(ObjectMapper mapper, boolean flatten) { public Map toHash(Object source) { JsonNode tree = this.typingMapper.valueToTree(source); - - return this.flatten ? flattenMap(tree.properties()) : this.untypedMapper.convertValue(tree, Map.class); + return this.flatten ? flattenMap(tree.properties()) : JsonMapper.shared().convertValue(tree, Map.class); } @Override @@ -279,13 +213,13 @@ public Object fromHash(Map hash) { if (this.flatten) { Map unflattenedHash = doUnflatten(hash); - byte[] unflattenedHashedBytes = this.untypedMapper.writeValueAsBytes(unflattenedHash); + byte[] unflattenedHashedBytes = JsonMapper.shared().writeValueAsBytes(unflattenedHash); Object hashedObject = this.typingMapper.reader().forType(Object.class).readValue(unflattenedHashedBytes); return hashedObject; } - return this.typingMapper.treeToValue(this.untypedMapper.valueToTree(hash), Object.class); + return this.typingMapper.treeToValue(JsonMapper.shared().valueToTree(hash), Object.class); } catch (Exception ex) { throw new MappingException(ex.getMessage(), ex); @@ -295,8 +229,8 @@ public Object fromHash(Map hash) { @SuppressWarnings("unchecked") private Map doUnflatten(Map source) { - Map result = new LinkedHashMap<>(); - Set treatSeparate = new LinkedHashSet<>(); + Map result = org.springframework.util.CollectionUtils.newLinkedHashMap(source.size()); + Set treatSeparate = org.springframework.util.CollectionUtils.newLinkedHashSet(source.size()); for (Entry entry : source.entrySet()) { @@ -573,112 +507,4 @@ public Calendar deserialize(JsonParser p, DeserializationContext ctxt) throws Ja return utc; } } - - // /** - // * {@link JsonDeserializer} for {@link Date} objects without considering type hints. - // */ - // private static class UntypedDateDeserializer extends JsonDeserializer { - // - // private final JsonDeserializer delegate = new UntypedObjectDeserializer(null, null); - // - // @Override - // public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) - // throws IOException { - // return deserialize(p, ctxt); - // } - // - // @Override - // public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - // - // Object value = delegate.deserialize(p, ctxt); - // - // if (value instanceof Date) { - // return (Date) value; - // } - // - // try { - // return ctxt.getConfig().getDateFormat().parse(value.toString()); - // } catch (ParseException ignore) { - // return new Date(NumberUtils.parseNumber(value.toString(), Long.class)); - // } - // } - // } - // - // /** - // * {@link JsonDeserializer} for {@link Calendar} objects without considering type hints. - // */ - // private static class UntypedCalendarDeserializer extends JsonDeserializer { - // - // private final UntypedDateDeserializer dateDeserializer = new UntypedDateDeserializer(); - // - // @Override - // public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) - // throws IOException { - // return deserialize(p, ctxt); - // } - // - // @Override - // public Calendar deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - // - // Date date = dateDeserializer.deserialize(p, ctxt); - // - // if (date != null) { - // Calendar calendar = Calendar.getInstance(); - // calendar.setTime(date); - // return calendar; - // } - // - // return null; - // } - // } - // - // /** - // * Untyped {@link JsonSerializer} to serialize plain values without writing JSON type hints. - // * - // * @param - // */ - // private static class UntypedSerializer extends JsonSerializer { - // - // private final JsonSerializer delegate; - // - // UntypedSerializer(JsonSerializer delegate) { - // this.delegate = delegate; - // } - // - // @Override - // public void serializeWithType(T value, JsonGenerator jsonGenerator, SerializerProvider serializers, - // TypeSerializer typeSerializer) throws IOException { - // - // serialize(value, jsonGenerator, serializers); - // } - // - // @Override - // public void serialize(@Nullable T value, JsonGenerator jsonGenerator, SerializerProvider serializers) - // throws IOException { - // - // if (value != null) { - // delegate.serialize(value, jsonGenerator, serializers); - // } else { - // serializers.defaultSerializeNull(jsonGenerator); - // } - // } - // } - // - // private static class DateToTimestampSerializer extends DateSerializer { - // - // // Prevent splitting to array. - // @Override - // protected boolean _asTimestamp(SerializerProvider serializers) { - // return true; - // } - // } - // - // private static class CalendarToTimestampSerializer extends CalendarSerializer { - // - // // Prevent splitting to array. - // @Override - // protected boolean _asTimestamp(SerializerProvider serializers) { - // return true; - // } - // } } diff --git a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperFlatteningUnitTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperFlatteningUnitTests.java index f11ef10ed9..122486b7f6 100644 --- a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperFlatteningUnitTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperFlatteningUnitTests.java @@ -22,6 +22,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; +import org.springframework.data.redis.hash.Jackson2HashMapper; import org.springframework.data.redis.hash.Jackson3HashMapper; /** @@ -32,7 +33,7 @@ public class Jackson3HashMapperFlatteningUnitTests extends Jackson3HashMapperUnitTests { Jackson3HashMapperFlatteningUnitTests() { - super(new Jackson3HashMapper(true)); + super(new Jackson3HashMapper(Jackson3HashMapper::preconfigure, true), new Jackson2HashMapper(true)); } @Test // GH-2593 diff --git a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperNonFlatteningUnitTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperNonFlatteningUnitTests.java index 791b6fe609..a229100b9d 100644 --- a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperNonFlatteningUnitTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperNonFlatteningUnitTests.java @@ -22,6 +22,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; +import org.springframework.data.redis.hash.Jackson2HashMapper; import org.springframework.data.redis.hash.Jackson3HashMapper; /** @@ -32,7 +33,7 @@ public class Jackson3HashMapperNonFlatteningUnitTests extends Jackson3HashMapperUnitTests { Jackson3HashMapperNonFlatteningUnitTests() { - super(new Jackson3HashMapper(false)); + super(new Jackson3HashMapper(Jackson3HashMapper::preconfigure, false), new Jackson2HashMapper(false)); } @Test // GH-2593 diff --git a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperUnitTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperUnitTests.java index 63f237ade3..ecd6ce11f7 100644 --- a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperUnitTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperUnitTests.java @@ -15,6 +15,8 @@ */ package org.springframework.data.redis.mapping; +import static org.assertj.core.api.Assertions.assertThat; + import java.math.BigDecimal; import java.math.BigInteger; import java.time.LocalDate; @@ -46,9 +48,12 @@ public abstract class Jackson3HashMapperUnitTests extends AbstractHashMapperTests { private final Jackson3HashMapper mapper; + private final Jackson2HashMapper legacyMapper; + + public Jackson3HashMapperUnitTests(Jackson3HashMapper mapper, Jackson2HashMapper legacyMapper) { - public Jackson3HashMapperUnitTests(Jackson3HashMapper mapper) { this.mapper = mapper; + this.legacyMapper = legacyMapper; } protected Jackson3HashMapper getMapper() { @@ -61,6 +66,21 @@ protected HashMapper mapperFor(Class t) { return getMapper(); } + protected void assertBackAndForwardMapping(Object o) { + + super.assertBackAndForwardMapping(o); + assertLegacyMapperCompatibility(o); + } + + protected void assertLegacyMapperCompatibility(Object o) { + + Map hash1 = legacyMapper.toHash(o); + assertThat(mapper.fromHash(hash1)).isEqualTo(o); + + Map hash2 = mapper.toHash(o); + assertThat(legacyMapper.fromHash(hash2)).isEqualTo(o); + } + @Test // DATAREDIS-423 void shouldMapTypedListOfSimpleType() { @@ -256,8 +276,8 @@ public boolean equals(Object obj) { } return Objects.equals(this.getObjects(), that.getObjects()) - && Objects.equals(this.getPersons(), that.getPersons()) - && Objects.equals(this.getStrings(), that.getStrings()); + && Objects.equals(this.getPersons(), that.getPersons()) + && Objects.equals(this.getStrings(), that.getStrings()); } @Override @@ -308,8 +328,8 @@ public boolean equals(Object obj) { } return Objects.equals(this.getObjects(), that.getObjects()) - && Objects.equals(this.getPersons(), that.getPersons()) - && Objects.equals(this.getStrings(), that.getStrings()); + && Objects.equals(this.getPersons(), that.getPersons()) + && Objects.equals(this.getStrings(), that.getStrings()); } @Override @@ -378,10 +398,9 @@ public boolean equals(Object obj) { } return Objects.equals(this.getString(), that.getString()) - && Objects.equals(this.getCalendar(), that.getCalendar()) - && Objects.equals(this.getDate(), that.getDate()) - && Objects.equals(this.getLocalDate(), that.getLocalDate()) - && Objects.equals(this.getLocalDateTime(), that.getLocalDateTime()); + && Objects.equals(this.getCalendar(), that.getCalendar()) && Objects.equals(this.getDate(), that.getDate()) + && Objects.equals(this.getLocalDate(), that.getLocalDate()) + && Objects.equals(this.getLocalDateTime(), that.getLocalDateTime()); } @Override @@ -391,13 +410,8 @@ public int hashCode() { @Override public String toString() { - return "WithDates{" + - "string='" + string + '\'' + - ", date=" + date + - ", calendar=" + calendar + - ", localDate=" + localDate + - ", localDateTime=" + localDateTime + - '}'; + return "WithDates{" + "string='" + string + '\'' + ", date=" + date + ", calendar=" + calendar + ", localDate=" + + localDate + ", localDateTime=" + localDateTime + '}'; } } @@ -433,8 +447,7 @@ public boolean equals(Object obj) { return false; } - return Objects.equals(this.getBigD(), that.getBigD()) - && Objects.equals(this.getBigI(), that.getBigI()); + return Objects.equals(this.getBigD(), that.getBigD()) && Objects.equals(this.getBigI(), that.getBigI()); } @Override From 02250dd3a63f1250133600c1838340d44a8b9703 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 10 Jun 2025 14:54:49 +0200 Subject: [PATCH 08/10] hjikp p --- .../data/redis/hash/Jackson3HashMapper.java | 10 ++++++--- .../mapping/AbstractHashMapperTests.java | 1 + ...Jackson3HashMapperFlatteningUnitTests.java | 3 +-- ...kson3HashMapperNonFlatteningUnitTests.java | 3 +-- .../mapping/Jackson3HashMapperUnitTests.java | 21 +------------------ 5 files changed, 11 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java b/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java index 6b61dd2ecd..20d8457a51 100644 --- a/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java +++ b/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java @@ -160,6 +160,7 @@ public class Jackson3HashMapper implements HashMapper { Jackson3HashMapper.class.getClassLoader()); private final ObjectMapper typingMapper; + private final ObjectMapper untypedMapper; private final boolean flatten; public Jackson3HashMapper( @@ -178,6 +179,7 @@ public static void preconfigure(MapperBuilder true).build(), DefaultTyping.NON_FINAL_AND_ENUMS, "@class") .configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + //.configure(DeserializationFeature., false) .changeDefaultPropertyInclusion(value -> value.withValueInclusion(Include.NON_NULL)); } @@ -195,6 +197,7 @@ public Jackson3HashMapper(ObjectMapper mapper, boolean flatten) { this.flatten = flatten; this.typingMapper = mapper; + this.untypedMapper = mapper.rebuild().deactivateDefaultTyping().build(); } @Override @@ -202,7 +205,7 @@ public Jackson3HashMapper(ObjectMapper mapper, boolean flatten) { public Map toHash(Object source) { JsonNode tree = this.typingMapper.valueToTree(source); - return this.flatten ? flattenMap(tree.properties()) : JsonMapper.shared().convertValue(tree, Map.class); + return this.flatten ? flattenMap(tree.properties()) : this.untypedMapper.convertValue(tree, Map.class); } @Override @@ -213,13 +216,14 @@ public Object fromHash(Map hash) { if (this.flatten) { Map unflattenedHash = doUnflatten(hash); - byte[] unflattenedHashedBytes = JsonMapper.shared().writeValueAsBytes(unflattenedHash); + System.out.println("unflat: " + unflattenedHash); + byte[] unflattenedHashedBytes = this.untypedMapper.writeValueAsBytes(unflattenedHash); Object hashedObject = this.typingMapper.reader().forType(Object.class).readValue(unflattenedHashedBytes); return hashedObject; } - return this.typingMapper.treeToValue(JsonMapper.shared().valueToTree(hash), Object.class); + return this.typingMapper.treeToValue(this.untypedMapper.valueToTree(hash), Object.class); } catch (Exception ex) { throw new MappingException(ex.getMessage(), ex); diff --git a/src/test/java/org/springframework/data/redis/mapping/AbstractHashMapperTests.java b/src/test/java/org/springframework/data/redis/mapping/AbstractHashMapperTests.java index 3807d13680..1d3fa8e256 100644 --- a/src/test/java/org/springframework/data/redis/mapping/AbstractHashMapperTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/AbstractHashMapperTests.java @@ -39,6 +39,7 @@ protected void assertBackAndForwardMapping(Object o) { HashMapper mapper = mapperFor(o.getClass()); Map hash = mapper.toHash(o); + System.out.println("hash: " + hash); assertThat(mapper.fromHash(hash)).isEqualTo(o); } diff --git a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperFlatteningUnitTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperFlatteningUnitTests.java index 122486b7f6..5e3e4b8835 100644 --- a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperFlatteningUnitTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperFlatteningUnitTests.java @@ -22,7 +22,6 @@ import java.util.Map; import org.junit.jupiter.api.Test; -import org.springframework.data.redis.hash.Jackson2HashMapper; import org.springframework.data.redis.hash.Jackson3HashMapper; /** @@ -33,7 +32,7 @@ public class Jackson3HashMapperFlatteningUnitTests extends Jackson3HashMapperUnitTests { Jackson3HashMapperFlatteningUnitTests() { - super(new Jackson3HashMapper(Jackson3HashMapper::preconfigure, true), new Jackson2HashMapper(true)); + super(new Jackson3HashMapper(Jackson3HashMapper::preconfigure,true)); } @Test // GH-2593 diff --git a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperNonFlatteningUnitTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperNonFlatteningUnitTests.java index a229100b9d..af19ff6a26 100644 --- a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperNonFlatteningUnitTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperNonFlatteningUnitTests.java @@ -22,7 +22,6 @@ import java.util.Map; import org.junit.jupiter.api.Test; -import org.springframework.data.redis.hash.Jackson2HashMapper; import org.springframework.data.redis.hash.Jackson3HashMapper; /** @@ -33,7 +32,7 @@ public class Jackson3HashMapperNonFlatteningUnitTests extends Jackson3HashMapperUnitTests { Jackson3HashMapperNonFlatteningUnitTests() { - super(new Jackson3HashMapper(Jackson3HashMapper::preconfigure, false), new Jackson2HashMapper(false)); + super(new Jackson3HashMapper(Jackson3HashMapper::preconfigure, false)); } @Test // GH-2593 diff --git a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperUnitTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperUnitTests.java index ecd6ce11f7..72631fdb64 100644 --- a/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperUnitTests.java +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson3HashMapperUnitTests.java @@ -15,8 +15,6 @@ */ package org.springframework.data.redis.mapping; -import static org.assertj.core.api.Assertions.assertThat; - import java.math.BigDecimal; import java.math.BigInteger; import java.time.LocalDate; @@ -48,12 +46,10 @@ public abstract class Jackson3HashMapperUnitTests extends AbstractHashMapperTests { private final Jackson3HashMapper mapper; - private final Jackson2HashMapper legacyMapper; - public Jackson3HashMapperUnitTests(Jackson3HashMapper mapper, Jackson2HashMapper legacyMapper) { + public Jackson3HashMapperUnitTests(Jackson3HashMapper mapper) { this.mapper = mapper; - this.legacyMapper = legacyMapper; } protected Jackson3HashMapper getMapper() { @@ -66,21 +62,6 @@ protected HashMapper mapperFor(Class t) { return getMapper(); } - protected void assertBackAndForwardMapping(Object o) { - - super.assertBackAndForwardMapping(o); - assertLegacyMapperCompatibility(o); - } - - protected void assertLegacyMapperCompatibility(Object o) { - - Map hash1 = legacyMapper.toHash(o); - assertThat(mapper.fromHash(hash1)).isEqualTo(o); - - Map hash2 = mapper.toHash(o); - assertThat(legacyMapper.fromHash(hash2)).isEqualTo(o); - } - @Test // DATAREDIS-423 void shouldMapTypedListOfSimpleType() { From c2ac3818bf2f76c48064ba7b277e3264d02bf6a0 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 11 Jun 2025 10:32:12 +0200 Subject: [PATCH 09/10] OMG it works - who would have thought that --- .../data/redis/hash/Jackson3HashMapper.java | 114 ++++++++++++++---- .../mapping/Jackson3CompatibilityTests.java | 59 +++++++++ .../Jackson3FlatteningCompatibilityTests.java | 59 +++++++++ 3 files changed, 208 insertions(+), 24 deletions(-) create mode 100644 src/test/java/org/springframework/data/redis/mapping/Jackson3CompatibilityTests.java create mode 100644 src/test/java/org/springframework/data/redis/mapping/Jackson3FlatteningCompatibilityTests.java diff --git a/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java b/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java index 20d8457a51..ee60a94df9 100644 --- a/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java +++ b/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java @@ -30,21 +30,32 @@ import tools.jackson.databind.ValueSerializer; import tools.jackson.databind.cfg.MapperBuilder; import tools.jackson.databind.deser.jdk.JavaUtilCalendarDeserializer; +import tools.jackson.databind.deser.jdk.JavaUtilDateDeserializer; +import tools.jackson.databind.deser.jdk.NumberDeserializers.BigDecimalDeserializer; +import tools.jackson.databind.deser.jdk.NumberDeserializers.BigIntegerDeserializer; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.exc.MismatchedInputException; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.json.JsonMapper.Builder; import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.jsontype.TypeSerializer; +import tools.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer; import tools.jackson.databind.module.SimpleDeserializers; import tools.jackson.databind.module.SimpleSerializers; import tools.jackson.databind.ser.Serializers; import tools.jackson.databind.ser.jdk.JavaUtilCalendarSerializer; import tools.jackson.databind.ser.jdk.JavaUtilDateSerializer; +import java.math.BigDecimal; +import java.math.BigInteger; import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.Collection; import java.util.Collections; -import java.util.GregorianCalendar; +import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; @@ -176,10 +187,9 @@ public Jackson3HashMapper( public static void preconfigure(MapperBuilder> builder) { builder.findAndAddModules().addModules(new HashMapperModule()) .activateDefaultTypingAsProperty(BasicPolymorphicTypeValidator.builder().allowIfBaseType(Object.class) - .allowIfSubType((ctx, clazz) -> true).build(), DefaultTyping.NON_FINAL_AND_ENUMS, "@class") + .allowIfSubType((ctx, clazz) -> true).build(), DefaultTyping.NON_FINAL, "@class") .configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - //.configure(DeserializationFeature., false) .changeDefaultPropertyInclusion(value -> value.withValueInclusion(Include.NON_NULL)); } @@ -197,7 +207,7 @@ public Jackson3HashMapper(ObjectMapper mapper, boolean flatten) { this.flatten = flatten; this.typingMapper = mapper; - this.untypedMapper = mapper.rebuild().deactivateDefaultTyping().build(); + this.untypedMapper = JsonMapper.shared(); } @Override @@ -216,7 +226,6 @@ public Object fromHash(Map hash) { if (this.flatten) { Map unflattenedHash = doUnflatten(hash); - System.out.println("unflat: " + unflattenedHash); byte[] unflattenedHashedBytes = this.untypedMapper.writeValueAsBytes(unflattenedHash); Object hashedObject = this.typingMapper.reader().forType(Object.class).readValue(unflattenedHashedBytes); @@ -328,9 +337,7 @@ private void doFlatten(String propertyPrefix, Set> input propertyPrefix = propertyPrefix + "."; } - Iterator> entries = inputMap.iterator(); - while (entries.hasNext()) { - Entry entry = entries.next(); + for(Entry entry : inputMap) { flattenElement(propertyPrefix + entry.getKey(), entry.getValue(), resultMap); } } @@ -351,24 +358,24 @@ private void flattenElement(String propertyPrefix, Object source, Map resultMap.put(propertyPrefix, element.textValue()); + case STRING -> resultMap.put(propertyPrefix, element.stringValue()); case NUMBER -> resultMap.put(propertyPrefix, element.numberValue()); case BOOLEAN -> resultMap.put(propertyPrefix, element.booleanValue()); case BINARY -> { @@ -410,7 +417,7 @@ private void flattenElement(String propertyPrefix, Object source, Map list, Map resultMap) { + private void flattenCollection(String propertyPrefix, Collection list, Map resultMap) { - for (int counter = 0; list.hasNext(); counter++) { - JsonNode element = list.next(); + Iterator iterator = list.iterator(); + for (int counter = 0; iterator.hasNext(); counter++) { + JsonNode element = iterator.next(); flattenElement(propertyPrefix + "[" + counter + "]", element, resultMap); } } @@ -473,38 +481,96 @@ public Version version() { public void setupModule(SetupContext context) { List> valueSerializers = new ArrayList<>(); - valueSerializers.add(new JavaUtilDateSerializer(true, null)); + valueSerializers.add(new JavaUtilDateSerializer(true, null) { + @Override + public void serializeWithType(Date value, JsonGenerator g, SerializationContext ctxt, TypeSerializer typeSer) + throws JacksonException { + serialize(value, g, ctxt); + } + }); valueSerializers.add(new UTCCalendarSerializer()); Serializers serializers = new SimpleSerializers(valueSerializers); context.addSerializers(serializers); Map, ValueDeserializer> valueDeserializers = new LinkedHashMap<>(); - valueDeserializers.put(GregorianCalendar.class, new UTCCalendarDeserializer()); + valueDeserializers.put(java.util.Calendar.class, + new UntypedFallbackDeserializer<>(new UntypedUTCCalendarDeserializer())); + valueDeserializers.put(java.util.Date.class, new UntypedFallbackDeserializer<>(new JavaUtilDateDeserializer())); + valueDeserializers.put(BigInteger.class, new UntypedFallbackDeserializer<>(new BigIntegerDeserializer())); + valueDeserializers.put(BigDecimal.class, new UntypedFallbackDeserializer<>(new BigDecimalDeserializer())); context.addDeserializers(new SimpleDeserializers(valueDeserializers)); } + + } + + static class UntypedFallbackDeserializer extends StdDeserializer { + + private final StdDeserializer delegate; + + protected UntypedFallbackDeserializer(StdDeserializer delegate) { + super(Object.class); + this.delegate = delegate; + } + + @Override + public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) + throws JacksonException { + + if (!(typeDeserializer instanceof AsPropertyTypeDeserializer asPropertySerializer)) { + return super.deserializeWithType(p, ctxt, typeDeserializer); + } + + try { + return super.deserializeWithType(p, ctxt, typeDeserializer); + } catch (MismatchedInputException e) { + if (!asPropertySerializer.baseType().isTypeOrSuperTypeOf(delegate.handledType())) { + throw e; + } + } + + return deserialize(p, ctxt); + + } + + @Override + @SuppressWarnings("unchecked") + public T deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + return (T) delegate.deserialize(p, ctxt); + } } static class UTCCalendarSerializer extends JavaUtilCalendarSerializer { + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + @Override public void serialize(Calendar value, JsonGenerator g, SerializationContext provider) throws JacksonException { Calendar utc = Calendar.getInstance(); utc.setTimeInMillis(value.getTimeInMillis()); - utc.setTimeZone(TimeZone.getTimeZone("UTC")); + utc.setTimeZone(UTC); super.serialize(utc, g, provider); } + + @Override + public void serializeWithType(Calendar value, JsonGenerator g, SerializationContext ctxt, TypeSerializer typeSer) + throws JacksonException { + serialize(value, g, ctxt); + } } - static class UTCCalendarDeserializer extends JavaUtilCalendarDeserializer { + static class UntypedUTCCalendarDeserializer extends JavaUtilCalendarDeserializer { + + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + @Override public Calendar deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { Calendar cal = super.deserialize(p, ctxt); - Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + Calendar utc = Calendar.getInstance(UTC); utc.setTimeInMillis(cal.getTimeInMillis()); utc.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); diff --git a/src/test/java/org/springframework/data/redis/mapping/Jackson3CompatibilityTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson3CompatibilityTests.java new file mode 100644 index 0000000000..e67e802081 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson3CompatibilityTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.redis.mapping; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; + +import org.junit.jupiter.api.Disabled; +import org.springframework.data.redis.hash.Jackson2HashMapper; +import org.springframework.data.redis.hash.Jackson3HashMapper; + +/** + * @author Christoph Strobl + */ +public class Jackson3CompatibilityTests extends Jackson3HashMapperUnitTests { + + private final Jackson2HashMapper jackson2HashMapper; + + public Jackson3CompatibilityTests() { + super(new Jackson3HashMapper(Jackson3HashMapper::preconfigure, false)); + this.jackson2HashMapper = new Jackson2HashMapper(false); + } + + @Override + @Disabled("with jackson 2 this used to render the timestamp as string. Now its a long and in line with calendar timestamp") + void dateValueShouldBeTreatedCorrectly() { + super.dateValueShouldBeTreatedCorrectly(); + } + + @Override + @Disabled("with jackson 2 used to render the enum and its type hint in an array. Now its just the enum value") + void enumsShouldBeTreatedCorrectly() { + super.enumsShouldBeTreatedCorrectly(); + } + + @Override + protected void assertBackAndForwardMapping(Object o) { + + Map hash3 = getMapper().toHash(o); + Map hash2 = jackson2HashMapper.toHash(o); + + assertThat(hash3).containsAllEntriesOf(hash2); + assertThat(getMapper().fromHash(hash2)).isEqualTo(o); + } +} diff --git a/src/test/java/org/springframework/data/redis/mapping/Jackson3FlatteningCompatibilityTests.java b/src/test/java/org/springframework/data/redis/mapping/Jackson3FlatteningCompatibilityTests.java new file mode 100644 index 0000000000..2c9d337abe --- /dev/null +++ b/src/test/java/org/springframework/data/redis/mapping/Jackson3FlatteningCompatibilityTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.redis.mapping; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; + +import org.junit.jupiter.api.Disabled; +import org.springframework.data.redis.hash.Jackson2HashMapper; +import org.springframework.data.redis.hash.Jackson3HashMapper; + +/** + * @author Christoph Strobl + */ +public class Jackson3FlatteningCompatibilityTests extends Jackson3HashMapperUnitTests { + + private final Jackson2HashMapper jackson2HashMapper; + + public Jackson3FlatteningCompatibilityTests() { + super(new Jackson3HashMapper(Jackson3HashMapper::preconfigure, true)); + this.jackson2HashMapper = new Jackson2HashMapper(true); + } + + @Override + @Disabled("with jackson 2 this used to render the timestamp as string. Now its a long and in line with calendar timestamp") + void dateValueShouldBeTreatedCorrectly() { + super.dateValueShouldBeTreatedCorrectly(); + } + + @Override + @Disabled("with jackson 2 used to render the enum and its type hint in an array. Now its just the enum value") + void enumsShouldBeTreatedCorrectly() { + super.enumsShouldBeTreatedCorrectly(); + } + + @Override + protected void assertBackAndForwardMapping(Object o) { + + Map hash3 = getMapper().toHash(o); + Map hash2 = jackson2HashMapper.toHash(o); + + assertThat(hash3).containsAllEntriesOf(hash2); + assertThat(getMapper().fromHash(hash2)).isEqualTo(o); + } +} From e2822e35aaa2974f16869a82818510f224fd6e18 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 11 Jun 2025 11:45:10 +0200 Subject: [PATCH 10/10] update documentation --- .../ROOT/pages/redis/hash-mappers.adoc | 87 ++++++++++++++++++- .../redis/redis-repositories/mapping.adoc | 8 +- .../ROOT/pages/redis/redis-streams.adoc | 2 +- .../modules/ROOT/pages/redis/template.adoc | 2 +- .../data/redis/hash/Jackson2HashMapper.java | 2 + .../data/redis/hash/Jackson3HashMapper.java | 17 ++-- .../Jackson2JsonRedisSerializer.java | 2 + .../redis/serializer/JacksonObjectReader.java | 2 + .../redis/serializer/JacksonObjectWriter.java | 2 + 9 files changed, 106 insertions(+), 18 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/redis/hash-mappers.adoc b/src/main/antora/modules/ROOT/pages/redis/hash-mappers.adoc index 334a2fd515..6f320d436b 100644 --- a/src/main/antora/modules/ROOT/pages/redis/hash-mappers.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/hash-mappers.adoc @@ -1,7 +1,7 @@ [[redis.hashmappers.root]] = Hash Mapping -Data can be stored by using various data structures within Redis. javadoc:org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer[] can convert objects in https://en.wikipedia.org/wiki/JSON[JSON] format. Ideally, JSON can be stored as a value by using plain keys. You can achieve a more sophisticated mapping of structured objects by using Redis hashes. Spring Data Redis offers various strategies for mapping data to hashes (depending on the use case): +Data can be stored by using various data structures within Redis. javadoc:org.springframework.data.redis.serializer.Jackson3JsonRedisSerializer[] can convert objects in https://en.wikipedia.org/wiki/JSON[JSON] format. Ideally, JSON can be stored as a value by using plain keys. You can achieve a more sophisticated mapping of structured objects by using Redis hashes. Spring Data Redis offers various strategies for mapping data to hashes (depending on the use case): * Direct mapping, by using javadoc:org.springframework.data.redis.core.HashOperations[] and a xref:redis.adoc#redis:serializer[serializer] * Using xref:repositories.adoc[Redis Repositories] @@ -16,7 +16,8 @@ Multiple implementations are available: * javadoc:org.springframework.data.redis.hash.BeanUtilsHashMapper[] using Spring's {spring-framework-javadoc}/org/springframework/beans/BeanUtils.html[BeanUtils]. * javadoc:org.springframework.data.redis.hash.ObjectHashMapper[] using xref:redis/redis-repositories/mapping.adoc[Object-to-Hash Mapping]. -* <> using https://github.com/FasterXML/jackson[FasterXML Jackson]. +* <> using https://github.com/FasterXML/jackson[FasterXML Jackson 3]. +* <> (deprecated) using https://github.com/FasterXML/jackson[FasterXML Jackson 2]. The following example shows one way to implement hash mapping: @@ -50,9 +51,91 @@ public class HashMapping { } ---- +[[redis.hashmappers.jackson3]] +=== Jackson3HashMapper + +javadoc:org.springframework.data.redis.hash.Jackson3HashMapper[] provides Redis Hash mapping for domain objects by using https://github.com/FasterXML/jackson[FasterXML Jackson 3]. +`Jackson3HashMapper` can map top-level properties as Hash field names and, optionally, flatten the structure. +Simple types map to simple values. Complex types (nested objects, collections, maps, and so on) are represented as nested JSON. + +Flattening creates individual hash entries for all nested properties and resolves complex types into simple types, as far as possible. + +Consider the following class and the data structure it contains: + +[source,java] +---- +public class Person { + String firstname; + String lastname; + Address address; + Date date; + LocalDateTime localDateTime; +} + +public class Address { + String city; + String country; +} +---- + +The following table shows how the data in the preceding class would appear in normal mapping: + +.Normal Mapping +[width="80%",cols="<1,<2",options="header"] +|==== +|Hash Field +|Value + +|firstname +|`Jon` + +|lastname +|`Snow` + +|address +|`{ "city" : "Castle Black", "country" : "The North" }` + +|date +|1561543964015 + +|localDateTime +|`2018-01-02T12:13:14` +|==== + +The following table shows how the data in the preceding class would appear in flat mapping: + +.Flat Mapping +[width="80%",cols="<1,<2",options="header"] +|==== +|Hash Field +|Value + +|firstname +|`Jon` + +|lastname +|`Snow` + +|address.city +|`Castle Black` + +|address.country +|`The North` + +|date +|1561543964015 + +|localDateTime +|`2018-01-02T12:13:14` +|==== + +NOTE: Flattening requires all property names to not interfere with the JSON path. Using dots or brackets in map keys or as property names is not supported when you use flattening. The resulting hash cannot be mapped back into an Object. + [[redis.hashmappers.jackson2]] === Jackson2HashMapper +WARNING: Jackson 2 based implementations have been deprecated and are subject to removal in a subsequent release. + javadoc:org.springframework.data.redis.hash.Jackson2HashMapper[] provides Redis Hash mapping for domain objects by using https://github.com/FasterXML/jackson[FasterXML Jackson]. `Jackson2HashMapper` can map top-level properties as Hash field names and, optionally, flatten the structure. Simple types map to simple values. Complex types (nested objects, collections, maps, and so on) are represented as nested JSON. diff --git a/src/main/antora/modules/ROOT/pages/redis/redis-repositories/mapping.adoc b/src/main/antora/modules/ROOT/pages/redis/redis-repositories/mapping.adoc index fb1a08c0f8..11ea76b302 100644 --- a/src/main/antora/modules/ROOT/pages/redis/redis-repositories/mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/redis-repositories/mapping.adoc @@ -92,11 +92,11 @@ The following example shows two sample byte array converters: @WritingConverter public class AddressToBytesConverter implements Converter { - private final Jackson2JsonRedisSerializer

serializer; + private final Jackson3JsonRedisSerializer
serializer; public AddressToBytesConverter() { - serializer = new Jackson2JsonRedisSerializer
(Address.class); + serializer = new Jackson3JsonRedisSerializer
(Address.class); serializer.setObjectMapper(new ObjectMapper()); } @@ -109,11 +109,11 @@ public class AddressToBytesConverter implements Converter { @ReadingConverter public class BytesToAddressConverter implements Converter { - private final Jackson2JsonRedisSerializer
serializer; + private final Jackson3JsonRedisSerializer
serializer; public BytesToAddressConverter() { - serializer = new Jackson2JsonRedisSerializer
(Address.class); + serializer = new Jackson3JsonRedisSerializer
(Address.class); serializer.setObjectMapper(new ObjectMapper()); } diff --git a/src/main/antora/modules/ROOT/pages/redis/redis-streams.adoc b/src/main/antora/modules/ROOT/pages/redis/redis-streams.adoc index 25d916f637..0d71ee07f5 100644 --- a/src/main/antora/modules/ROOT/pages/redis/redis-streams.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/redis-streams.adoc @@ -291,7 +291,7 @@ You may provide a `HashMapper` suitable for your requirements when obtaining `St [source,java] ---- redisTemplate() - .opsForStream(new Jackson2HashMapper(true)) + .opsForStream(new Jackson3HashMapper(true)) .add(record); <1> ---- <1> XADD user-logon * "firstname" "night" "@class" "com.example.User" "lastname" "angel" diff --git a/src/main/antora/modules/ROOT/pages/redis/template.adoc b/src/main/antora/modules/ROOT/pages/redis/template.adoc index 05af2e789b..c9b3ddd3f7 100644 --- a/src/main/antora/modules/ROOT/pages/redis/template.adoc +++ b/src/main/antora/modules/ROOT/pages/redis/template.adoc @@ -365,7 +365,7 @@ Multiple implementations are available (including two that have been already men * javadoc:org.springframework.data.redis.serializer.JdkSerializationRedisSerializer[], which is used by default for javadoc:org.springframework.data.redis.cache.RedisCache[] and javadoc:org.springframework.data.redis.core.RedisTemplate[]. * the `StringRedisSerializer`. -However, one can use `OxmSerializer` for Object/XML mapping through Spring {spring-framework-docs}/data-access.html#oxm[OXM] support or javadoc:org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer[] or javadoc:org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer[] for storing data in https://en.wikipedia.org/wiki/JSON[JSON] format. +However, one can use `OxmSerializer` for Object/XML mapping through Spring {spring-framework-docs}/data-access.html#oxm[OXM] support or javadoc:org.springframework.data.redis.serializer.Jackson3JsonRedisSerializer[] or javadoc:org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer[] for storing data in https://en.wikipedia.org/wiki/JSON[JSON] format. Do note that the storage format is not limited only to values. It can be used for keys, values, or hashes without any restrictions. diff --git a/src/main/java/org/springframework/data/redis/hash/Jackson2HashMapper.java b/src/main/java/org/springframework/data/redis/hash/Jackson2HashMapper.java index 8788c41ba4..d16d263d69 100644 --- a/src/main/java/org/springframework/data/redis/hash/Jackson2HashMapper.java +++ b/src/main/java/org/springframework/data/redis/hash/Jackson2HashMapper.java @@ -143,7 +143,9 @@ * @author Mark Paluch * @author John Blum * @since 1.8 + * @deprecated since 4.0 */ +@Deprecated(since = "4.0", forRemoval = true) public class Jackson2HashMapper implements HashMapper { private static final boolean SOURCE_VERSION_PRESENT = diff --git a/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java b/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java index ee60a94df9..20cd8d906f 100644 --- a/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java +++ b/src/main/java/org/springframework/data/redis/hash/Jackson3HashMapper.java @@ -70,7 +70,6 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.redis.support.collections.CollectionUtils; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; -import org.springframework.lang.NonNull; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -161,9 +160,7 @@ * * * @author Christoph Strobl - * @author Mark Paluch - * @author John Blum - * @since 1.8 + * @since 4.0 */ public class Jackson3HashMapper implements HashMapper { @@ -301,23 +298,23 @@ private Map doUnflatten(Map source) { return result; } - private boolean isIndexed(@NonNull String value) { + private boolean isIndexed(String value) { return value.indexOf('[') > -1; } - private boolean isNotIndexed(@NonNull String value) { + private boolean isNotIndexed(String value) { return !isIndexed(value); } - private boolean isNonNestedIndexed(@NonNull String value) { + private boolean isNonNestedIndexed(String value) { return value.endsWith("]"); } - private int getIndex(@NonNull String indexedValue) { + private int getIndex(String indexedValue) { return Integer.parseInt(indexedValue.substring(indexedValue.indexOf('[') + 1, indexedValue.length() - 1)); } - private @NonNull String stripIndex(@NonNull String indexedValue) { + private String stripIndex(String indexedValue) { int indexOfLeftBracket = indexedValue.indexOf("["); @@ -337,7 +334,7 @@ private void doFlatten(String propertyPrefix, Set> input propertyPrefix = propertyPrefix + "."; } - for(Entry entry : inputMap) { + for (Entry entry : inputMap) { flattenElement(propertyPrefix + entry.getKey(), entry.getValue(), resultMap); } } diff --git a/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java index 32d626bdd8..7b6ba4d9ba 100644 --- a/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java @@ -40,7 +40,9 @@ * @author Thomas Darimont * @author Mark Paluch * @since 1.2 + * @deprecated since 4.0 in favor of {@link Jackson3JsonRedisSerializer}. */ +@Deprecated(since = "4.0", forRemoval = true) public class Jackson2JsonRedisSerializer implements RedisSerializer { /** diff --git a/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java index e2c1d943ec..9da953f8dd 100644 --- a/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java +++ b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java @@ -30,8 +30,10 @@ * * @author Mark Paluch * @since 3.0 + * @deprecated since 4.0 in favor of {@link Jackson3ObjectReader}. */ @FunctionalInterface +@Deprecated(since = "4.0", forRemoval = true) public interface JacksonObjectReader { /** diff --git a/src/main/java/org/springframework/data/redis/serializer/JacksonObjectWriter.java b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectWriter.java index 88db313130..5d13b67b66 100644 --- a/src/main/java/org/springframework/data/redis/serializer/JacksonObjectWriter.java +++ b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectWriter.java @@ -28,8 +28,10 @@ * * @author Mark Paluch * @since 3.0 + * @deprecated since 4.0 in favor of {@link Jackson3ObjectWriter}. */ @FunctionalInterface +@Deprecated(since = "4.0", forRemoval = true) public interface JacksonObjectWriter { /**