From ba552f1d2448c71b888f2cc5ee8c574a5d35d5d2 Mon Sep 17 00:00:00 2001 From: ioann Date: Thu, 4 Jan 2018 17:14:03 +0300 Subject: [PATCH 1/2] Allow GSON customization via properties or beans Update GSON support to allow customization with either properties or customize beans. See gh-11498 --- .../gson/GsonAutoConfiguration.java | 127 ++++++++- .../gson/GsonBuilderCustomizer.java | 37 +++ .../autoconfigure/gson/GsonProperties.java | 190 +++++++++++++ .../gson/GsonAutoConfigurationTests.java | 251 ++++++++++++++++-- .../appendix-application-properties.adoc | 13 + 5 files changed, 590 insertions(+), 28 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonBuilderCustomizer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java index fe667a3e59..ab51bf9799 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2014 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. @@ -16,28 +16,145 @@ package org.springframework.boot.autoconfigure.gson; +import java.util.List; + +import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.LongSerializationPolicy; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.Ordered; /** * {@link EnableAutoConfiguration Auto-configuration} for Gson. * * @author David Liu + * @author Ivan Golovko * @since 1.2.0 */ @Configuration @ConditionalOnClass(Gson.class) public class GsonAutoConfiguration { - @Bean - @ConditionalOnMissingBean - public Gson gson() { - return new Gson(); + @Configuration + static class GsonConfiguration { + + @Bean + @Primary + @ConditionalOnMissingBean(Gson.class) + public Gson gson(GsonBuilder gsonBuilder) { + return gsonBuilder.create(); + } + } + + @Configuration + static class GsonBuilderConfiguration { + + @Bean + public GsonBuilder gsonBuilder(List customizers) { + final GsonBuilder gsonBuilder = new GsonBuilder(); + customizers.forEach(c -> c.customize(gsonBuilder)); + return gsonBuilder; + } + + } + + @Configuration + @EnableConfigurationProperties(GsonProperties.class) + static class GsonBuilderCustomizerConfiguration { + + @Bean + public StandardGsonBuilderCustomizer standardGsonBuilderCustomizer( + GsonProperties gsonProperties) { + return new StandardGsonBuilderCustomizer(gsonProperties); + } + + private static final class StandardGsonBuilderCustomizer + implements GsonBuilderCustomizer, Ordered { + + private final GsonProperties properties; + + StandardGsonBuilderCustomizer(GsonProperties properties) { + this.properties = properties; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public void customize(GsonBuilder gsonBuilder) { + + boolean generateNonExecutableJson = this.properties + .isGenerateNonExecutableJson(); + if (generateNonExecutableJson) { + gsonBuilder.generateNonExecutableJson(); + } + + boolean excludeFieldsWithoutExposeAnnotation = this.properties + .isExcludeFieldsWithoutExposeAnnotation(); + if (excludeFieldsWithoutExposeAnnotation) { + gsonBuilder.excludeFieldsWithoutExposeAnnotation(); + } + + boolean serializeNulls = this.properties.isSerializeNulls(); + if (serializeNulls) { + gsonBuilder.serializeNulls(); + } + + boolean enableComplexMapKeySerialization = this.properties + .isEnableComplexMapKeySerialization(); + if (enableComplexMapKeySerialization) { + gsonBuilder.enableComplexMapKeySerialization(); + } + + boolean disableInnerClassSerialization = this.properties + .isDisableInnerClassSerialization(); + if (disableInnerClassSerialization) { + gsonBuilder.disableInnerClassSerialization(); + } + + LongSerializationPolicy longSerializationPolicy = this.properties + .getLongSerializationPolicy(); + if (longSerializationPolicy != null) { + gsonBuilder.setLongSerializationPolicy(longSerializationPolicy); + } + + FieldNamingPolicy fieldNamingPolicy = this.properties + .getFieldNamingPolicy(); + if (fieldNamingPolicy != null) { + gsonBuilder.setFieldNamingPolicy(fieldNamingPolicy); + } + + boolean prettyPrinting = this.properties.isPrettyPrinting(); + if (prettyPrinting) { + gsonBuilder.setPrettyPrinting(); + } + + boolean isLenient = this.properties.isLenient(); + if (isLenient) { + gsonBuilder.setLenient(); + } + + boolean disableHtmlEscaping = this.properties.isDisableHtmlEscaping(); + if (disableHtmlEscaping) { + gsonBuilder.disableHtmlEscaping(); + } + + String dateFormat = this.properties.getDateFormat(); + if (dateFormat != null) { + gsonBuilder.setDateFormat(dateFormat); + } + } + } } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonBuilderCustomizer.java new file mode 100644 index 0000000000..cf9054f3e6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonBuilderCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2017 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Callback interface that can be implemented by beans wishing to further customize the + * {@link Gson} via {@link GsonBuilder} retaining its default auto-configuration. + * + * @author Ivan Golovko + * @since 2.0.0 + */ +@FunctionalInterface +public interface GsonBuilderCustomizer { + + /** + * Customize the GsonBuilder. + * @param gsonBuilder the GsonBuilder to customize + */ + void customize(GsonBuilder gsonBuilder); +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java new file mode 100644 index 0000000000..2ca8f96d70 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java @@ -0,0 +1,190 @@ +/* + * Copyright 2012-2017 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.gson; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.LongSerializationPolicy; +import com.google.gson.annotations.Expose; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties to configure {@link Gson}. + * + * @author Ivan Golovko + * @since 2.0.0 + */ +@ConfigurationProperties(prefix = "spring.gson") +public class GsonProperties { + + /** + * Makes the output JSON non-executable in Javascript by prefixing the generated JSON + * with some special text. + */ + private boolean generateNonExecutableJson; + + /** + * Configures {@link Gson} to exclude all fields from consideration for serialization + * or deserialization that do not have the {@link Expose} annotation. + */ + private boolean excludeFieldsWithoutExposeAnnotation; + + /** + * Configure {@link Gson} to serialize null fields. + */ + private boolean serializeNulls; + + /** + * Enabling this feature will only change the serialized form if the map key is a + * complex type (i.e. non-primitive) in its serialized JSON form + */ + private boolean enableComplexMapKeySerialization; + + /** + * Configures {@link Gson} to exclude inner classes during serialization. + */ + private boolean disableInnerClassSerialization; + + /** + * Configures {@link Gson} to apply a specific serialization policy for Long and long + * objects. + */ + private LongSerializationPolicy longSerializationPolicy; + + /** + * Configures {@link Gson} to apply a specific naming policy to an object's field + * during serialization and deserialization. + */ + private FieldNamingPolicy fieldNamingPolicy; + + /** + * Configures {@link Gson} to output Json that fits in a page for pretty printing. + * This option only affects Json serialization. + */ + private boolean prettyPrinting; + + /** + * By default, {@link Gson} is strict and only accepts JSON as specified by RFC 4627. + * This option makes the parser liberal in what it accepts. + */ + private boolean lenient; + + /** + * By default, {@link Gson} escapes HTML characters such as < > etc. Use this option + * to configure Gson to pass-through HTML characters as is. + */ + private boolean disableHtmlEscaping; + + /** + * Configures {@link Gson} to serialize Date objects according to the pattern + * provided. + */ + private String dateFormat; + + public boolean isGenerateNonExecutableJson() { + return this.generateNonExecutableJson; + } + + public void setGenerateNonExecutableJson(boolean generateNonExecutableJson) { + this.generateNonExecutableJson = generateNonExecutableJson; + } + + public boolean isExcludeFieldsWithoutExposeAnnotation() { + return this.excludeFieldsWithoutExposeAnnotation; + } + + public void setExcludeFieldsWithoutExposeAnnotation( + boolean excludeFieldsWithoutExposeAnnotation) { + this.excludeFieldsWithoutExposeAnnotation = excludeFieldsWithoutExposeAnnotation; + } + + public boolean isSerializeNulls() { + return this.serializeNulls; + } + + public void setSerializeNulls(boolean serializeNulls) { + this.serializeNulls = serializeNulls; + } + + public boolean isEnableComplexMapKeySerialization() { + return this.enableComplexMapKeySerialization; + } + + public void setEnableComplexMapKeySerialization( + boolean enableComplexMapKeySerialization) { + this.enableComplexMapKeySerialization = enableComplexMapKeySerialization; + } + + public boolean isDisableInnerClassSerialization() { + return this.disableInnerClassSerialization; + } + + public void setDisableInnerClassSerialization( + boolean disableInnerClassSerialization) { + this.disableInnerClassSerialization = disableInnerClassSerialization; + } + + public LongSerializationPolicy getLongSerializationPolicy() { + return this.longSerializationPolicy; + } + + public void setLongSerializationPolicy( + LongSerializationPolicy longSerializationPolicy) { + this.longSerializationPolicy = longSerializationPolicy; + } + + public FieldNamingPolicy getFieldNamingPolicy() { + return this.fieldNamingPolicy; + } + + public void setFieldNamingPolicy(FieldNamingPolicy fieldNamingPolicy) { + this.fieldNamingPolicy = fieldNamingPolicy; + } + + public boolean isPrettyPrinting() { + return this.prettyPrinting; + } + + public void setPrettyPrinting(boolean prettyPrinting) { + this.prettyPrinting = prettyPrinting; + } + + public boolean isLenient() { + return this.lenient; + } + + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + public boolean isDisableHtmlEscaping() { + return this.disableHtmlEscaping; + } + + public void setDisableHtmlEscaping(boolean disableHtmlEscaping) { + this.disableHtmlEscaping = disableHtmlEscaping; + } + + public String getDateFormat() { + return this.dateFormat; + } + + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java index fe20419b88..f077a0c73f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2016 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. @@ -16,49 +16,254 @@ package org.springframework.boot.autoconfigure.gson; +import java.lang.reflect.Field; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; -import org.junit.After; -import org.junit.Before; +import com.google.gson.LongSerializationPolicy; +import org.joda.time.DateTime; import org.junit.Test; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; import static org.assertj.core.api.Assertions.assertThat; - /** * Tests for {@link GsonAutoConfiguration}. * * @author David Liu + * @author Ivan Golovko */ public class GsonAutoConfigurationTests { - AnnotationConfigApplicationContext context; - - @Before - public void setUp() { - this.context = new AnnotationConfigApplicationContext(); - } - - @After - public void tearDown() { - if (this.context != null) { - this.context.close(); - } - } + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GsonAutoConfiguration.class)); @Test public void gsonRegistration() { - this.context.register(GsonAutoConfiguration.class); - this.context.refresh(); - Gson gson = this.context.getBean(Gson.class); - assertThat(gson.toJson(new DataObject())).isEqualTo("{\"data\":\"hello\"}"); + this.contextRunner.run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.toJson(new DataObject())).isEqualTo("{\"data\":1}"); + }); + } + + @Test + public void generateNonExecutableJson() { + this.contextRunner + .withPropertyValues("spring.gson.generate-non-executable-json:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.toJson(new DataObject())) + .isNotEqualTo("{\"data\":1}"); + assertThat(gson.toJson(new DataObject())).endsWith("{\"data\":1}"); + }); + } + + @Test + public void excludeFieldsWithoutExposeAnnotation() { + this.contextRunner + .withPropertyValues( + "spring.gson.exclude-fields-without-expose-annotation:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.toJson(new DataObject())).isEqualTo("{}"); + }); + } + + @Test + public void serializeNulls() { + this.contextRunner.withPropertyValues("spring.gson.serialize-nulls:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.serializeNulls()).isTrue(); + }); + } + + @Test + public void enableComplexMapKeySerialization() { + this.contextRunner + .withPropertyValues( + "spring.gson.enable-complex-map-key-serialization:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + Map original = new LinkedHashMap<>(); + original.put(new DataObject(), "a"); + assertThat(gson.toJson(original)).isEqualTo("[[{\"data\":1},\"a\"]]"); + }); + } + + @Test + public void notDisableInnerClassSerialization() { + this.contextRunner.run(context -> { + Gson gson = context.getBean(Gson.class); + WrapperObject wrapperObject = new WrapperObject(); + assertThat(gson.toJson(wrapperObject.new NestedObject())) + .isEqualTo("{\"data\":\"nested\"}"); + }); + } + + @Test + public void disableInnerClassSerialization() { + this.contextRunner + .withPropertyValues("spring.gson.disable-inner-class-serialization:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + WrapperObject wrapperObject = new WrapperObject(); + assertThat(gson.toJson(wrapperObject.new NestedObject())) + .isEqualTo("null"); + }); + } + + @Test + public void withLongSerializationPolicy() { + this.contextRunner.withPropertyValues( + "spring.gson.long-serialization-policy:" + LongSerializationPolicy.STRING) + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.toJson(new DataObject())) + .isEqualTo("{\"data\":\"1\"}"); + }); + } + + @Test + public void withFieldNamingPolicy() { + FieldNamingPolicy fieldNamingPolicy = FieldNamingPolicy.UPPER_CAMEL_CASE; + this.contextRunner + .withPropertyValues( + "spring.gson.field-naming-policy:" + fieldNamingPolicy) + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.fieldNamingStrategy()).isEqualTo(fieldNamingPolicy); + }); + } + + @Test + public void additionalGsonBuilderCustomization() { + this.contextRunner.withUserConfiguration(GsonBuilderCustomConfig.class) + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.toJson(new DataObject())).isEqualTo("{}"); + }); + } + + @Test + public void withPrettyPrinting() { + this.contextRunner.withPropertyValues("spring.gson.pretty-printing:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.toJson(new DataObject())) + .isEqualTo("{\n \"data\": 1\n}"); + }); + } + + @Test + public void withoutLenient() throws Exception { + this.contextRunner.run(context -> { + Gson gson = context.getBean(Gson.class); + /* + * It seems, that lenient setting not work in version 2.8.2 We get access to + * it via reflection + */ + Field lenientField = gson.getClass().getDeclaredField("lenient"); + lenientField.setAccessible(true); + boolean lenient = lenientField.getBoolean(gson); + + assertThat(lenient).isFalse(); + }); + } + + @Test + public void withLenient() throws Exception { + this.contextRunner.withPropertyValues("spring.gson.lenient:true").run(context -> { + Gson gson = context.getBean(Gson.class); + + /* + * It seems, that lenient setting not work in version 2.8.0 of gson We get + * access to it via reflection + */ + Field lenientField = gson.getClass().getDeclaredField("lenient"); + lenientField.setAccessible(true); + boolean lenient = lenientField.getBoolean(gson); + + assertThat(lenient).isTrue(); + }); + } + + @Test + public void withHtmlEscaping() { + this.contextRunner.run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.htmlSafe()).isTrue(); + }); + } + + @Test + public void withoutHtmlEscaping() { + this.contextRunner.withPropertyValues("spring.gson.disable-html-escaping:true") + .run(context -> { + Gson gson = context.getBean(Gson.class); + assertThat(gson.htmlSafe()).isFalse(); + }); + + } + + @Test + public void customDateFormat() { + this.contextRunner.withPropertyValues("spring.gson.date-format:H") + .run(context -> { + Gson gson = context.getBean(Gson.class); + DateTime dateTime = new DateTime(1988, 6, 25, 20, 30); + Date date = dateTime.toDate(); + assertThat(gson.toJson(date)).isEqualTo("\"20\""); + }); + } + + protected static class GsonBuilderCustomConfig { + + @Bean + public GsonBuilderCustomizer customSerializationExclusionStrategy() { + return (gsonBuilder) -> gsonBuilder + .addSerializationExclusionStrategy(new ExclusionStrategy() { + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + return "data".equals(fieldAttributes.getName()); + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return false; + } + }); + } + } public class DataObject { @SuppressWarnings("unused") - private String data = "hello"; + public static final String STATIC_DATA = "bye"; + @SuppressWarnings("unused") + private Long data = 1L; + + public void setData(Long data) { + this.data = data; + } + } + + public class WrapperObject { + + @SuppressWarnings("unused") + class NestedObject { + @SuppressWarnings("unused") + private String data = "nested"; + } } } diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 9c5d4781c3..9a9cbed9fe 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -340,6 +340,19 @@ content into your application. Rather, pick only the properties that you need. spring.jackson.serialization.*= # Jackson on/off features that affect the way Java objects are serialized. spring.jackson.time-zone= # Time zone used when formatting dates. For instance, "America/Los_Angeles" or "GMT+10". + # GSON ({sc-spring-boot-autoconfigure}/gson/GsonProperties.{sc-ext}[GsonProperties]) + spring.gson.date-format= # Configures Gson to serialize Date objects according to the pattern provided. + spring.gson.disable-html-escaping=false # By default, Gson escapes HTML characters such as < > etc. Use this option to configure Gson to pass-through HTML characters as is. + spring.gson.disable-inner-class-serialization=false # Configures Gson to exclude inner classes during serialization. + spring.gson.enable-complex-map-key-serialization=false # Enabling this feature will only change the serialized form if the map key is a complex type (i.e. non-primitive) in its serialized JSON form + spring.gson.exclude-fields-without-expose-annotation=false # Configures Gson to exclude all fields from consideration for serialization or deserialization that do not have the Expose annotation. + spring.gson.field-naming-policy= # Configures Gson to apply a specific naming policy to an object's field during serialization and deserialization. + spring.gson.generate-non-executable-json=false # Makes the output JSON non-executable in Javascript by prefixing the generated JSON with some special text. + spring.gson.lenient=false # By default, Gson is strict and only accepts JSON as specified by RFC 4627. This option makes the parser liberal in what it accepts. + spring.gson.long-serialization-policy= # Configures Gson to apply a specific serialization policy for Long and long objects. + spring.gson.pretty-printing=false # Configures Gson to output Json that fits in a page for pretty printing. This option only affects Json serialization. + spring.gson.serialize-nulls=false # Configure Gson to serialize null fields. + # JERSEY ({sc-spring-boot-autoconfigure}/jersey/JerseyProperties.{sc-ext}[JerseyProperties]) spring.jersey.application-path= # Path that serves as the base URI for the application. If specified, overrides the value of "@ApplicationPath". spring.jersey.filter.order=0 # Jersey filter chain order. From 286a3bb10aa6884c4c93245d1c928ec7d3db69fb Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 8 Jan 2018 22:39:45 -0800 Subject: [PATCH 2/2] Polish GSON customization support Closes gh-11498 --- .../gson/GsonAutoConfiguration.java | 159 ++++++------------ .../gson/GsonBuilderCustomizer.java | 2 +- .../autoconfigure/gson/GsonProperties.java | 86 +++++----- .../gson/GsonAutoConfigurationTests.java | 8 +- .../appendix-application-properties.adoc | 24 +-- 5 files changed, 109 insertions(+), 170 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java index ab51bf9799..c3e2e56472 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2018 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. @@ -18,18 +18,16 @@ package org.springframework.boot.autoconfigure.gson; import java.util.List; -import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.LongSerializationPolicy; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; import org.springframework.core.Ordered; /** @@ -41,120 +39,65 @@ import org.springframework.core.Ordered; */ @Configuration @ConditionalOnClass(Gson.class) +@EnableConfigurationProperties(GsonProperties.class) public class GsonAutoConfiguration { - @Configuration - static class GsonConfiguration { - - @Bean - @Primary - @ConditionalOnMissingBean(Gson.class) - public Gson gson(GsonBuilder gsonBuilder) { - return gsonBuilder.create(); - } + @Bean + public GsonBuilder gsonBuilder(List customizers) { + GsonBuilder builder = new GsonBuilder(); + customizers.forEach(c -> c.customize(builder)); + return builder; } - @Configuration - static class GsonBuilderConfiguration { - - @Bean - public GsonBuilder gsonBuilder(List customizers) { - final GsonBuilder gsonBuilder = new GsonBuilder(); - customizers.forEach(c -> c.customize(gsonBuilder)); - return gsonBuilder; - } - + @Bean + @ConditionalOnMissingBean(Gson.class) + public Gson gson(GsonBuilder gsonBuilder) { + return gsonBuilder.create(); } - @Configuration - @EnableConfigurationProperties(GsonProperties.class) - static class GsonBuilderCustomizerConfiguration { + @Bean + public StandardGsonBuilderCustomizer standardGsonBuilderCustomizer( + GsonProperties gsonProperties) { + return new StandardGsonBuilderCustomizer(gsonProperties); + } - @Bean - public StandardGsonBuilderCustomizer standardGsonBuilderCustomizer( - GsonProperties gsonProperties) { - return new StandardGsonBuilderCustomizer(gsonProperties); + private static final class StandardGsonBuilderCustomizer + implements GsonBuilderCustomizer, Ordered { + + private final GsonProperties properties; + + StandardGsonBuilderCustomizer(GsonProperties properties) { + this.properties = properties; } - private static final class StandardGsonBuilderCustomizer - implements GsonBuilderCustomizer, Ordered { - - private final GsonProperties properties; - - StandardGsonBuilderCustomizer(GsonProperties properties) { - this.properties = properties; - } - - @Override - public int getOrder() { - return 0; - } - - @Override - public void customize(GsonBuilder gsonBuilder) { - - boolean generateNonExecutableJson = this.properties - .isGenerateNonExecutableJson(); - if (generateNonExecutableJson) { - gsonBuilder.generateNonExecutableJson(); - } - - boolean excludeFieldsWithoutExposeAnnotation = this.properties - .isExcludeFieldsWithoutExposeAnnotation(); - if (excludeFieldsWithoutExposeAnnotation) { - gsonBuilder.excludeFieldsWithoutExposeAnnotation(); - } - - boolean serializeNulls = this.properties.isSerializeNulls(); - if (serializeNulls) { - gsonBuilder.serializeNulls(); - } - - boolean enableComplexMapKeySerialization = this.properties - .isEnableComplexMapKeySerialization(); - if (enableComplexMapKeySerialization) { - gsonBuilder.enableComplexMapKeySerialization(); - } - - boolean disableInnerClassSerialization = this.properties - .isDisableInnerClassSerialization(); - if (disableInnerClassSerialization) { - gsonBuilder.disableInnerClassSerialization(); - } - - LongSerializationPolicy longSerializationPolicy = this.properties - .getLongSerializationPolicy(); - if (longSerializationPolicy != null) { - gsonBuilder.setLongSerializationPolicy(longSerializationPolicy); - } - - FieldNamingPolicy fieldNamingPolicy = this.properties - .getFieldNamingPolicy(); - if (fieldNamingPolicy != null) { - gsonBuilder.setFieldNamingPolicy(fieldNamingPolicy); - } - - boolean prettyPrinting = this.properties.isPrettyPrinting(); - if (prettyPrinting) { - gsonBuilder.setPrettyPrinting(); - } - - boolean isLenient = this.properties.isLenient(); - if (isLenient) { - gsonBuilder.setLenient(); - } - - boolean disableHtmlEscaping = this.properties.isDisableHtmlEscaping(); - if (disableHtmlEscaping) { - gsonBuilder.disableHtmlEscaping(); - } - - String dateFormat = this.properties.getDateFormat(); - if (dateFormat != null) { - gsonBuilder.setDateFormat(dateFormat); - } - } + @Override + public int getOrder() { + return 0; } + + @Override + public void customize(GsonBuilder builder) { + GsonProperties properties = this.properties; + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getGenerateNonExecutableJson) + .toCall(builder::generateNonExecutableJson); + map.from(properties::getExcludeFieldsWithoutExposeAnnotation) + .toCall(builder::excludeFieldsWithoutExposeAnnotation); + map.from(properties::getSerializeNulls).toCall(builder::serializeNulls); + map.from(properties::getEnableComplexMapKeySerialization) + .toCall(builder::enableComplexMapKeySerialization); + map.from(properties::getDisableInnerClassSerialization) + .toCall(builder::disableInnerClassSerialization); + map.from(properties::getLongSerializationPolicy) + .to(builder::setLongSerializationPolicy); + map.from(properties::getFieldNamingPolicy).to(builder::setFieldNamingPolicy); + map.from(properties::getPrettyPrinting).toCall(builder::setPrettyPrinting); + map.from(properties::getLenient).toCall(builder::setLenient); + map.from(properties::getDisableHtmlEscaping) + .toCall(builder::disableHtmlEscaping); + map.from(properties::getDateFormat).to(builder::setDateFormat); + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonBuilderCustomizer.java index cf9054f3e6..cecf888ed4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonBuilderCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonBuilderCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java index 2ca8f96d70..d918d5c52f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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,7 +19,6 @@ package org.springframework.boot.autoconfigure.gson; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.LongSerializationPolicy; -import com.google.gson.annotations.Expose; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -33,109 +32,103 @@ import org.springframework.boot.context.properties.ConfigurationProperties; public class GsonProperties { /** - * Makes the output JSON non-executable in Javascript by prefixing the generated JSON - * with some special text. + * Whether to generate non executable JSON by prefixing the output with some special + * text. */ - private boolean generateNonExecutableJson; + private Boolean generateNonExecutableJson; /** - * Configures {@link Gson} to exclude all fields from consideration for serialization - * or deserialization that do not have the {@link Expose} annotation. + * Whether to exclude all fields from consideration for serialization or + * deserialization that do not have the "Expose" annotation. */ - private boolean excludeFieldsWithoutExposeAnnotation; + private Boolean excludeFieldsWithoutExposeAnnotation; /** - * Configure {@link Gson} to serialize null fields. + * Whether to to serialize null fields. */ - private boolean serializeNulls; + private Boolean serializeNulls; /** - * Enabling this feature will only change the serialized form if the map key is a - * complex type (i.e. non-primitive) in its serialized JSON form + * Whether to enabled serialization of complex map keys (i.e. non-primitives). */ - private boolean enableComplexMapKeySerialization; + private Boolean enableComplexMapKeySerialization; /** - * Configures {@link Gson} to exclude inner classes during serialization. + * Whether to exclude inner classes during serialization. */ - private boolean disableInnerClassSerialization; + private Boolean disableInnerClassSerialization; /** - * Configures {@link Gson} to apply a specific serialization policy for Long and long - * objects. + * Serialization policy for Long and long types. */ private LongSerializationPolicy longSerializationPolicy; /** - * Configures {@link Gson} to apply a specific naming policy to an object's field - * during serialization and deserialization. + * The naming policy that should be applied to an object's field during serialization + * and deserialization. */ private FieldNamingPolicy fieldNamingPolicy; /** - * Configures {@link Gson} to output Json that fits in a page for pretty printing. - * This option only affects Json serialization. + * Whether to output serialized JSON that fits in a page for pretty printing. */ - private boolean prettyPrinting; + private Boolean prettyPrinting; /** - * By default, {@link Gson} is strict and only accepts JSON as specified by RFC 4627. - * This option makes the parser liberal in what it accepts. + * Whether to be lenient about parsing JSON that doesn't conform to RFC 4627. */ - private boolean lenient; + private Boolean lenient; /** - * By default, {@link Gson} escapes HTML characters such as < > etc. Use this option - * to configure Gson to pass-through HTML characters as is. + * Whether to disable the escaping of HTML characters such as '<' '>' etc. */ - private boolean disableHtmlEscaping; + private Boolean disableHtmlEscaping; /** - * Configures {@link Gson} to serialize Date objects according to the pattern - * provided. + * The format to use when serializing Date objects. */ private String dateFormat; - public boolean isGenerateNonExecutableJson() { + public Boolean getGenerateNonExecutableJson() { return this.generateNonExecutableJson; } - public void setGenerateNonExecutableJson(boolean generateNonExecutableJson) { + public void setGenerateNonExecutableJson(Boolean generateNonExecutableJson) { this.generateNonExecutableJson = generateNonExecutableJson; } - public boolean isExcludeFieldsWithoutExposeAnnotation() { + public Boolean getExcludeFieldsWithoutExposeAnnotation() { return this.excludeFieldsWithoutExposeAnnotation; } public void setExcludeFieldsWithoutExposeAnnotation( - boolean excludeFieldsWithoutExposeAnnotation) { + Boolean excludeFieldsWithoutExposeAnnotation) { this.excludeFieldsWithoutExposeAnnotation = excludeFieldsWithoutExposeAnnotation; } - public boolean isSerializeNulls() { + public Boolean getSerializeNulls() { return this.serializeNulls; } - public void setSerializeNulls(boolean serializeNulls) { + public void setSerializeNulls(Boolean serializeNulls) { this.serializeNulls = serializeNulls; } - public boolean isEnableComplexMapKeySerialization() { + public Boolean getEnableComplexMapKeySerialization() { return this.enableComplexMapKeySerialization; } public void setEnableComplexMapKeySerialization( - boolean enableComplexMapKeySerialization) { + Boolean enableComplexMapKeySerialization) { this.enableComplexMapKeySerialization = enableComplexMapKeySerialization; } - public boolean isDisableInnerClassSerialization() { + public Boolean getDisableInnerClassSerialization() { return this.disableInnerClassSerialization; } public void setDisableInnerClassSerialization( - boolean disableInnerClassSerialization) { + Boolean disableInnerClassSerialization) { this.disableInnerClassSerialization = disableInnerClassSerialization; } @@ -156,27 +149,27 @@ public class GsonProperties { this.fieldNamingPolicy = fieldNamingPolicy; } - public boolean isPrettyPrinting() { + public Boolean getPrettyPrinting() { return this.prettyPrinting; } - public void setPrettyPrinting(boolean prettyPrinting) { + public void setPrettyPrinting(Boolean prettyPrinting) { this.prettyPrinting = prettyPrinting; } - public boolean isLenient() { + public Boolean getLenient() { return this.lenient; } - public void setLenient(boolean lenient) { + public void setLenient(Boolean lenient) { this.lenient = lenient; } - public boolean isDisableHtmlEscaping() { + public Boolean getDisableHtmlEscaping() { return this.disableHtmlEscaping; } - public void setDisableHtmlEscaping(boolean disableHtmlEscaping) { + public void setDisableHtmlEscaping(Boolean disableHtmlEscaping) { this.disableHtmlEscaping = disableHtmlEscaping; } @@ -187,4 +180,5 @@ public class GsonProperties { public void setDateFormat(String dateFormat) { this.dateFormat = dateFormat; } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java index f077a0c73f..5e2cc2cfc3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2018 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. @@ -34,6 +34,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link GsonAutoConfiguration}. * @@ -246,7 +247,6 @@ public class GsonAutoConfigurationTests { public class DataObject { - @SuppressWarnings("unused") public static final String STATIC_DATA = "bye"; @SuppressWarnings("unused") @@ -255,14 +255,16 @@ public class GsonAutoConfigurationTests { public void setData(Long data) { this.data = data; } + } public class WrapperObject { @SuppressWarnings("unused") class NestedObject { - @SuppressWarnings("unused") + private String data = "nested"; + } } diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 9a9cbed9fe..87a0a09b09 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -340,18 +340,18 @@ content into your application. Rather, pick only the properties that you need. spring.jackson.serialization.*= # Jackson on/off features that affect the way Java objects are serialized. spring.jackson.time-zone= # Time zone used when formatting dates. For instance, "America/Los_Angeles" or "GMT+10". - # GSON ({sc-spring-boot-autoconfigure}/gson/GsonProperties.{sc-ext}[GsonProperties]) - spring.gson.date-format= # Configures Gson to serialize Date objects according to the pattern provided. - spring.gson.disable-html-escaping=false # By default, Gson escapes HTML characters such as < > etc. Use this option to configure Gson to pass-through HTML characters as is. - spring.gson.disable-inner-class-serialization=false # Configures Gson to exclude inner classes during serialization. - spring.gson.enable-complex-map-key-serialization=false # Enabling this feature will only change the serialized form if the map key is a complex type (i.e. non-primitive) in its serialized JSON form - spring.gson.exclude-fields-without-expose-annotation=false # Configures Gson to exclude all fields from consideration for serialization or deserialization that do not have the Expose annotation. - spring.gson.field-naming-policy= # Configures Gson to apply a specific naming policy to an object's field during serialization and deserialization. - spring.gson.generate-non-executable-json=false # Makes the output JSON non-executable in Javascript by prefixing the generated JSON with some special text. - spring.gson.lenient=false # By default, Gson is strict and only accepts JSON as specified by RFC 4627. This option makes the parser liberal in what it accepts. - spring.gson.long-serialization-policy= # Configures Gson to apply a specific serialization policy for Long and long objects. - spring.gson.pretty-printing=false # Configures Gson to output Json that fits in a page for pretty printing. This option only affects Json serialization. - spring.gson.serialize-nulls=false # Configure Gson to serialize null fields. + # GSON ({sc-spring-boot-autoconfigure}/gson/GsonProperties.{sc-ext}[GsonProperties]) + spring.gson.date-format= # The format to use when serializing Date objects. + spring.gson.disable-html-escaping= # Whether to disable the escaping of HTML characters such as '<' '>' etc. + spring.gson.disable-inner-class-serialization= # Whether to exclude inner classes during serialization. + spring.gson.enable-complex-map-key-serialization= # Whether to enabled serialization of complex map keys (i.e. non-primitives). + spring.gson.exclude-fields-without-expose-annotation= # Whether to exclude all fields from consideration for serialization or deserialization that do not have the "Expose" annotation. + spring.gson.field-naming-policy= # The naming policy that should be applied to an object's field during serialization and deserialization. + spring.gson.generate-non-executable-json= # Whether to generate non executable JSON by prefixing the output with some special text. + spring.gson.lenient= # Whether to be lenient about parsing JSON that doesn't conform to RFC 4627. + spring.gson.long-serialization-policy= # Serialization policy for Long and long types. + spring.gson.pretty-printing= # Whether to output serialized JSON that fits in a page for pretty printing. + spring.gson.serialize-nulls= # Whether to to serialize null fields. # JERSEY ({sc-spring-boot-autoconfigure}/jersey/JerseyProperties.{sc-ext}[JerseyProperties]) spring.jersey.application-path= # Path that serves as the base URI for the application. If specified, overrides the value of "@ApplicationPath".