Populate a JsonHeaders.RESOLVABLE_TYPE on reply (#3163)

* Populate a JsonHeaders.RESOLVABLE_TYPE on reply

Fixes https://github.com/spring-projects/spring-integration-samples/issues/277

In the `AbstractAmqpOutboundEndpoint` the `JsonHeaders.RESOLVABLE_TYPE`
from request message is copied to reply message making inconsistency downstream.
The `JsonToObjectTransformer` consults first a `JsonHeaders.RESOLVABLE_TYPE`
and deserialize payload to wrong type

* Fix `DefaultAmqpHeaderMapper` to populate a `JsonHeaders.RESOLVABLE_TYPE`
alongside with other `JsonHeaders` populated from the reply AMQP message.
This way a `JsonHeaders.RESOLVABLE_TYPE` from request message won't have effect
* To get access to classes, supply `AbstractHeaderMapper` with a bean factory `ClassLoader`
* Introduce a couple utility methods into `JsonHeaders` for building a `ResolvableType`

**Cherry-pick to 5.2.x**

* * Fix code formatting for arguments wrapping
This commit is contained in:
Artem Bilan
2020-01-31 14:17:19 -05:00
committed by GitHub
parent 94c8cf7e21
commit 2f4394d8b5
7 changed files with 225 additions and 138 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -29,15 +29,13 @@ import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Transformer implementation that converts a JSON string payload into an instance of the
* provided target Class. By default this transformer uses
* {@linkplain org.springframework.integration.support.json.JsonObjectMapperProvider}
* factory to get an instance of Jackson 1 or Jackson 2 JSON-processor
* {@linkplain JsonObjectMapper} implementation depending on the jackson-databind or
* jackson-mapper-asl libs on the classpath. Any other {@linkplain JsonObjectMapper}
* factory to get an instance of Jackson JSON-processor
* if jackson-databind lib is present on the classpath. Any other {@linkplain JsonObjectMapper}
* implementation can be provided.
* <p>Since version 3.0, you can omit the target class and the target type can be
* determined by the {@link JsonHeaders} type entries - including the contents of a
@@ -47,10 +45,11 @@ import org.springframework.util.ClassUtils;
* @author Mark Fisher
* @author Artem Bilan
*
* @since 2.0
*
* @see JsonObjectMapper
* @see org.springframework.integration.support.json.JsonObjectMapperProvider
*
* @since 2.0
*/
public class JsonToObjectTransformer extends AbstractTransformer implements BeanClassLoaderAware {
@@ -149,37 +148,13 @@ public class JsonToObjectTransformer extends AbstractTransformer implements Bean
Object valueType = headers.get(JsonHeaders.RESOLVABLE_TYPE);
Object typeIdHeader = headers.get(JsonHeaders.TYPE_ID);
if (!(valueType instanceof ResolvableType) && typeIdHeader != null) {
Class<?> targetClass = getClassForValue(typeIdHeader);
Class<?> contentClass = null;
Class<?> keyClass = null;
Object contentTypeHeader = headers.get(JsonHeaders.CONTENT_TYPE_ID);
if (contentTypeHeader != null) {
contentClass = getClassForValue(contentTypeHeader);
}
Object keyTypeHeader = headers.get(JsonHeaders.KEY_TYPE_ID);
if (keyTypeHeader != null) {
keyClass = getClassForValue(keyTypeHeader);
}
valueType = JsonObjectMapper.buildResolvableType(targetClass, contentClass, keyClass);
valueType =
JsonHeaders.buildResolvableType(this.classLoader, typeIdHeader,
headers.get(JsonHeaders.CONTENT_TYPE_ID), headers.get(JsonHeaders.KEY_TYPE_ID));
}
return valueType instanceof ResolvableType
? (ResolvableType) valueType
: null;
}
private Class<?> getClassForValue(Object classValue) {
if (classValue instanceof Class<?>) {
return (Class<?>) classValue;
}
else {
try {
return ClassUtils.forName(classValue.toString(), this.classLoader);
}
catch (ClassNotFoundException | LinkageError e) {
throw new IllegalStateException(e);
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -28,10 +28,12 @@ import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.lang.Nullable;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringUtils;
@@ -43,9 +45,11 @@ import org.springframework.util.StringUtils;
* @author Oleg Zhurakousky
* @author Stephane Nicoll
* @author Gary Russell
* @author Artem Bilan
*
* @since 2.1
*/
public abstract class AbstractHeaderMapper<T> implements RequestReplyHeaderMapper<T> {
public abstract class AbstractHeaderMapper<T> implements RequestReplyHeaderMapper<T>, BeanClassLoaderAware {
/**
* A special pattern that only matches standard request headers.
@@ -74,9 +78,11 @@ public abstract class AbstractHeaderMapper<T> implements RequestReplyHeaderMappe
private final Collection<String> replyHeaderNames;
private volatile HeaderMatcher requestHeaderMatcher;
private HeaderMatcher requestHeaderMatcher;
private volatile HeaderMatcher replyHeaderMatcher;
private HeaderMatcher replyHeaderMatcher;
private ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
/**
* Create a new instance.
@@ -96,6 +102,15 @@ public abstract class AbstractHeaderMapper<T> implements RequestReplyHeaderMappe
this.replyHeaderMatcher = createDefaultHeaderMatcher(this.standardHeaderPrefix, this.replyHeaderNames);
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
protected ClassLoader getClassLoader() {
return this.classLoader;
}
/**
* Provide the header names that should be mapped from a request
* to a {@link MessageHeaders}.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* Copyright 2013-2020 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.
@@ -20,6 +20,11 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* Pre-defined names and prefixes to be used for setting and/or retrieving JSON
* entries from/to Message Headers and other adapter, e.g. AMQP.
@@ -43,7 +48,7 @@ public final class JsonHeaders {
public static final String KEY_TYPE_ID = PREFIX + "__KeyTypeId__";
/**
* The header to represent a {@link org.springframework.core.ResolvableType}
* The header to represent a {@link ResolvableType}
* for the target deserialized object.
* @since 5.2
*/
@@ -52,4 +57,70 @@ public final class JsonHeaders {
public static final Collection<String> HEADERS =
Collections.unmodifiableList(Arrays.asList(TYPE_ID, CONTENT_TYPE_ID, KEY_TYPE_ID, RESOLVABLE_TYPE));
/**
* Build a {@link ResolvableType} for provided class components.
* @param classLoader a {@link ClassLoader} t load classes for components if needed.
* @param targetClassValue the class representation object.
* @param contentClassValue the collection element (or map value) class representation object.
* @param keyClassValue the map key class representation object.
* @return the {@link ResolvableType} based on provided class components
* @since 5.2.4
*/
public static ResolvableType buildResolvableType(ClassLoader classLoader, Object targetClassValue,
@Nullable Object contentClassValue, @Nullable Object keyClassValue) {
Class<?> targetClass = getClassForValue(classLoader, targetClassValue);
Class<?> keyClass = getClassForValue(classLoader, keyClassValue);
Class<?> contentClass = getClassForValue(classLoader, contentClassValue);
return buildResolvableType(targetClass, contentClass, keyClass);
}
@Nullable
private static Class<?> getClassForValue(ClassLoader classLoader, @Nullable Object classValue) {
if (classValue instanceof Class<?>) {
return (Class<?>) classValue;
}
else if (classValue != null) {
try {
return ClassUtils.forName(classValue.toString(), classLoader);
}
catch (ClassNotFoundException | LinkageError e) {
throw new IllegalStateException(e);
}
}
else {
return null;
}
}
/**
* Build a {@link ResolvableType} for provided class components.
* @param targetClass the class to use.
* @param contentClass the collection element (or map value) class.
* @param keyClass the map key class.
* @return the {@link ResolvableType} based on provided class components
* @since 5.2.4
*/
public static ResolvableType buildResolvableType(Class<?> targetClass, @Nullable Class<?> contentClass,
@Nullable Class<?> keyClass) {
if (keyClass != null) {
return TypeDescriptor
.map(targetClass,
TypeDescriptor.valueOf(keyClass),
TypeDescriptor.valueOf(contentClass))
.getResolvableType();
}
else if (contentClass != null) {
return TypeDescriptor
.collection(targetClass,
TypeDescriptor.valueOf(contentClass))
.getResolvableType();
}
else {
return ResolvableType.forClass(targetClass);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 the original author or authors.
* Copyright 2013-2020 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.
@@ -23,9 +23,7 @@ import java.util.Collection;
import java.util.Map;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.integration.mapping.support.JsonHeaders;
import org.springframework.lang.Nullable;
/**
* Strategy interface to convert an Object to/from the JSON representation.
@@ -102,29 +100,7 @@ public interface JsonObjectMapper<N, P> {
}
}
map.put(JsonHeaders.RESOLVABLE_TYPE, buildResolvableType(targetClass, contentClass, keyClass));
}
static ResolvableType buildResolvableType(Class<?> targetClass, @Nullable Class<?> contentClass,
@Nullable Class<?> keyClass) {
if (keyClass != null) {
return TypeDescriptor
.map(targetClass,
TypeDescriptor.valueOf(keyClass),
TypeDescriptor.valueOf(contentClass))
.getResolvableType();
}
else if (contentClass != null) {
return TypeDescriptor
.collection(targetClass,
TypeDescriptor.valueOf(contentClass))
.getResolvableType();
}
else {
return ResolvableType.forClass(targetClass);
}
map.put(JsonHeaders.RESOLVABLE_TYPE, JsonHeaders.buildResolvableType(targetClass, contentClass, keyClass));
}
}