Commit ba552f1d authored by ioann's avatar ioann Committed by Phillip Webb

Allow GSON customization via properties or beans

Update GSON support to allow customization with either properties or
customize beans.

See gh-11498
parent 9cb5f3da
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -16,28 +16,145 @@ ...@@ -16,28 +16,145 @@
package org.springframework.boot.autoconfigure.gson; package org.springframework.boot.autoconfigure.gson;
import java.util.List;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson; 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.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for Gson. * {@link EnableAutoConfiguration Auto-configuration} for Gson.
* *
* @author David Liu * @author David Liu
* @author Ivan Golovko
* @since 1.2.0 * @since 1.2.0
*/ */
@Configuration @Configuration
@ConditionalOnClass(Gson.class) @ConditionalOnClass(Gson.class)
public class GsonAutoConfiguration { public class GsonAutoConfiguration {
@Configuration
static class GsonConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean(Gson.class)
public Gson gson(GsonBuilder gsonBuilder) {
return gsonBuilder.create();
}
}
@Configuration
static class GsonBuilderConfiguration {
@Bean @Bean
@ConditionalOnMissingBean public GsonBuilder gsonBuilder(List<GsonBuilderCustomizer> customizers) {
public Gson gson() { final GsonBuilder gsonBuilder = new GsonBuilder();
return new Gson(); 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);
}
}
}
} }
} }
/*
* 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);
}
/*
* 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;
}
}
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -16,49 +16,254 @@ ...@@ -16,49 +16,254 @@
package org.springframework.boot.autoconfigure.gson; 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 com.google.gson.Gson;
import org.junit.After; import com.google.gson.LongSerializationPolicy;
import org.junit.Before; import org.joda.time.DateTime;
import org.junit.Test; 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; import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Tests for {@link GsonAutoConfiguration}. * Tests for {@link GsonAutoConfiguration}.
* *
* @author David Liu * @author David Liu
* @author Ivan Golovko
*/ */
public class GsonAutoConfigurationTests { public class GsonAutoConfigurationTests {
AnnotationConfigApplicationContext context; private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GsonAutoConfiguration.class));
@Before @Test
public void setUp() { public void gsonRegistration() {
this.context = new AnnotationConfigApplicationContext(); this.contextRunner.run(context -> {
Gson gson = context.getBean(Gson.class);
assertThat(gson.toJson(new DataObject())).isEqualTo("{\"data\":1}");
});
} }
@After @Test
public void tearDown() { public void generateNonExecutableJson() {
if (this.context != null) { this.contextRunner
this.context.close(); .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 @Test
public void gsonRegistration() { public void serializeNulls() {
this.context.register(GsonAutoConfiguration.class); this.contextRunner.withPropertyValues("spring.gson.serialize-nulls:true")
this.context.refresh(); .run(context -> {
Gson gson = this.context.getBean(Gson.class); Gson gson = context.getBean(Gson.class);
assertThat(gson.toJson(new DataObject())).isEqualTo("{\"data\":\"hello\"}"); 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<DataObject, String> 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 { public class DataObject {
@SuppressWarnings("unused") @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";
}
} }
} }
...@@ -340,6 +340,19 @@ content into your application. Rather, pick only the properties that you need. ...@@ -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.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". 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]) # 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.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. spring.jersey.filter.order=0 # Jersey filter chain order.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment