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.jms.support.converter; 018 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.OutputStreamWriter; 022import java.io.StringWriter; 023import java.io.UnsupportedEncodingException; 024import java.util.HashMap; 025import java.util.Map; 026import javax.jms.BytesMessage; 027import javax.jms.JMSException; 028import javax.jms.Message; 029import javax.jms.Session; 030import javax.jms.TextMessage; 031 032import com.fasterxml.jackson.annotation.JsonView; 033import com.fasterxml.jackson.databind.DeserializationFeature; 034import com.fasterxml.jackson.databind.JavaType; 035import com.fasterxml.jackson.databind.MapperFeature; 036import com.fasterxml.jackson.databind.ObjectMapper; 037import com.fasterxml.jackson.databind.ObjectWriter; 038 039import org.springframework.beans.factory.BeanClassLoaderAware; 040import org.springframework.core.MethodParameter; 041import org.springframework.util.Assert; 042import org.springframework.util.ClassUtils; 043 044/** 045 * Message converter that uses Jackson 2.x to convert messages to and from JSON. 046 * Maps an object to a {@link BytesMessage}, or to a {@link TextMessage} if the 047 * {@link #setTargetType targetType} is set to {@link MessageType#TEXT}. 048 * Converts from a {@link TextMessage} or {@link BytesMessage} to an object. 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.6 and higher, as of Spring 4.3. 057 * 058 * @author Mark Pollack 059 * @author Dave Syer 060 * @author Juergen Hoeller 061 * @author Stephane Nicoll 062 * @since 3.1.4 063 */ 064public class MappingJackson2MessageConverter implements SmartMessageConverter, BeanClassLoaderAware { 065 066 /** 067 * The default encoding used for writing to text messages: UTF-8. 068 */ 069 public static final String DEFAULT_ENCODING = "UTF-8"; 070 071 072 private ObjectMapper objectMapper; 073 074 private MessageType targetType = MessageType.BYTES; 075 076 private String encoding = DEFAULT_ENCODING; 077 078 private String encodingPropertyName; 079 080 private String typeIdPropertyName; 081 082 private Map<String, Class<?>> idClassMappings = new HashMap<String, Class<?>>(); 083 084 private Map<Class<?>, String> classIdMappings = new HashMap<Class<?>, String>(); 085 086 private ClassLoader beanClassLoader; 087 088 089 public MappingJackson2MessageConverter() { 090 this.objectMapper = new ObjectMapper(); 091 this.objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false); 092 this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 093 } 094 095 /** 096 * Specify the {@link ObjectMapper} to use instead of using the default. 097 */ 098 public void setObjectMapper(ObjectMapper objectMapper) { 099 Assert.notNull(objectMapper, "ObjectMapper must not be null"); 100 this.objectMapper = objectMapper; 101 } 102 103 /** 104 * Specify whether {@link #toMessage(Object, Session)} should marshal to a 105 * {@link BytesMessage} or a {@link TextMessage}. 106 * <p>The default is {@link MessageType#BYTES}, i.e. this converter marshals to 107 * a {@link BytesMessage}. Note that the default version of this converter 108 * supports {@link MessageType#BYTES} and {@link MessageType#TEXT} only. 109 * @see MessageType#BYTES 110 * @see MessageType#TEXT 111 */ 112 public void setTargetType(MessageType targetType) { 113 Assert.notNull(targetType, "MessageType must not be null"); 114 this.targetType = targetType; 115 } 116 117 /** 118 * Specify the encoding to use when converting to and from text-based 119 * message body content. The default encoding will be "UTF-8". 120 * <p>When reading from a text-based message, an encoding may have been 121 * suggested through a special JMS property which will then be preferred 122 * over the encoding set on this MessageConverter instance. 123 * @see #setEncodingPropertyName 124 */ 125 public void setEncoding(String encoding) { 126 this.encoding = encoding; 127 } 128 129 /** 130 * Specify the name of the JMS message property that carries the encoding from 131 * bytes to String and back is BytesMessage is used during the conversion process. 132 * <p>Default is none. Setting this property is optional; if not set, UTF-8 will 133 * be used for decoding any incoming bytes message. 134 * @see #setEncoding 135 */ 136 public void setEncodingPropertyName(String encodingPropertyName) { 137 this.encodingPropertyName = encodingPropertyName; 138 } 139 140 /** 141 * Specify the name of the JMS message property that carries the type id for the 142 * contained object: either a mapped id value or a raw Java class name. 143 * <p>Default is none. <b>NOTE: This property needs to be set in order to allow 144 * for converting from an incoming message to a Java object.</b> 145 * @see #setTypeIdMappings 146 */ 147 public void setTypeIdPropertyName(String typeIdPropertyName) { 148 this.typeIdPropertyName = typeIdPropertyName; 149 } 150 151 /** 152 * Specify mappings from type ids to Java classes, if desired. 153 * This allows for synthetic ids in the type id message property, 154 * instead of transferring Java class names. 155 * <p>Default is no custom mappings, i.e. transferring raw Java class names. 156 * @param typeIdMappings a Map with type id values as keys and Java classes as values 157 */ 158 public void setTypeIdMappings(Map<String, Class<?>> typeIdMappings) { 159 this.idClassMappings = new HashMap<String, Class<?>>(); 160 for (Map.Entry<String, Class<?>> entry : typeIdMappings.entrySet()) { 161 String id = entry.getKey(); 162 Class<?> clazz = entry.getValue(); 163 this.idClassMappings.put(id, clazz); 164 this.classIdMappings.put(clazz, id); 165 } 166 } 167 168 @Override 169 public void setBeanClassLoader(ClassLoader classLoader) { 170 this.beanClassLoader = classLoader; 171 } 172 173 174 @Override 175 public Message toMessage(Object object, Session session) throws JMSException, MessageConversionException { 176 Message message; 177 try { 178 switch (this.targetType) { 179 case TEXT: 180 message = mapToTextMessage(object, session, this.objectMapper); 181 break; 182 case BYTES: 183 message = mapToBytesMessage(object, session, this.objectMapper); 184 break; 185 default: 186 message = mapToMessage(object, session, this.objectMapper, this.targetType); 187 } 188 } 189 catch (IOException ex) { 190 throw new MessageConversionException("Could not map JSON object [" + object + "]", ex); 191 } 192 setTypeIdOnMessage(object, message); 193 return message; 194 } 195 196 @Override 197 public Message toMessage(Object object, Session session, Object conversionHint) 198 throws JMSException, MessageConversionException { 199 200 return toMessage(object, session, getSerializationView(conversionHint)); 201 } 202 203 /** 204 * Convert a Java object to a JMS Message using the specified json view 205 * and the supplied session to create the message object. 206 * @param object the object to convert 207 * @param session the Session to use for creating a JMS Message 208 * @param jsonView the view to use to filter the content 209 * @return the JMS Message 210 * @throws javax.jms.JMSException if thrown by JMS API methods 211 * @throws MessageConversionException in case of conversion failure 212 * @since 4.3 213 */ 214 public Message toMessage(Object object, Session session, Class<?> jsonView) 215 throws JMSException, MessageConversionException { 216 217 if (jsonView != null) { 218 return toMessage(object, session, this.objectMapper.writerWithView(jsonView)); 219 } 220 else { 221 return toMessage(object, session, this.objectMapper.writer()); 222 } 223 } 224 225 @Override 226 public Object fromMessage(Message message) throws JMSException, MessageConversionException { 227 try { 228 JavaType targetJavaType = getJavaTypeForMessage(message); 229 return convertToObject(message, targetJavaType); 230 } 231 catch (IOException ex) { 232 throw new MessageConversionException("Failed to convert JSON message content", ex); 233 } 234 } 235 236 protected Message toMessage(Object object, Session session, ObjectWriter objectWriter) 237 throws JMSException, MessageConversionException { 238 239 Message message; 240 try { 241 switch (this.targetType) { 242 case TEXT: 243 message = mapToTextMessage(object, session, objectWriter); 244 break; 245 case BYTES: 246 message = mapToBytesMessage(object, session, objectWriter); 247 break; 248 default: 249 message = mapToMessage(object, session, objectWriter, this.targetType); 250 } 251 } 252 catch (IOException ex) { 253 throw new MessageConversionException("Could not map JSON object [" + object + "]", ex); 254 } 255 setTypeIdOnMessage(object, message); 256 return message; 257 } 258 259 260 /** 261 * Map the given object to a {@link TextMessage}. 262 * @param object the object to be mapped 263 * @param session current JMS session 264 * @param objectMapper the mapper to use 265 * @return the resulting message 266 * @throws JMSException if thrown by JMS methods 267 * @throws IOException in case of I/O errors 268 * @see Session#createBytesMessage 269 * @deprecated as of 4.3, use {@link #mapToTextMessage(Object, Session, ObjectWriter)} 270 */ 271 @Deprecated 272 protected TextMessage mapToTextMessage(Object object, Session session, ObjectMapper objectMapper) 273 throws JMSException, IOException { 274 275 return mapToTextMessage(object, session, objectMapper.writer()); 276 } 277 278 /** 279 * Map the given object to a {@link TextMessage}. 280 * @param object the object to be mapped 281 * @param session current JMS session 282 * @param objectWriter the writer to use 283 * @return the resulting message 284 * @throws JMSException if thrown by JMS methods 285 * @throws IOException in case of I/O errors 286 * @since 4.3 287 * @see Session#createBytesMessage 288 */ 289 protected TextMessage mapToTextMessage(Object object, Session session, ObjectWriter objectWriter) 290 throws JMSException, IOException { 291 292 StringWriter writer = new StringWriter(1024); 293 objectWriter.writeValue(writer, object); 294 return session.createTextMessage(writer.toString()); 295 } 296 297 /** 298 * Map the given object to a {@link BytesMessage}. 299 * @param object the object to be mapped 300 * @param session current JMS session 301 * @param objectMapper the mapper to use 302 * @return the resulting message 303 * @throws JMSException if thrown by JMS methods 304 * @throws IOException in case of I/O errors 305 * @see Session#createBytesMessage 306 * @deprecated as of 4.3, use {@link #mapToBytesMessage(Object, Session, ObjectWriter)} 307 */ 308 @Deprecated 309 protected BytesMessage mapToBytesMessage(Object object, Session session, ObjectMapper objectMapper) 310 throws JMSException, IOException { 311 312 return mapToBytesMessage(object, session, objectMapper.writer()); 313 } 314 315 316 /** 317 * Map the given object to a {@link BytesMessage}. 318 * @param object the object to be mapped 319 * @param session current JMS session 320 * @param objectWriter the writer to use 321 * @return the resulting message 322 * @throws JMSException if thrown by JMS methods 323 * @throws IOException in case of I/O errors 324 * @since 4.3 325 * @see Session#createBytesMessage 326 */ 327 protected BytesMessage mapToBytesMessage(Object object, Session session, ObjectWriter objectWriter) 328 throws JMSException, IOException { 329 330 ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); 331 OutputStreamWriter writer = new OutputStreamWriter(bos, this.encoding); 332 objectWriter.writeValue(writer, object); 333 334 BytesMessage message = session.createBytesMessage(); 335 message.writeBytes(bos.toByteArray()); 336 if (this.encodingPropertyName != null) { 337 message.setStringProperty(this.encodingPropertyName, this.encoding); 338 } 339 return message; 340 } 341 342 /** 343 * Template method that allows for custom message mapping. 344 * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or 345 * {@link MessageType#BYTES}. 346 * <p>The default implementation throws an {@link IllegalArgumentException}. 347 * @param object the object to marshal 348 * @param session the JMS Session 349 * @param objectMapper the mapper to use 350 * @param targetType the target message type (other than TEXT or BYTES) 351 * @return the resulting message 352 * @throws JMSException if thrown by JMS methods 353 * @throws IOException in case of I/O errors 354 * @deprecated as of 4.3, use {@link #mapToMessage(Object, Session, ObjectWriter, MessageType)} 355 */ 356 @Deprecated 357 protected Message mapToMessage(Object object, Session session, ObjectMapper objectMapper, MessageType targetType) 358 throws JMSException, IOException { 359 360 return mapToMessage(object, session, objectMapper.writer(), targetType); 361 } 362 363 /** 364 * Template method that allows for custom message mapping. 365 * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or 366 * {@link MessageType#BYTES}. 367 * <p>The default implementation throws an {@link IllegalArgumentException}. 368 * @param object the object to marshal 369 * @param session the JMS Session 370 * @param objectWriter the writer to use 371 * @param targetType the target message type (other than TEXT or BYTES) 372 * @return the resulting message 373 * @throws JMSException if thrown by JMS methods 374 * @throws IOException in case of I/O errors 375 */ 376 protected Message mapToMessage(Object object, Session session, ObjectWriter objectWriter, MessageType targetType) 377 throws JMSException, IOException { 378 379 throw new IllegalArgumentException("Unsupported message type [" + targetType + 380 "]. MappingJackson2MessageConverter by default only supports TextMessages and BytesMessages."); 381 } 382 383 /** 384 * Set a type id for the given payload object on the given JMS Message. 385 * <p>The default implementation consults the configured type id mapping and 386 * sets the resulting value (either a mapped id or the raw Java class name) 387 * into the configured type id message property. 388 * @param object the payload object to set a type id for 389 * @param message the JMS Message on which to set the type id property 390 * @throws JMSException if thrown by JMS methods 391 * @see #getJavaTypeForMessage(javax.jms.Message) 392 * @see #setTypeIdPropertyName(String) 393 * @see #setTypeIdMappings(java.util.Map) 394 */ 395 protected void setTypeIdOnMessage(Object object, Message message) throws JMSException { 396 if (this.typeIdPropertyName != null) { 397 String typeId = this.classIdMappings.get(object.getClass()); 398 if (typeId == null) { 399 typeId = object.getClass().getName(); 400 } 401 message.setStringProperty(this.typeIdPropertyName, typeId); 402 } 403 } 404 405 /** 406 * Convenience method to dispatch to converters for individual message types. 407 */ 408 private Object convertToObject(Message message, JavaType targetJavaType) throws JMSException, IOException { 409 if (message instanceof TextMessage) { 410 return convertFromTextMessage((TextMessage) message, targetJavaType); 411 } 412 else if (message instanceof BytesMessage) { 413 return convertFromBytesMessage((BytesMessage) message, targetJavaType); 414 } 415 else { 416 return convertFromMessage(message, targetJavaType); 417 } 418 } 419 420 /** 421 * Convert a TextMessage to a Java Object with the specified type. 422 * @param message the input message 423 * @param targetJavaType the target type 424 * @return the message converted to an object 425 * @throws JMSException if thrown by JMS 426 * @throws IOException in case of I/O errors 427 */ 428 protected Object convertFromTextMessage(TextMessage message, JavaType targetJavaType) 429 throws JMSException, IOException { 430 431 String body = message.getText(); 432 return this.objectMapper.readValue(body, targetJavaType); 433 } 434 435 /** 436 * Convert a BytesMessage to a Java Object with the specified type. 437 * @param message the input message 438 * @param targetJavaType the target type 439 * @return the message converted to an object 440 * @throws JMSException if thrown by JMS 441 * @throws IOException in case of I/O errors 442 */ 443 protected Object convertFromBytesMessage(BytesMessage message, JavaType targetJavaType) 444 throws JMSException, IOException { 445 446 String encoding = this.encoding; 447 if (this.encodingPropertyName != null && message.propertyExists(this.encodingPropertyName)) { 448 encoding = message.getStringProperty(this.encodingPropertyName); 449 } 450 byte[] bytes = new byte[(int) message.getBodyLength()]; 451 message.readBytes(bytes); 452 try { 453 String body = new String(bytes, encoding); 454 return this.objectMapper.readValue(body, targetJavaType); 455 } 456 catch (UnsupportedEncodingException ex) { 457 throw new MessageConversionException("Cannot convert bytes to String", ex); 458 } 459 } 460 461 /** 462 * Template method that allows for custom message mapping. 463 * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or 464 * {@link MessageType#BYTES}. 465 * <p>The default implementation throws an {@link IllegalArgumentException}. 466 * @param message the input message 467 * @param targetJavaType the target type 468 * @return the message converted to an object 469 * @throws JMSException if thrown by JMS 470 * @throws IOException in case of I/O errors 471 */ 472 protected Object convertFromMessage(Message message, JavaType targetJavaType) 473 throws JMSException, IOException { 474 475 throw new IllegalArgumentException("Unsupported message type [" + message.getClass() + 476 "]. MappingJacksonMessageConverter by default only supports TextMessages and BytesMessages."); 477 } 478 479 /** 480 * Determine a Jackson JavaType for the given JMS Message, 481 * typically parsing a type id message property. 482 * <p>The default implementation parses the configured type id property name 483 * and consults the configured type id mapping. This can be overridden with 484 * a different strategy, e.g. doing some heuristics based on message origin. 485 * @param message the JMS Message from which to get the type id property 486 * @throws JMSException if thrown by JMS methods 487 * @see #setTypeIdOnMessage(Object, javax.jms.Message) 488 * @see #setTypeIdPropertyName(String) 489 * @see #setTypeIdMappings(java.util.Map) 490 */ 491 protected JavaType getJavaTypeForMessage(Message message) throws JMSException { 492 String typeId = message.getStringProperty(this.typeIdPropertyName); 493 if (typeId == null) { 494 throw new MessageConversionException( 495 "Could not find type id property [" + this.typeIdPropertyName + "] on message [" + 496 message.getJMSMessageID() + "] from destination [" + message.getJMSDestination() + "]"); 497 } 498 Class<?> mappedClass = this.idClassMappings.get(typeId); 499 if (mappedClass != null) { 500 return this.objectMapper.getTypeFactory().constructType(mappedClass); 501 } 502 try { 503 Class<?> typeClass = ClassUtils.forName(typeId, this.beanClassLoader); 504 return this.objectMapper.getTypeFactory().constructType(typeClass); 505 } 506 catch (Throwable ex) { 507 throw new MessageConversionException("Failed to resolve type id [" + typeId + "]", ex); 508 } 509 } 510 511 /** 512 * Determine a Jackson serialization view based on the given conversion hint. 513 * @param conversionHint the conversion hint Object as passed into the 514 * converter for the current conversion attempt 515 * @return the serialization view class, or {@code null} if none 516 */ 517 protected Class<?> getSerializationView(Object conversionHint) { 518 if (conversionHint instanceof MethodParameter) { 519 MethodParameter methodParam = (MethodParameter) conversionHint; 520 JsonView annotation = methodParam.getParameterAnnotation(JsonView.class); 521 if (annotation == null) { 522 annotation = methodParam.getMethodAnnotation(JsonView.class); 523 if (annotation == null) { 524 return null; 525 } 526 } 527 return extractViewClass(annotation, conversionHint); 528 } 529 else if (conversionHint instanceof JsonView) { 530 return extractViewClass((JsonView) conversionHint, conversionHint); 531 } 532 else if (conversionHint instanceof Class) { 533 return (Class<?>) conversionHint; 534 } 535 else { 536 return null; 537 } 538 } 539 540 private Class<?> extractViewClass(JsonView annotation, Object conversionHint) { 541 Class<?>[] classes = annotation.value(); 542 if (classes.length != 1) { 543 throw new IllegalArgumentException( 544 "@JsonView only supported for handler methods with exactly 1 class argument: " + conversionHint); 545 } 546 return classes[0]; 547 } 548 549}