如何创建确定性 Jackson ObjectMapper?

Bil*_*eil 7 java json jackson

我希望能够跨 JVM 生成任何 Java POJO 的 MD5 校验和。该方法是将对象序列化为 JSON,然后对 JSON 进行 MD5。

问题是 Jackson 的 JSON 序列化不是确定性的,主要是因为许多集合不是确定性的。

ObjectMapper mapper = new ObjectMapper()                                               
    .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)                                           
    .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
    ... // all other custom modules / features
;
Run Code Online (Sandbox Code Playgroud)

这两个功能解决了在 POJO 和 Map 上保持字段排序的两个问题。

下一个挑战是动态修改任何集合并对其进行排序。这要求每个集合中的每个元素都是可排序的,但我们假设现在可以。

有没有办法拦截每个集合并在序列化之前对其进行排序?

Bil*_*eil 7

我通过以下代码实现了这一点。阅读有关创建具有一定确定性的 Jackson ObjectMapper 的更多信息

public class DeterministicObjectMapper {

    private DeterministicObjectMapper() { }

    public static ObjectMapper create(ObjectMapper original, CustomComparators customComparators) {
        ObjectMapper mapper = original.copy()
            .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
            .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);

        /*
         *  Get the original instance of the SerializerProvider before we add our custom module.
         *  Our Collection Delegating code does not call itself.
         */
        SerializerProvider serializers = mapper.getSerializerProviderInstance();

        // This module is reponsible for replacing non-deterministic objects
        // with deterministic ones. Example convert Set to a sorted List.
        SimpleModule module = new SimpleModule();
        module.addSerializer(Collection.class,
             new CustomDelegatingSerializerProvider(serializers, new CollectionToSortedListConverter(customComparators))
        );
        mapper.registerModule(module);
        return mapper;
    }

    /*
     * We need this class to delegate to the original SerializerProvider
     * before we added our module to it. If we have a Collection -> Collection converter
     * it delegates to itself and infinite loops until the stack overflows.
     */
    private static class CustomDelegatingSerializerProvider extends StdDelegatingSerializer
    {
        private final SerializerProvider serializerProvider;

        private CustomDelegatingSerializerProvider(SerializerProvider serializerProvider,
                                                   Converter<?, ?> converter)
        {
            super(converter);
            this.serializerProvider = serializerProvider;
        }

        @Override
        protected StdDelegatingSerializer withDelegate(Converter<Object,?> converter,
                                                       JavaType delegateType, JsonSerializer<?> delegateSerializer)
        {
            return new StdDelegatingSerializer(converter, delegateType, delegateSerializer);
        }

        /*
         *  If we do not override this method to delegate to the original
         *  serializerProvider we get a stack overflow exception because it recursively
         *  calls itself. Basically we are hijacking the Collection serializer to first
         *  sort the list then delegate it back to the original serializer.
         */
        @Override
        public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
                throws JsonMappingException
        {
            return super.createContextual(serializerProvider, property);
        }
    }

    private static class CollectionToSortedListConverter extends StdConverter<Collection<?>, Collection<?>>
    {
        private final CustomComparators customComparators;

        public CollectionToSortedListConverter(CustomComparators customComparators) {
            this.customComparators = customComparators;
        }
        @Override
        public Collection<? extends Object> convert(Collection<?> value)
        {
            if (value == null || value.isEmpty())
            {
                return Collections.emptyList();
            }

            /**
             * Sort all elements by class first, then by our custom comparator.
             * If the collection is heterogeneous or has anonymous classes its useful
             * to first sort by the class name then by the comparator. We don't care
             * about that actual sort order, just that it is deterministic.
             */
            Comparator<Object> comparator = Comparator.comparing(x -> x.getClass().getName())
                                                      .thenComparing(customComparators::compare);
            Collection<? extends Object> filtered = Seq.seq(value)
                                                       .filter(Objects::nonNull)
                                                       .sorted(comparator)
                                                       .toList();
            if (filtered.isEmpty())
            {
                return Collections.emptyList();
            }

            return filtered;
        }
    }

    public static class CustomComparators {
        private final LinkedHashMap<Class<?>, Comparator<? extends Object>> customComparators;

        public CustomComparators() {
            customComparators = new LinkedHashMap<>();
        }

        public <T> void addConverter(Class<T> clazz, Comparator<?> comparator) {
            customComparators.put(clazz, comparator);
        }

        @SuppressWarnings({ "unchecked", "rawtypes" })
        public int compare(Object first, Object second) {
            // If the object is comparable use its comparator
            if (first instanceof Comparable) {
                return ((Comparable) first).compareTo(second);
            }

            // If the object is not comparable try a custom supplied comparator
            for (Entry<Class<?>, Comparator<?>> entry : customComparators.entrySet()) {
                Class<?> clazz = entry.getKey();
                if (first.getClass().isAssignableFrom(clazz)) {
                    Comparator<Object> comparator = (Comparator<Object>) entry.getValue();
                    return comparator.compare(first, second);
                }
            }

            // we have no way to order the collection so fail hard
            String message = String.format("Cannot compare object of type %s without a custom comparator", first.getClass().getName());
            throw new UnsupportedOperationException(message);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)