Reputation: 3991
I have IntegrationFlow
where I call HTTP endpoint:
@Bean
public IntegrationFlow getInformationFlow(RestTemplate restTemplate) {
return IntegrationFlows.from(GET_RESPONSE_ENTITY)
.handle(Http
.outboundGateway(url + "/application/{code}", restTemplate)
.httpMethod(GET)
.uriVariable("code", "payload")
.expectedResponseType(new ParameterizedTypeReference<ResponseEntity<Information>>() {
})
).get();
}
This flow is executed when getInformation
is called thanks to Gateway
@MessagingGateway
public interface MyGateway {
@Gateway(requestChannel = GET_RESPONSE_ENTITY)
ResponseEntity<Information> getInformation(String code);
}
Code above throws following exception:
Caused by: org.springframework.http.converter.HttpMessageConversionException:
Type definition error: [simple type, class org.springframework.http.ResponseEntity];
nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `org.springframework.http.ResponseEntity`
(no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
Jackson tries to deserialize into ResponseEntity
instead of Information
class
My goal is to check status code first and optionally check Information
fields
Is it possible to return ResponseEntity from method annotated by @Gateway
?
Upvotes: 1
Views: 1104
Reputation: 2354
TL;DR: This is a new feature (thanks to OP) available in Spring Integration >=5.5.0. You need to:
MessagingGateway
so it returns a ResponseEntity
ResponseEntity
HttpMessageConverter
that supports ResponseEntity
The GitHub issue/feature request that OP created, resulted in an actual feature being implemented in Spring Integration. It is now possible to receive ResponseEntity
objects. What Artem Bilan explained was correct prior to the implementation of the feature and still is correct if the flag extractResponseBody
is set to true
(default). If it is set to false
, then it is pretty easy to get a ResponseEntity
(now).
First get a new version (>=5.5.0) of Spring Integration in your POM.xml
or build.gradle
dependencies {
...
implementation "org.springframework.integration:spring-integration-http:5.5.2"
...
}
Now in my case I previously had a REST call that resulted in a String
. Now I want a ResponseEntity<String>
. So the MessagingGateway
I previously defined...
old
@MessagingGateway
public interface Client {
/**
* Makes call to (another) Server
*/
@Gateway(requestChannel = "requestChannel", replyChannel = "replyChannel")
String makeSomeCall(@Header("url") String url, @Payload SomePayload sp);
}
... becomes (only return value changed):
new
@MessagingGateway
public interface Client {
/**
* Makes call to (another) server
*/
@Gateway(requestChannel = "requestChannel", replyChannel = "replyChannel")
ResponseEntity<String> makeSomeCall(@Header("url") String url, @Payload SomePayload sp);
}
Now if we leave it this way, then it still won't work. We need to tell Spring, that the ResponseBody should not be extracted. We do this in the integration flow:
@Bean
MessageChannel replyChannel() {
return MessageChannels.direct("replyChannel").get();
}
@Bean
MessageChannel clientChannel() {
return MessageChannels.direct().get();
}
IntegrationFlow clientFlow() {
final SpelExpressionParser parser = new SpelExpressionParser();
return IntegrationFlows.from(clientChannel())
.handle(Http.outboundGateway(parser.parseExpression("headers.url"), restTemplate)
.charset("UTF-8")
.extractResponseBody(false) // <-- this is new w/ default being true!
.expectedResponseType(ResponseEntity.class)) // <-- this was String.class
.channel(clientReplyChannel())
.get();
}
If we leave it here, it will still not work. The program will not be able to find an appropriate HttpMessageConverter
. You need to write one on your own. It all depends on what the content of your ResponseEntity
is. Thankfully, you most likely don't need to write a whole HttpMessageConverter
on your own. You can simply extend the "approriate" one and tell it to accept ResponseEntity
. My new MessageConverter looks like this.
@Component
public class ResponseEntityHttpMessageConverter extends StringHttpMessageConverter {
public ResponseEntityHttpMessageConverter() {
}
@Override
public boolean supports(final Class<?> clazz) {
return ResponseEntity.class == clazz;
}
}
Spring Boot should be able to pick it up on its own. I believe there is plenty of material about this online in case you get stuck.
Hint: Just don't make the mistake to implement HttpMessageConverter<ResponseEntity>
. Spring will unwrap the ResponseEntity for the MessageConverter
itself. So the converter should only be for the content.
Upvotes: 0
Reputation: 121272
Short answer to your question: it depends on that MyGateway
contract implementation. It is really not a gateway (or any interface API) responsibility to deal with returns. They define only contracts. It is already your goal to implement such a contract properly.
What I mean by this that Spring Integration with its EIP components does not go further than regular Java program design and architecture. It is just a particular case that this contract is an IntegrationFlow
as an implementation. Therefore the problem is not with a contract, but rather implementation details, which is an HTTP call in your case.
So, better to ask the question like:
How to return a
ResponseEntity<Information>
from theHttp.outboundGateway()
with a Jackson message converter?
That's why I asked you on Gitter for this SO thread to better understand what is going on. Your original question is misleading and really has nothing to do with a @MessagingGateway
. I'm even sure that is some clue in the stack trace that the problem happens on the RestTemplate
call, not in the @MessagingGateway
.
Now trying to help you to fix your explicit problem.
The AbstractHttpRequestExecutingMessageHandler
does not return a ResponseEntity
when there is a body
:
protected Object getReply(ResponseEntity<?> httpResponse) {
HttpHeaders httpHeaders = httpResponse.getHeaders();
Map<String, Object> headers = this.headerMapper.toHeaders(httpHeaders);
if (this.transferCookies) {
doConvertSetCookie(headers);
}
AbstractIntegrationMessageBuilder<?> replyBuilder;
MessageBuilderFactory messageBuilderFactory = getMessageBuilderFactory();
if (httpResponse.hasBody()) {
Object responseBody = httpResponse.getBody();
replyBuilder = (responseBody instanceof Message<?>)
? messageBuilderFactory.fromMessage((Message<?>) responseBody)
: messageBuilderFactory.withPayload(responseBody); // NOSONAR - hasBody()
}
else {
replyBuilder = messageBuilderFactory.withPayload(httpResponse);
}
replyBuilder.setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE,
httpResponse.getStatusCode());
return replyBuilder.copyHeaders(headers);
}
Only if there is no body. In that case it means there is nothing to map into a payload
for the reply message, therefore we use the whole ResponseEntity
.
As you see in this code snippet, the StatusCode
is mapped to the org.springframework.integration.http.HttpHeaders.STATUS_CODE
reply message header.
There is also some explanation in docs on the matter: https://docs.spring.io/spring-integration/docs/current/reference/html/http.html#using-cookies.
From here it means that your expectedResponseType
can be only as an Information
type. The RestTemplate
with its HttpMessageConverts
indeed doesn't know what to do with a ResponseEntity
type to map.
Since it may return just an Information
in the payload if it comes in the response, or may return the whole ResponseEntity
with an empty body, it looks like you have to add some routing and transformation logic before returning a ResponseEntity<Information>
as a reply to your @MessagingGateway
call. Or you can revise that gateway contract and really implement a status code check in the integration flow via router or filter - and your @MessagingGateway
consumer would be free from the HTTP stuff like status code checking and headers conversion.
Although it might not hurt to have some option on the AbstractHttpRequestExecutingMessageHandler
to always return the whole ResponseEntity
as a payload. Feel free to raise a GH issue a we will consider to implement it sooner than later!
Upvotes: 2