001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.messaging.converter; 018 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.StringWriter; 022import java.io.Writer; 023import java.lang.reflect.Type; 024import java.nio.charset.Charset; 025import java.util.concurrent.atomic.AtomicReference; 026 027import com.fasterxml.jackson.annotation.JsonView; 028import com.fasterxml.jackson.core.JsonEncoding; 029import com.fasterxml.jackson.core.JsonGenerator; 030import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; 031import com.fasterxml.jackson.databind.DeserializationFeature; 032import com.fasterxml.jackson.databind.JavaType; 033import com.fasterxml.jackson.databind.JsonMappingException; 034import com.fasterxml.jackson.databind.MapperFeature; 035import com.fasterxml.jackson.databind.ObjectMapper; 036import com.fasterxml.jackson.databind.SerializationFeature; 037 038import org.springframework.core.GenericTypeResolver; 039import org.springframework.core.MethodParameter; 040import org.springframework.lang.Nullable; 041import org.springframework.messaging.Message; 042import org.springframework.messaging.MessageHeaders; 043import org.springframework.util.Assert; 044import org.springframework.util.ClassUtils; 045import org.springframework.util.MimeType; 046 047/** 048 * A Jackson 2 based {@link MessageConverter} implementation. 049 * 050 * <p>It customizes Jackson's default properties with the following ones: 051 * <ul> 052 * <li>{@link MapperFeature#DEFAULT_VIEW_INCLUSION} is disabled</li> 053 * <li>{@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} is disabled</li> 054 * </ul> 055 * 056 * <p>Compatible with Jackson 2.9 and higher, as of Spring 5.1. 057 * 058 * @author Rossen Stoyanchev 059 * @author Juergen Hoeller 060 * @author Sebastien Deleuze 061 * @since 4.0 062 */ 063public class MappingJackson2MessageConverter extends AbstractMessageConverter { 064 065 private ObjectMapper objectMapper; 066 067 @Nullable 068 private Boolean prettyPrint; 069 070 071 /** 072 * Construct a {@code MappingJackson2MessageConverter} supporting 073 * the {@code application/json} MIME type with {@code UTF-8} character set. 074 */ 075 public MappingJackson2MessageConverter() { 076 super(new MimeType("application", "json")); 077 this.objectMapper = initObjectMapper(); 078 } 079 080 /** 081 * Construct a {@code MappingJackson2MessageConverter} supporting 082 * one or more custom MIME types. 083 * @param supportedMimeTypes the supported MIME types 084 * @since 4.1.5 085 */ 086 public MappingJackson2MessageConverter(MimeType... supportedMimeTypes) { 087 super(supportedMimeTypes); 088 this.objectMapper = initObjectMapper(); 089 } 090 091 092 private ObjectMapper initObjectMapper() { 093 ObjectMapper objectMapper = new ObjectMapper(); 094 objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false); 095 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 096 return objectMapper; 097 } 098 099 /** 100 * Set the {@code ObjectMapper} for this converter. 101 * If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used. 102 * <p>Setting a custom-configured {@code ObjectMapper} is one way to take further 103 * control of the JSON serialization process. For example, an extended 104 * {@link com.fasterxml.jackson.databind.ser.SerializerFactory} can be 105 * configured that provides custom serializers for specific types. The other 106 * option for refining the serialization process is to use Jackson's provided 107 * annotations on the types to be serialized, in which case a custom-configured 108 * ObjectMapper is unnecessary. 109 */ 110 public void setObjectMapper(ObjectMapper objectMapper) { 111 Assert.notNull(objectMapper, "ObjectMapper must not be null"); 112 this.objectMapper = objectMapper; 113 configurePrettyPrint(); 114 } 115 116 /** 117 * Return the underlying {@code ObjectMapper} for this converter. 118 */ 119 public ObjectMapper getObjectMapper() { 120 return this.objectMapper; 121 } 122 123 /** 124 * Whether to use the {@link DefaultPrettyPrinter} when writing JSON. 125 * This is a shortcut for setting up an {@code ObjectMapper} as follows: 126 * <pre class="code"> 127 * ObjectMapper mapper = new ObjectMapper(); 128 * mapper.configure(SerializationFeature.INDENT_OUTPUT, true); 129 * converter.setObjectMapper(mapper); 130 * </pre> 131 */ 132 public void setPrettyPrint(boolean prettyPrint) { 133 this.prettyPrint = prettyPrint; 134 configurePrettyPrint(); 135 } 136 137 private void configurePrettyPrint() { 138 if (this.prettyPrint != null) { 139 this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint); 140 } 141 } 142 143 144 @Override 145 protected boolean canConvertFrom(Message<?> message, @Nullable Class<?> targetClass) { 146 if (targetClass == null || !supportsMimeType(message.getHeaders())) { 147 return false; 148 } 149 JavaType javaType = this.objectMapper.constructType(targetClass); 150 AtomicReference<Throwable> causeRef = new AtomicReference<>(); 151 if (this.objectMapper.canDeserialize(javaType, causeRef)) { 152 return true; 153 } 154 logWarningIfNecessary(javaType, causeRef.get()); 155 return false; 156 } 157 158 @Override 159 protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) { 160 if (!supportsMimeType(headers)) { 161 return false; 162 } 163 AtomicReference<Throwable> causeRef = new AtomicReference<>(); 164 if (this.objectMapper.canSerialize(payload.getClass(), causeRef)) { 165 return true; 166 } 167 logWarningIfNecessary(payload.getClass(), causeRef.get()); 168 return false; 169 } 170 171 /** 172 * Determine whether to log the given exception coming from a 173 * {@link ObjectMapper#canDeserialize} / {@link ObjectMapper#canSerialize} check. 174 * @param type the class that Jackson tested for (de-)serializability 175 * @param cause the Jackson-thrown exception to evaluate 176 * (typically a {@link JsonMappingException}) 177 * @since 4.3 178 */ 179 protected void logWarningIfNecessary(Type type, @Nullable Throwable cause) { 180 if (cause == null) { 181 return; 182 } 183 184 // Do not log warning for serializer not found (note: different message wording on Jackson 2.9) 185 boolean debugLevel = (cause instanceof JsonMappingException && cause.getMessage().startsWith("Cannot find")); 186 187 if (debugLevel ? logger.isDebugEnabled() : logger.isWarnEnabled()) { 188 String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") + 189 "serialization for type [" + type + "]"; 190 if (debugLevel) { 191 logger.debug(msg, cause); 192 } 193 else if (logger.isDebugEnabled()) { 194 logger.warn(msg, cause); 195 } 196 else { 197 logger.warn(msg + ": " + cause); 198 } 199 } 200 } 201 202 @Override 203 protected boolean supports(Class<?> clazz) { 204 // should not be called, since we override canConvertFrom/canConvertTo instead 205 throw new UnsupportedOperationException(); 206 } 207 208 @Override 209 @Nullable 210 protected Object convertFromInternal(Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) { 211 JavaType javaType = getJavaType(targetClass, conversionHint); 212 Object payload = message.getPayload(); 213 Class<?> view = getSerializationView(conversionHint); 214 try { 215 if (ClassUtils.isAssignableValue(targetClass, payload)) { 216 return payload; 217 } 218 else if (payload instanceof byte[]) { 219 if (view != null) { 220 return this.objectMapper.readerWithView(view).forType(javaType).readValue((byte[]) payload); 221 } 222 else { 223 return this.objectMapper.readValue((byte[]) payload, javaType); 224 } 225 } 226 else { 227 // Assuming a text-based source payload 228 if (view != null) { 229 return this.objectMapper.readerWithView(view).forType(javaType).readValue(payload.toString()); 230 } 231 else { 232 return this.objectMapper.readValue(payload.toString(), javaType); 233 } 234 } 235 } 236 catch (IOException ex) { 237 throw new MessageConversionException(message, "Could not read JSON: " + ex.getMessage(), ex); 238 } 239 } 240 241 private JavaType getJavaType(Class<?> targetClass, @Nullable Object conversionHint) { 242 if (conversionHint instanceof MethodParameter) { 243 MethodParameter param = (MethodParameter) conversionHint; 244 param = param.nestedIfOptional(); 245 if (Message.class.isAssignableFrom(param.getParameterType())) { 246 param = param.nested(); 247 } 248 Type genericParameterType = param.getNestedGenericParameterType(); 249 Class<?> contextClass = param.getContainingClass(); 250 Type type = GenericTypeResolver.resolveType(genericParameterType, contextClass); 251 return this.objectMapper.constructType(type); 252 } 253 return this.objectMapper.constructType(targetClass); 254 } 255 256 @Override 257 @Nullable 258 protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, 259 @Nullable Object conversionHint) { 260 261 try { 262 Class<?> view = getSerializationView(conversionHint); 263 if (byte[].class == getSerializedPayloadClass()) { 264 ByteArrayOutputStream out = new ByteArrayOutputStream(1024); 265 JsonEncoding encoding = getJsonEncoding(getMimeType(headers)); 266 try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(out, encoding)) { 267 if (view != null) { 268 this.objectMapper.writerWithView(view).writeValue(generator, payload); 269 } 270 else { 271 this.objectMapper.writeValue(generator, payload); 272 } 273 payload = out.toByteArray(); 274 } 275 } 276 else { 277 // Assuming a text-based target payload 278 Writer writer = new StringWriter(1024); 279 if (view != null) { 280 this.objectMapper.writerWithView(view).writeValue(writer, payload); 281 } 282 else { 283 this.objectMapper.writeValue(writer, payload); 284 } 285 payload = writer.toString(); 286 } 287 } 288 catch (IOException ex) { 289 throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex); 290 } 291 return payload; 292 } 293 294 /** 295 * Determine a Jackson serialization view based on the given conversion hint. 296 * @param conversionHint the conversion hint Object as passed into the 297 * converter for the current conversion attempt 298 * @return the serialization view class, or {@code null} if none 299 * @since 4.2 300 */ 301 @Nullable 302 protected Class<?> getSerializationView(@Nullable Object conversionHint) { 303 if (conversionHint instanceof MethodParameter) { 304 MethodParameter param = (MethodParameter) conversionHint; 305 JsonView annotation = (param.getParameterIndex() >= 0 ? 306 param.getParameterAnnotation(JsonView.class) : param.getMethodAnnotation(JsonView.class)); 307 if (annotation != null) { 308 return extractViewClass(annotation, conversionHint); 309 } 310 } 311 else if (conversionHint instanceof JsonView) { 312 return extractViewClass((JsonView) conversionHint, conversionHint); 313 } 314 else if (conversionHint instanceof Class) { 315 return (Class<?>) conversionHint; 316 } 317 318 // No JSON view specified... 319 return null; 320 } 321 322 private Class<?> extractViewClass(JsonView annotation, Object conversionHint) { 323 Class<?>[] classes = annotation.value(); 324 if (classes.length != 1) { 325 throw new IllegalArgumentException( 326 "@JsonView only supported for handler methods with exactly 1 class argument: " + conversionHint); 327 } 328 return classes[0]; 329 } 330 331 /** 332 * Determine the JSON encoding to use for the given content type. 333 * @param contentType the MIME type from the MessageHeaders, if any 334 * @return the JSON encoding to use (never {@code null}) 335 */ 336 protected JsonEncoding getJsonEncoding(@Nullable MimeType contentType) { 337 if (contentType != null && contentType.getCharset() != null) { 338 Charset charset = contentType.getCharset(); 339 for (JsonEncoding encoding : JsonEncoding.values()) { 340 if (charset.name().equals(encoding.getJavaName())) { 341 return encoding; 342 } 343 } 344 } 345 return JsonEncoding.UTF8; 346 } 347 348}