Browse Source
This commit introduces a JacksonJsonMessageConverter Jackson 3 variant of MappingJackson2MessageConverter. See gh-33798pull/34893/head
8 changed files with 272 additions and 14 deletions
@ -0,0 +1,236 @@
@@ -0,0 +1,236 @@
|
||||
/* |
||||
* Copyright 2002-2025 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 |
||||
* |
||||
* https://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.messaging.converter; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.StringWriter; |
||||
import java.io.Writer; |
||||
import java.nio.charset.Charset; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView; |
||||
import org.jspecify.annotations.Nullable; |
||||
import tools.jackson.core.JacksonException; |
||||
import tools.jackson.core.JsonEncoding; |
||||
import tools.jackson.core.JsonGenerator; |
||||
import tools.jackson.databind.JavaType; |
||||
import tools.jackson.databind.ObjectMapper; |
||||
import tools.jackson.databind.cfg.MapperBuilder; |
||||
import tools.jackson.databind.json.JsonMapper; |
||||
|
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.messaging.Message; |
||||
import org.springframework.messaging.MessageHeaders; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.MimeType; |
||||
|
||||
/** |
||||
* A Jackson 3.x based {@link MessageConverter} implementation. |
||||
* |
||||
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s |
||||
* found by {@link MapperBuilder#findModules(ClassLoader)}. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @since 7.0 |
||||
*/ |
||||
public class JacksonJsonMessageConverter extends AbstractMessageConverter { |
||||
|
||||
private static final MimeType[] DEFAULT_MIME_TYPES = new MimeType[] { |
||||
new MimeType("application", "json"), new MimeType("application", "*+json")}; |
||||
|
||||
private final ObjectMapper objectMapper; |
||||
|
||||
|
||||
/** |
||||
* Construct a new instance with a {@link JsonMapper} customized with the |
||||
* {@link tools.jackson.databind.JacksonModule}s found by |
||||
* {@link MapperBuilder#findModules(ClassLoader)}. |
||||
*/ |
||||
public JacksonJsonMessageConverter() { |
||||
this(DEFAULT_MIME_TYPES); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new instance with a {@link JsonMapper} customized |
||||
* with the {@link tools.jackson.databind.JacksonModule}s found |
||||
* by {@link MapperBuilder#findModules(ClassLoader)} and the |
||||
* provided {@link MimeType}s. |
||||
* @param supportedMimeTypes the supported MIME types |
||||
*/ |
||||
public JacksonJsonMessageConverter(MimeType... supportedMimeTypes) { |
||||
super(supportedMimeTypes); |
||||
this.objectMapper = JsonMapper.builder().findAndAddModules(JacksonJsonMessageConverter.class.getClassLoader()).build(); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new instance with the provided {@link ObjectMapper}. |
||||
* @see JsonMapper#builder() |
||||
* @see MapperBuilder#findModules(ClassLoader) |
||||
*/ |
||||
public JacksonJsonMessageConverter(ObjectMapper objectMapper) { |
||||
this(objectMapper, DEFAULT_MIME_TYPES); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new instance with the provided {@link ObjectMapper} and the |
||||
* provided {@link MimeType}s. |
||||
* @see JsonMapper#builder() |
||||
* @see MapperBuilder#findModules(ClassLoader) |
||||
*/ |
||||
public JacksonJsonMessageConverter(ObjectMapper objectMapper, MimeType... supportedMimeTypes) { |
||||
super(supportedMimeTypes); |
||||
Assert.notNull(objectMapper, "ObjectMapper must not be null"); |
||||
this.objectMapper = objectMapper; |
||||
} |
||||
|
||||
@Override |
||||
protected boolean canConvertFrom(Message<?> message, @Nullable Class<?> targetClass) { |
||||
return targetClass != null && supportsMimeType(message.getHeaders()); |
||||
} |
||||
|
||||
@Override |
||||
protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) { |
||||
return supportsMimeType(headers); |
||||
} |
||||
|
||||
@Override |
||||
protected boolean supports(Class<?> clazz) { |
||||
// should not be called, since we override canConvertFrom/canConvertTo instead
|
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
@Override |
||||
protected @Nullable Object convertFromInternal(Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) { |
||||
JavaType javaType = this.objectMapper.constructType(getResolvedType(targetClass, conversionHint)); |
||||
Object payload = message.getPayload(); |
||||
Class<?> view = getSerializationView(conversionHint); |
||||
try { |
||||
if (ClassUtils.isAssignableValue(targetClass, payload)) { |
||||
return payload; |
||||
} |
||||
else if (payload instanceof byte[] bytes) { |
||||
if (view != null) { |
||||
return this.objectMapper.readerWithView(view).forType(javaType).readValue(bytes); |
||||
} |
||||
else { |
||||
return this.objectMapper.readValue(bytes, javaType); |
||||
} |
||||
} |
||||
else { |
||||
// Assuming a text-based source payload
|
||||
if (view != null) { |
||||
return this.objectMapper.readerWithView(view).forType(javaType).readValue(payload.toString()); |
||||
} |
||||
else { |
||||
return this.objectMapper.readValue(payload.toString(), javaType); |
||||
} |
||||
} |
||||
} |
||||
catch (JacksonException ex) { |
||||
throw new MessageConversionException(message, "Could not read JSON: " + ex.getMessage(), ex); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected @Nullable Object convertToInternal(Object payload, @Nullable MessageHeaders headers, |
||||
@Nullable Object conversionHint) { |
||||
|
||||
try { |
||||
Class<?> view = getSerializationView(conversionHint); |
||||
if (byte[].class == getSerializedPayloadClass()) { |
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(1024); |
||||
JsonEncoding encoding = getJsonEncoding(getMimeType(headers)); |
||||
try (JsonGenerator generator = this.objectMapper.createGenerator(out, encoding)) { |
||||
if (view != null) { |
||||
this.objectMapper.writerWithView(view).writeValue(generator, payload); |
||||
} |
||||
else { |
||||
this.objectMapper.writeValue(generator, payload); |
||||
} |
||||
payload = out.toByteArray(); |
||||
} |
||||
} |
||||
else { |
||||
// Assuming a text-based target payload
|
||||
Writer writer = new StringWriter(1024); |
||||
if (view != null) { |
||||
this.objectMapper.writerWithView(view).writeValue(writer, payload); |
||||
} |
||||
else { |
||||
this.objectMapper.writeValue(writer, payload); |
||||
} |
||||
payload = writer.toString(); |
||||
} |
||||
} |
||||
catch (JacksonException ex) { |
||||
throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex); |
||||
} |
||||
return payload; |
||||
} |
||||
|
||||
/** |
||||
* Determine a Jackson serialization view based on the given conversion hint. |
||||
* @param conversionHint the conversion hint Object as passed into the |
||||
* converter for the current conversion attempt |
||||
* @return the serialization view class, or {@code null} if none |
||||
*/ |
||||
protected @Nullable Class<?> getSerializationView(@Nullable Object conversionHint) { |
||||
if (conversionHint instanceof MethodParameter param) { |
||||
JsonView annotation = (param.getParameterIndex() >= 0 ? |
||||
param.getParameterAnnotation(JsonView.class) : param.getMethodAnnotation(JsonView.class)); |
||||
if (annotation != null) { |
||||
return extractViewClass(annotation, conversionHint); |
||||
} |
||||
} |
||||
else if (conversionHint instanceof JsonView jsonView) { |
||||
return extractViewClass(jsonView, conversionHint); |
||||
} |
||||
else if (conversionHint instanceof Class<?> clazz) { |
||||
return clazz; |
||||
} |
||||
|
||||
// No JSON view specified...
|
||||
return null; |
||||
} |
||||
|
||||
private Class<?> extractViewClass(JsonView annotation, Object conversionHint) { |
||||
Class<?>[] classes = annotation.value(); |
||||
if (classes.length != 1) { |
||||
throw new IllegalArgumentException( |
||||
"@JsonView only supported for handler methods with exactly 1 class argument: " + conversionHint); |
||||
} |
||||
return classes[0]; |
||||
} |
||||
|
||||
/** |
||||
* Determine the JSON encoding to use for the given content type. |
||||
* @param contentType the MIME type from the MessageHeaders, if any |
||||
* @return the JSON encoding to use (never {@code null}) |
||||
*/ |
||||
protected JsonEncoding getJsonEncoding(@Nullable MimeType contentType) { |
||||
if (contentType != null && contentType.getCharset() != null) { |
||||
Charset charset = contentType.getCharset(); |
||||
for (JsonEncoding encoding : JsonEncoding.values()) { |
||||
if (charset.name().equals(encoding.getJavaName())) { |
||||
return encoding; |
||||
} |
||||
} |
||||
} |
||||
return JsonEncoding.UTF8; |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue