Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JsonType of request overrides response type when using amqp inbound gateway in combination with json converters (jackson2) [INT-3285] #7262

Closed
spring-operator opened this issue Feb 5, 2014 · 17 comments

Comments

@spring-operator
Copy link
Contributor

Jan Philipp opened INT-3285 and commented

Okay, I'm still note sure if I get this right.

The scenario: If have two components communication via Spring Integration, via RabbitMQ (AMQP), via AMAP Inbound and Outbound Gateways, via a Jackson2 Message Converter.

(No conversion service is involved, it's a standard setup without spring wev)

Component 1: Sends a message via an AMQP Outbound Gateway with a configured Jackson2 message converter. Requesting type is A, returning type is B. Works fine, internally toJSON() like we would expected.

Component 2: Receives messages via an AMQP Inbound Gateway with a configured Jackson2 message converter. Requesting type is A and returning type BB. Internal fromJson() works fine, and the actual processing works without problems.

Well.. however, until the reply message is processed. The reply message will be created containing the (correct JSON) of the payload (an object of type B) but with a type hint header (TypeId) of the requesting type A.

So far I can tell you: Actually, the reply message was correctly transformed via toJSON by the configured Jackson2 converter (I could verify the jackson2 converter was a) used and b) processed the payload correctly including adding a message header). However, in AmqpInboundGateway.java:100 headerMapper.fromHeadersToReply() will be invoked. Called with a correct value of headers.__TypeId__ (!) representing the type B, it will be in this call be overridden in AbstractHeaderMapper.java:141-149 with the corresponding value of the request type AA (made by the message converter I think).


Coming from the old approach with two more or less simple directives from-json/to-json, this is now very confusing. :( Is this a bug or a miss configuration of myself?


Affects: 3.0.1

Reference URL: http://stackoverflow.com/questions/21595553/spring-integration-amqp-wrong-json-type-on-response

1 votes, 5 watchers

@spring-operator
Copy link
Contributor Author

Jan Philipp commented

Component 2 has the following configuration

<int-amqp:inbound-gateway connection-factory="connectionFactory" request-channel="requestChannel"
                          reply-channel="replyChannel" error-channel="errorChannel"
                          queue-names="queue" message-converter="jackson2JsonMessageConverter"/>

And, to make it complete:

	@Bean
	public Jackson2JsonMessageConverter jackson2JsonMessageConverter() {
		final ObjectMapper mapper = new ObjectMapper();
		mapper.registerModule(new JodaModule());

		final Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
		converter.setClassMapper(classMapper());
		converter.setJsonObjectMapper(mapper);

		return converter;
	}

	@Bean
	public ClassMapper classMapper() {
		final Map<String, Class<?>> map = new HashMap<>();

		for (Class type : new Class[] { A.class, B.class }) {
			map.put(type.getName(), type);
		}

		final DefaultClassMapper mapper = new DefaultClassMapper();
		mapper.setIdClassMapping(map);
		return mapper;
	}

@spring-operator
Copy link
Contributor Author

Jan Philipp commented

@ServiceActivator
public B receive(@Payload final A a, @Headers final Map<String, Object> headers) {
// ...
}

@spring-operator
Copy link
Contributor Author

Artem Bilan commented

Actually AMQP MessageConverter isn't applied for reply Message on the Inbound side.
As far as Spring Integration populates standard headers from AMQP MessageProperties and, if you don't change them in the Integration message flow, they will return to the AmqpInboundGateway together with reply Message.
And here it is a point where your request _TypeId_ header is populated to the AMQP reply headers.

If you don't use <int:object-to-json-transformer> you have to take care of them yourself.
For this purpose there is out of the box JsonHeaders class. It's properties are treated as standard AMQP headers.

Do I understand you correctly?

@spring-operator
Copy link
Contributor Author

Jan Philipp commented

The statics of JsonHeaders are the names of the headers which are being overridden with them of the request message.

Okay. How do I "intercept" the reply message converting? Must I exclude the header from the reply (there is that mapped-reply-headers in the inbound-gateway directive)?

If you don't use int:object-to-json-transformer you have to take care of them yourself.

Perhaps I did not get it right, but they are not compatible with Jackson2?

@spring-operator
Copy link
Contributor Author

Artem Bilan commented

How do I "intercept" the reply message converting?

<header-filter> should help

mapped-reply-headers

It depends on if you need to provide the __TypeId__ header in the reply.

but they are not compatible with Jackson2?

Not sure what you mean. By default <int:object-to-json-transformer> since Spring Integration 3.0 uses Jackson2 by default, if it is present in the classpath.

Actually exactly JsonHeaders are used to/from AMQP by Spring Integration components (DefaultAmqpHeaderMapper, ObjectToJsonTransformer, JsonToObjectTransformer)

This can help you to do it yourself: Jackson2JsonObjectMapper

@spring-operator
Copy link
Contributor Author

Jan Philipp commented

Not sure what you mean.

Hm, I'm actually not even sure myself at the moment. :) I was migration a 2.2 setup to 3.0 and was issued with transformation issues (Joda DateTime) and Jackson1 mappings. Perhaps the last one was my own failure because both Jacksons were accidentally available in the classpath and INT found v1 before v2.

Nevertheless, then I found the new message converters which looks exactly what I need. Also, I have to define my own message converter because of Joda Module (out of the box it does not work). However, a (custom) message converter is not used automatically.

But, I still do not get it. What I'm doing wrong here? I not happy to intercept stuff if there would be a better, "framework recommended" way.

It depends on if you need to provide the TypeId header in the reply.

Actually, I only want to provide the method API "public Response process(Request request)" via AMQP and using Jackson2 (incl. Joda bindings) as transport/serialization.

By the way, thank you for the quick responses so far! :)

@spring-operator
Copy link
Contributor Author

Artem Bilan commented

OK. Got it.
So, my recommendation: use <int:object-to-json-transformer> with your custom Jackson 2 mapper as a constructor argument for Jackson2JsonObjectMapper.
And everything will be OK.

One more time: AmqpInboundGateway doesn't apply MessageConverter for the reply message.

Let us know and we close the issue as 'Works as Designed'

@spring-operator
Copy link
Contributor Author

Jan Philipp commented

Ah, okay. Yes, thank you! Work as designed, actually. Close it.
Thank you, that helps me a lot.


Just for the records (I'm also don't like unresolved questions :p)

1: Provide Joda (or even other custom) mappings for Jackson2: just and only this configuration snippet

@Bean
public JsonObjectMapper<?> jsonObjectMapper() {
     final ObjectMapper mapper = new ObjectMapper();
     mapper.registerModule(new JodaModule());
     return new Jackson2JsonObjectMapper(mapper);
}

2: Provide the "constructor" argument (well, I'd actually read the xml parser to understand what do you mean with that)

For each usage of directives json-to-object-transformer or object-to-json-transformer, add a explicit object-mapper (otherwise the transformers invokes JacksonJsonObjectMapperProvider.newInstance() which will be always the standard mapper).

<!-- either that...-->
<int:json-to-object-transformer object-mapper="jsonObjectMapper" type="your.package.Request"/>
<!-- or this-->
<int:object-to-transformer object-mapper="jsonObjectMapper"/>

Perhaps the last one could be resolved automatically (at least if custom defined) in the future?

@spring-operator
Copy link
Contributor Author

Artem Bilan commented

Do you mean try to resolve the bean with name jsonObjectMapper, if there is no object-mapper attribute provided?
Interest.
But you should understand that your application may work with different domains, where there is need to have different ObjectMapper configurations.
Finally you end up with object-mapper attribute definitions and that tiny 'explicit' convenience fizzles.

Anyway, thank you for pointing out the Spring Integration + Spring AMQP symmetry one more time.

@spring-operator
Copy link
Contributor Author

Jan Philipp commented

Okay, that is right. Perhaps something like a default for a custom factory, i.e. for all json converters? Currently it is resolved statically. Well, that's.. static :)

One of the missing points on my side was that I'd forgot totally json-to-object-transformer is only a shortcut for a real bean creation.

@spring-operator
Copy link
Contributor Author

Artem Bilan commented

Reopened as there is really a case (this the SO link) where MessageConverter provides correct headers for reply, but HeaderMapper overrides them from Spring Integration MessageHeaders.

Gary Russell, how do you think? Will it be OK, if we ignore MessageHeaders for AMQP reply message, if the MessageConverter has populated them before?
In current case Jackson2JsonMessageConverter correctly builds the reply message and MessageProperties, but after that the MessagePostProcessor invokes AmqpHeaderMapper, which populates standard headers and looks like in request-reply scenario they are request headers.

Or at least provide the option override-message-properties=true/false to allow for end-user to control the behaviour.

Jan Philipp, my apologize: I wasn't right yesterday and AmqpInboundGateway really populate the messageConverter to the amqpTemplate to send a reply message

@spring-operator
Copy link
Contributor Author

Artem Bilan commented

Actually the MessageConverter is the main actor to build AMQP Message, so I think its Headers should have a precedence over Integration headers. And have to populate the last one, if they absent in MessageProperties.
WDYT?

@spring-operator
Copy link
Contributor Author

Gary Russell commented

I am not sure I have completely followed this entire thread of comments but it sounds like this is a similar problem to one we had with HTTP some time ago whereby the inbound Content-Length was incorrectly copied to the outbound reply.

We solved that by adding setExcludedInboundStandardResponseHeaderNames() to the mapper, with the default being

private static String[] HTTP_RESPONSE_HEADER_NAMES_INBOUND_EXCLUSIONS = new String[] {
     CONTENT_LENGTH
};

This is a little different because the headers are generated by the MessageConverter, but it sounds similar.

@spring-operator
Copy link
Contributor Author

Marty Pitt commented

Hi

I believe this may be a regression, as the configuration I'm using in my most recent project (as shown on the SO post) is taken directly from another SI project.

The previous project was running under SI 2.2, and the current one on SI 3.0.0-RELEASE.

@spring-operator
Copy link
Contributor Author

Marty Pitt commented

Confirmed -- I just rolled back to SI 2.2.6.RELEASE, and the issue went away.

@spring-operator
Copy link
Contributor Author

Artem Bilan commented

PR: #1050

@spring-operator
Copy link
Contributor Author

Gary Russell commented

Merged into master and 3.0.x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants