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.ParameterizedType; 024import java.lang.reflect.Type; 025import java.lang.reflect.TypeVariable; 026import java.nio.charset.Charset; 027import java.util.Arrays; 028import java.util.concurrent.atomic.AtomicReference; 029 030import com.fasterxml.jackson.annotation.JsonView; 031import com.fasterxml.jackson.core.JsonEncoding; 032import com.fasterxml.jackson.core.JsonGenerator; 033import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; 034import com.fasterxml.jackson.databind.DeserializationFeature; 035import com.fasterxml.jackson.databind.JavaType; 036import com.fasterxml.jackson.databind.JsonMappingException; 037import com.fasterxml.jackson.databind.MapperFeature; 038import com.fasterxml.jackson.databind.ObjectMapper; 039import com.fasterxml.jackson.databind.SerializationFeature; 040import com.fasterxml.jackson.databind.type.TypeFactory; 041 042import org.springframework.core.MethodParameter; 043import org.springframework.core.ResolvableType; 044import org.springframework.messaging.Message; 045import org.springframework.messaging.MessageHeaders; 046import org.springframework.util.Assert; 047import org.springframework.util.MimeType; 048 049/** 050 * A Jackson 2 based {@link MessageConverter} implementation. 051 * 052 * <p>It customizes Jackson's default properties with the following ones: 053 * <ul> 054 * <li>{@link MapperFeature#DEFAULT_VIEW_INCLUSION} is disabled</li> 055 * <li>{@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} is disabled</li> 056 * </ul> 057 * 058 * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. 059 * 060 * @author Rossen Stoyanchev 061 * @author Juergen Hoeller 062 * @author Sebastien Deleuze 063 * @since 4.0 064 */ 065public class MappingJackson2MessageConverter extends AbstractMessageConverter { 066 067 private ObjectMapper objectMapper; 068 069 private Boolean prettyPrint; 070 071 072 /** 073 * Construct a {@code MappingJackson2MessageConverter} supporting 074 * the {@code application/json} MIME type with {@code UTF-8} character set. 075 */ 076 public MappingJackson2MessageConverter() { 077 super(new MimeType("application", "json", Charset.forName("UTF-8"))); 078 initObjectMapper(); 079 } 080 081 /** 082 * Construct a {@code MappingJackson2MessageConverter} supporting 083 * one or more custom MIME types. 084 * @param supportedMimeTypes the supported MIME types 085 * @since 4.1.5 086 */ 087 public MappingJackson2MessageConverter(MimeType... supportedMimeTypes) { 088 super(Arrays.asList(supportedMimeTypes)); 089 initObjectMapper(); 090 } 091 092 093 private void initObjectMapper() { 094 this.objectMapper = new ObjectMapper(); 095 this.objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false); 096 this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 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, Class<?> targetClass) { 146 if (targetClass == null || !supportsMimeType(message.getHeaders())) { 147 return false; 148 } 149 JavaType javaType = this.objectMapper.getTypeFactory().constructType(targetClass); 150 AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>(); 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, MessageHeaders headers) { 160 if (payload == null || !supportsMimeType(headers)) { 161 return false; 162 } 163 AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>(); 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, 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 && 186 (cause.getMessage().startsWith("Can not find") || cause.getMessage().startsWith("Cannot find"))); 187 188 if (debugLevel ? logger.isDebugEnabled() : logger.isWarnEnabled()) { 189 String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") + 190 "serialization for type [" + type + "]"; 191 if (debugLevel) { 192 logger.debug(msg, cause); 193 } 194 else if (logger.isDebugEnabled()) { 195 logger.warn(msg, cause); 196 } 197 else { 198 logger.warn(msg + ": " + cause); 199 } 200 } 201 } 202 203 @Override 204 protected boolean supports(Class<?> clazz) { 205 // should not be called, since we override canConvertFrom/canConvertTo instead 206 throw new UnsupportedOperationException(); 207 } 208 209 @Override 210 protected Object convertFromInternal(Message<?> message, Class<?> targetClass, Object conversionHint) { 211 JavaType javaType = getJavaType(targetClass, conversionHint); 212 Object payload = message.getPayload(); 213 Class<?> view = getSerializationView(conversionHint); 214 // Note: in the view case, calling withType instead of forType for compatibility with Jackson <2.5 215 try { 216 if (payload instanceof byte[]) { 217 if (view != null) { 218 return this.objectMapper.readerWithView(view).forType(javaType).readValue((byte[]) payload); 219 } 220 else { 221 return this.objectMapper.readValue((byte[]) payload, javaType); 222 } 223 } 224 else { 225 // Assuming a text-based source payload 226 if (view != null) { 227 return this.objectMapper.readerWithView(view).forType(javaType).readValue(payload.toString()); 228 } 229 else { 230 return this.objectMapper.readValue(payload.toString(), javaType); 231 } 232 } 233 } 234 catch (IOException ex) { 235 throw new MessageConversionException(message, "Could not read JSON: " + ex.getMessage(), ex); 236 } 237 } 238 239 private JavaType getJavaType(Class<?> targetClass, Object conversionHint) { 240 if (conversionHint instanceof MethodParameter) { 241 MethodParameter param = (MethodParameter) conversionHint; 242 param = param.nestedIfOptional(); 243 if (Message.class.isAssignableFrom(param.getParameterType())) { 244 param = param.nested(); 245 } 246 Type genericParameterType = param.getNestedGenericParameterType(); 247 Class<?> contextClass = param.getContainingClass(); 248 return getJavaType(genericParameterType, contextClass); 249 } 250 return this.objectMapper.getTypeFactory().constructType(targetClass); 251 } 252 253 private JavaType getJavaType(Type type, Class<?> contextClass) { 254 TypeFactory typeFactory = this.objectMapper.getTypeFactory(); 255 if (contextClass != null) { 256 ResolvableType resolvedType = ResolvableType.forType(type); 257 if (type instanceof TypeVariable) { 258 ResolvableType resolvedTypeVariable = resolveVariable( 259 (TypeVariable<?>) type, ResolvableType.forClass(contextClass)); 260 if (resolvedTypeVariable != ResolvableType.NONE) { 261 return typeFactory.constructType(resolvedTypeVariable.resolve()); 262 } 263 } 264 else if (type instanceof ParameterizedType && resolvedType.hasUnresolvableGenerics()) { 265 ParameterizedType parameterizedType = (ParameterizedType) type; 266 Class<?>[] generics = new Class<?>[parameterizedType.getActualTypeArguments().length]; 267 Type[] typeArguments = parameterizedType.getActualTypeArguments(); 268 for (int i = 0; i < typeArguments.length; i++) { 269 Type typeArgument = typeArguments[i]; 270 if (typeArgument instanceof TypeVariable) { 271 ResolvableType resolvedTypeArgument = resolveVariable( 272 (TypeVariable<?>) typeArgument, ResolvableType.forClass(contextClass)); 273 if (resolvedTypeArgument != ResolvableType.NONE) { 274 generics[i] = resolvedTypeArgument.resolve(); 275 } 276 else { 277 generics[i] = ResolvableType.forType(typeArgument).resolve(); 278 } 279 } 280 else { 281 generics[i] = ResolvableType.forType(typeArgument).resolve(); 282 } 283 } 284 return typeFactory.constructType(ResolvableType. 285 forClassWithGenerics(resolvedType.getRawClass(), generics).getType()); 286 } 287 } 288 return typeFactory.constructType(type); 289 } 290 291 private ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) { 292 ResolvableType resolvedType; 293 if (contextType.hasGenerics()) { 294 resolvedType = ResolvableType.forType(typeVariable, contextType); 295 if (resolvedType.resolve() != null) { 296 return resolvedType; 297 } 298 } 299 300 ResolvableType superType = contextType.getSuperType(); 301 if (superType != ResolvableType.NONE) { 302 resolvedType = resolveVariable(typeVariable, superType); 303 if (resolvedType.resolve() != null) { 304 return resolvedType; 305 } 306 } 307 for (ResolvableType ifc : contextType.getInterfaces()) { 308 resolvedType = resolveVariable(typeVariable, ifc); 309 if (resolvedType.resolve() != null) { 310 return resolvedType; 311 } 312 } 313 return ResolvableType.NONE; 314 } 315 316 @Override 317 protected Object convertToInternal(Object payload, MessageHeaders headers, Object conversionHint) { 318 try { 319 Class<?> view = getSerializationView(conversionHint); 320 if (byte[].class == getSerializedPayloadClass()) { 321 ByteArrayOutputStream out = new ByteArrayOutputStream(1024); 322 JsonEncoding encoding = getJsonEncoding(getMimeType(headers)); 323 JsonGenerator generator = this.objectMapper.getFactory().createGenerator(out, encoding); 324 if (view != null) { 325 this.objectMapper.writerWithView(view).writeValue(generator, payload); 326 } 327 else { 328 this.objectMapper.writeValue(generator, payload); 329 } 330 payload = out.toByteArray(); 331 } 332 else { 333 // Assuming a text-based target payload 334 Writer writer = new StringWriter(1024); 335 if (view != null) { 336 this.objectMapper.writerWithView(view).writeValue(writer, payload); 337 } 338 else { 339 this.objectMapper.writeValue(writer, payload); 340 } 341 payload = writer.toString(); 342 } 343 } 344 catch (IOException ex) { 345 throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex); 346 } 347 return payload; 348 } 349 350 /** 351 * Determine a Jackson serialization view based on the given conversion hint. 352 * @param conversionHint the conversion hint Object as passed into the 353 * converter for the current conversion attempt 354 * @return the serialization view class, or {@code null} if none 355 * @since 4.2 356 */ 357 protected Class<?> getSerializationView(Object conversionHint) { 358 if (conversionHint instanceof MethodParameter) { 359 MethodParameter param = (MethodParameter) conversionHint; 360 JsonView annotation = (param.getParameterIndex() >= 0 ? 361 param.getParameterAnnotation(JsonView.class) : param.getMethodAnnotation(JsonView.class)); 362 if (annotation != null) { 363 return extractViewClass(annotation, conversionHint); 364 } 365 } 366 else if (conversionHint instanceof JsonView) { 367 return extractViewClass((JsonView) conversionHint, conversionHint); 368 } 369 else if (conversionHint instanceof Class) { 370 return (Class<?>) conversionHint; 371 } 372 373 // No JSON view specified... 374 return null; 375 } 376 377 private Class<?> extractViewClass(JsonView annotation, Object conversionHint) { 378 Class<?>[] classes = annotation.value(); 379 if (classes.length != 1) { 380 throw new IllegalArgumentException( 381 "@JsonView only supported for handler methods with exactly 1 class argument: " + conversionHint); 382 } 383 return classes[0]; 384 } 385 386 /** 387 * Determine the JSON encoding to use for the given content type. 388 * @param contentType the MIME type from the MessageHeaders, if any 389 * @return the JSON encoding to use (never {@code null}) 390 */ 391 protected JsonEncoding getJsonEncoding(MimeType contentType) { 392 if (contentType != null && contentType.getCharset() != null) { 393 Charset charset = contentType.getCharset(); 394 for (JsonEncoding encoding : JsonEncoding.values()) { 395 if (charset.name().equals(encoding.getJavaName())) { 396 return encoding; 397 } 398 } 399 } 400 return JsonEncoding.UTF8; 401 } 402 403}