Skip to content

Commit 214bc40

Browse files
committed
Provide Gson/JSON-B MessageConverter for spring-messaging (aligned with spring-web)
Closes gh-21496
1 parent ad5072a commit 214bc40

File tree

11 files changed

+971
-68
lines changed

11 files changed

+971
-68
lines changed

spring-messaging/spring-messaging.gradle

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ dependencies {
1212
optional("io.rsocket:rsocket-core")
1313
optional("io.rsocket:rsocket-transport-netty")
1414
optional("com.fasterxml.jackson.core:jackson-databind")
15+
optional("com.google.code.gson:gson")
16+
optional("javax.json.bind:javax.json.bind-api")
1517
optional("javax.xml.bind:jaxb-api")
1618
optional("com.google.protobuf:protobuf-java-util")
1719
optional("org.jetbrains.kotlinx:kotlinx-coroutines-core")
@@ -31,8 +33,10 @@ dependencies {
3133
testCompile("org.jetbrains.kotlin:kotlin-stdlib")
3234
testCompile("org.xmlunit:xmlunit-assertj")
3335
testCompile("org.xmlunit:xmlunit-matchers")
36+
testRuntime("com.sun.activation:javax.activation")
3437
testRuntime("com.sun.xml.bind:jaxb-core")
3538
testRuntime("com.sun.xml.bind:jaxb-impl")
36-
testRuntime("com.sun.activation:javax.activation")
39+
testRuntime("javax.json:javax.json-api")
40+
testRuntime("org.apache.johnzon:johnzon-jsonb")
3741
testRuntime(project(":spring-context"))
3842
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.messaging.converter;
18+
19+
import java.io.ByteArrayInputStream;
20+
import java.io.ByteArrayOutputStream;
21+
import java.io.InputStream;
22+
import java.io.InputStreamReader;
23+
import java.io.OutputStreamWriter;
24+
import java.io.Reader;
25+
import java.io.Writer;
26+
import java.lang.reflect.Type;
27+
import java.nio.charset.Charset;
28+
import java.nio.charset.StandardCharsets;
29+
30+
import org.springframework.lang.Nullable;
31+
import org.springframework.messaging.Message;
32+
import org.springframework.messaging.MessageHeaders;
33+
import org.springframework.util.ClassUtils;
34+
import org.springframework.util.MimeType;
35+
36+
/**
37+
* Common base class for plain JSON converters, e.g. Gson and JSON-B.
38+
*
39+
* @author Juergen Hoeller
40+
* @since 5.3
41+
* @see GsonMessageConverter
42+
* @see JsonbMessageConverter
43+
* @see #fromJson(Reader, Type)
44+
* @see #fromJson(String, Type)
45+
* @see #toJson(Object, Type)
46+
* @see #toJson(Object, Type, Writer)
47+
*/
48+
public abstract class AbstractJsonMessageConverter extends AbstractMessageConverter {
49+
50+
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
51+
52+
53+
protected AbstractJsonMessageConverter() {
54+
super(new MimeType("application", "json"));
55+
}
56+
57+
58+
@Override
59+
protected boolean supports(Class<?> clazz) {
60+
return true;
61+
}
62+
63+
@Override
64+
@Nullable
65+
protected Object convertFromInternal(Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
66+
try {
67+
Type resolvedType = getResolvedType(targetClass, conversionHint);
68+
Object payload = message.getPayload();
69+
if (ClassUtils.isAssignableValue(targetClass, payload)) {
70+
return payload;
71+
}
72+
else if (payload instanceof byte[]) {
73+
return fromJson(getReader((byte[]) payload, message.getHeaders()), resolvedType);
74+
}
75+
else {
76+
// Assuming a text-based source payload
77+
return fromJson(payload.toString(), resolvedType);
78+
}
79+
}
80+
catch (Exception ex) {
81+
throw new MessageConversionException(message, "Could not read JSON: " + ex.getMessage(), ex);
82+
}
83+
}
84+
85+
@Override
86+
@Nullable
87+
protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) {
88+
try {
89+
Type resolvedType = getResolvedType(payload.getClass(), conversionHint);
90+
if (byte[].class == getSerializedPayloadClass()) {
91+
ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
92+
Writer writer = getWriter(out, headers);
93+
toJson(payload, resolvedType, writer);
94+
writer.flush();
95+
return out.toByteArray();
96+
}
97+
else {
98+
// Assuming a text-based target payload
99+
return toJson(payload, resolvedType);
100+
}
101+
}
102+
catch (Exception ex) {
103+
throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex);
104+
}
105+
}
106+
107+
108+
private Reader getReader(byte[] payload, @Nullable MessageHeaders headers) {
109+
InputStream in = new ByteArrayInputStream(payload);
110+
return new InputStreamReader(in, getCharsetToUse(headers));
111+
}
112+
113+
private Writer getWriter(ByteArrayOutputStream out, @Nullable MessageHeaders headers) {
114+
return new OutputStreamWriter(out, getCharsetToUse(headers));
115+
}
116+
117+
private Charset getCharsetToUse(@Nullable MessageHeaders headers) {
118+
MimeType mimeType = getMimeType(headers);
119+
return (mimeType != null && mimeType.getCharset() != null ? mimeType.getCharset() : DEFAULT_CHARSET);
120+
}
121+
122+
123+
protected abstract Object fromJson(Reader reader, Type resolvedType);
124+
125+
protected abstract Object fromJson(String payload, Type resolvedType);
126+
127+
protected abstract void toJson(Object payload, Type resolvedType, Writer writer);
128+
129+
protected abstract String toJson(Object payload, Type resolvedType);
130+
131+
}

spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.messaging.converter;
1818

19+
import java.lang.reflect.Type;
1920
import java.util.ArrayList;
2021
import java.util.Arrays;
2122
import java.util.Collection;
@@ -25,6 +26,8 @@
2526
import org.apache.commons.logging.Log;
2627
import org.apache.commons.logging.LogFactory;
2728

29+
import org.springframework.core.GenericTypeResolver;
30+
import org.springframework.core.MethodParameter;
2831
import org.springframework.lang.Nullable;
2932
import org.springframework.messaging.Message;
3033
import org.springframework.messaging.MessageHeaders;
@@ -92,7 +95,7 @@ public List<MimeType> getSupportedMimeTypes() {
9295
}
9396

9497
/**
95-
* Allows sub-classes to add more supported mime types.
98+
* Allows subclasses to add more supported mime types.
9699
* @since 5.2.2
97100
*/
98101
protected void addSupportedMimeTypes(MimeType... supportedMimeTypes) {
@@ -167,21 +170,6 @@ public Class<?> getSerializedPayloadClass() {
167170
}
168171

169172

170-
/**
171-
* Returns the default content type for the payload. Called when
172-
* {@link #toMessage(Object, MessageHeaders)} is invoked without message headers or
173-
* without a content type header.
174-
* <p>By default, this returns the first element of the {@link #getSupportedMimeTypes()
175-
* supportedMimeTypes}, if any. Can be overridden in sub-classes.
176-
* @param payload the payload being converted to message
177-
* @return the content type, or {@code null} if not known
178-
*/
179-
@Nullable
180-
protected MimeType getDefaultContentType(Object payload) {
181-
List<MimeType> mimeTypes = getSupportedMimeTypes();
182-
return (!mimeTypes.isEmpty() ? mimeTypes.get(0) : null);
183-
}
184-
185173
@Override
186174
@Nullable
187175
public final Object fromMessage(Message<?> message, Class<?> targetClass) {
@@ -197,10 +185,6 @@ public final Object fromMessage(Message<?> message, Class<?> targetClass, @Nulla
197185
return convertFromInternal(message, targetClass, conversionHint);
198186
}
199187

200-
protected boolean canConvertFrom(Message<?> message, Class<?> targetClass) {
201-
return (supports(targetClass) && supportsMimeType(message.getHeaders()));
202-
}
203-
204188
@Override
205189
@Nullable
206190
public final Message<?> toMessage(Object payload, @Nullable MessageHeaders headers) {
@@ -240,6 +224,11 @@ public final Message<?> toMessage(Object payload, @Nullable MessageHeaders heade
240224
return builder.build();
241225
}
242226

227+
228+
protected boolean canConvertFrom(Message<?> message, Class<?> targetClass) {
229+
return (supports(targetClass) && supportsMimeType(message.getHeaders()));
230+
}
231+
243232
protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) {
244233
return (supports(payload.getClass()) && supportsMimeType(headers));
245234
}
@@ -265,6 +254,22 @@ protected MimeType getMimeType(@Nullable MessageHeaders headers) {
265254
return (headers != null && this.contentTypeResolver != null ? this.contentTypeResolver.resolve(headers) : null);
266255
}
267256

257+
/**
258+
* Return the default content type for the payload. Called when
259+
* {@link #toMessage(Object, MessageHeaders)} is invoked without
260+
* message headers or without a content type header.
261+
* <p>By default, this returns the first element of the
262+
* {@link #getSupportedMimeTypes() supportedMimeTypes}, if any.
263+
* Can be overridden in subclasses.
264+
* @param payload the payload being converted to a message
265+
* @return the content type, or {@code null} if not known
266+
*/
267+
@Nullable
268+
protected MimeType getDefaultContentType(Object payload) {
269+
List<MimeType> mimeTypes = getSupportedMimeTypes();
270+
return (!mimeTypes.isEmpty() ? mimeTypes.get(0) : null);
271+
}
272+
268273

269274
/**
270275
* Whether the given class is supported by this converter.
@@ -307,4 +312,19 @@ protected Object convertToInternal(
307312
return null;
308313
}
309314

315+
316+
static Type getResolvedType(Class<?> targetClass, @Nullable Object conversionHint) {
317+
if (conversionHint instanceof MethodParameter) {
318+
MethodParameter param = (MethodParameter) conversionHint;
319+
param = param.nestedIfOptional();
320+
if (Message.class.isAssignableFrom(param.getParameterType())) {
321+
param = param.nested();
322+
}
323+
Type genericParameterType = param.getNestedGenericParameterType();
324+
Class<?> contextClass = param.getContainingClass();
325+
return GenericTypeResolver.resolveType(genericParameterType, contextClass);
326+
}
327+
return targetClass;
328+
}
329+
310330
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.messaging.converter;
18+
19+
import java.io.Reader;
20+
import java.io.Writer;
21+
import java.lang.reflect.ParameterizedType;
22+
import java.lang.reflect.Type;
23+
24+
import com.google.gson.Gson;
25+
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* Implementation of {@link MessageConverter} that can read and write JSON
30+
* using <a href="https://code.google.com/p/google-gson/">Google Gson</a>.
31+
*
32+
* @author Juergen Hoeller
33+
* @since 5.3
34+
* @see com.google.gson.Gson
35+
* @see com.google.gson.GsonBuilder
36+
* @see #setGson
37+
*/
38+
public class GsonMessageConverter extends AbstractJsonMessageConverter {
39+
40+
private Gson gson;
41+
42+
43+
/**
44+
* Construct a new {@code GsonMessageConverter} with default configuration.
45+
*/
46+
public GsonMessageConverter() {
47+
this.gson = new Gson();
48+
}
49+
50+
/**
51+
* Construct a new {@code GsonMessageConverter} with the given delegate.
52+
* @param gson the Gson instance to use
53+
*/
54+
public GsonMessageConverter(Gson gson) {
55+
Assert.notNull(gson, "A Gson instance is required");
56+
this.gson = gson;
57+
}
58+
59+
60+
/**
61+
* Set the {@code Gson} instance to use.
62+
* If not set, a default {@link Gson#Gson() Gson} instance will be used.
63+
* <p>Setting a custom-configured {@code Gson} is one way to take further
64+
* control of the JSON serialization process.
65+
* @see #GsonMessageConverter(Gson)
66+
*/
67+
public void setGson(Gson gson) {
68+
Assert.notNull(gson, "A Gson instance is required");
69+
this.gson = gson;
70+
}
71+
72+
/**
73+
* Return the configured {@code Gson} instance for this converter.
74+
*/
75+
public Gson getGson() {
76+
return this.gson;
77+
}
78+
79+
80+
@Override
81+
protected Object fromJson(Reader reader, Type resolvedType) {
82+
return getGson().fromJson(reader, resolvedType);
83+
}
84+
85+
@Override
86+
protected Object fromJson(String payload, Type resolvedType) {
87+
return getGson().fromJson(payload, resolvedType);
88+
}
89+
90+
@Override
91+
protected void toJson(Object payload, Type resolvedType, Writer writer) {
92+
if (resolvedType instanceof ParameterizedType) {
93+
getGson().toJson(payload, resolvedType, writer);
94+
}
95+
else {
96+
getGson().toJson(payload, writer);
97+
}
98+
}
99+
100+
@Override
101+
protected String toJson(Object payload, Type resolvedType) {
102+
if (resolvedType instanceof ParameterizedType) {
103+
return getGson().toJson(payload, resolvedType);
104+
}
105+
else {
106+
return getGson().toJson(payload);
107+
}
108+
}
109+
110+
}

0 commit comments

Comments
 (0)