From fe081b1742f5901c0c938ef6e33d89d20adf6d89 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 16 Jul 2021 09:04:46 +0100 Subject: [PATCH] Add Gson converter immediately before default Jackson converter Previously, when the preferred json mapper was set to Gson, the Gson HTTP message converter was added before any other converters. This changed the form of String responses that were already valid. When Jackson is in use, a string converter is used as it appears earlier in the list than the Jackson converter. When the mapper is switched to Gson, the Gson converter is added first in the list of converters and the Strong converter is no longer used. This results in the String, that was already valid JSON, being converted again. This changes its form as quotes are escaped, etc. This commit updates HttpMessageConverters so that the Gson converter is added to the list immediately before the default Jackson converter. This is done by considering the Gson converter to be an equivalent of the Jackson converter. Fixes gh-27354 --- .../http/HttpMessageConverters.java | 29 +++++++++++++++++-- .../http/HttpMessageConvertersTests.java | 14 ++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConverters.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConverters.java index 2792bb81b8..2ca518f800 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConverters.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -20,8 +20,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; @@ -63,6 +65,15 @@ public class HttpMessageConverters implements Iterable> NON_REPLACING_CONVERTERS = Collections.unmodifiableList(nonReplacingConverters); } + private static final Map, Class> EQUIVALENT_CONVERTERS; + + static { + Map, Class> equivalentConverters = new HashMap<>(); + putIfExists(equivalentConverters, "org.springframework.http.converter.json.MappingJackson2HttpMessageConverter", + "org.springframework.http.converter.json.GsonHttpMessageConverter"); + EQUIVALENT_CONVERTERS = Collections.unmodifiableMap(equivalentConverters); + } + private final List> converters; /** @@ -132,7 +143,12 @@ public class HttpMessageConverters implements Iterable> return false; } } - return ClassUtils.isAssignableValue(defaultConverter.getClass(), candidate); + Class converterClass = defaultConverter.getClass(); + if (ClassUtils.isAssignableValue(converterClass, candidate)) { + return true; + } + Class equivalentClass = EQUIVALENT_CONVERTERS.get(converterClass); + return equivalentClass != null && ClassUtils.isAssignableValue(equivalentClass, candidate); } private void configurePartConverters(AllEncompassingFormHttpMessageConverter formConverter, @@ -220,4 +236,13 @@ public class HttpMessageConverters implements Iterable> } } + private static void putIfExists(Map, Class> map, String keyClassName, String valueClassName) { + try { + map.put(Class.forName(keyClassName), Class.forName(valueClassName)); + } + catch (ClassNotFoundException | NoClassDefFoundError ex) { + // Ignore + } + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersTests.java index f8468fa85b..34dffc7c22 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 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. @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.http; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -28,6 +29,7 @@ import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.ResourceRegionHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter; +import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; @@ -81,6 +83,16 @@ class HttpMessageConvertersTests { assertThat(converters.getConverters().indexOf(converter1)).isNotEqualTo(0); } + @Test + void addBeforeExistingEquivalentConverter() { + GsonHttpMessageConverter converter1 = new GsonHttpMessageConverter(); + HttpMessageConverters converters = new HttpMessageConverters(converter1); + List> converterClasses = converters.getConverters().stream().map(HttpMessageConverter::getClass) + .collect(Collectors.toList()); + assertThat(converterClasses).containsSequence(GsonHttpMessageConverter.class, + MappingJackson2HttpMessageConverter.class); + } + @Test void addNewConverters() { HttpMessageConverter converter1 = mock(HttpMessageConverter.class);