Search code examples
spring-integrationspring-integration-dslspring-integration-http

Unable to set Reply Channel to Message Header in Spring Integration


There is definitely something I must be missing so seeking help here.

I am building a simple REST application using Spring Integration with the following HTTP inbound gateway:

<!-- Gateway -->
    <int-http:inbound-gateway id="fruitQuotePOSTGateway"
                              request-channel="fruitQuotePOSTRequests"
                              supported-methods="POST"
                              path="/api/v1/fruit-quote"
                              request-payload-type="java.lang.String"
                              reply-timeout="10000"
                              reply-channel="fruitQuotePOSTResponses"
                              error-channel="applicationErrors">
        <int-http:request-mapping consumes="application/xml" produces="application/xml"/>
    </int-http:inbound-gateway>

Once a XML enters this gateway, it undergoes the following simple steps:

  • Transformation to generate the JAXB object corresponding to the incoming request
  • Message header enrichment that reads a "uuid" from the JAXB Object and sets it to the header of the SI (Spring Integration) message
  • Transformation to generate a XML response to the calling client.

To start with, here is the XML configuration of the entire application (omitted the HTTP namespaces, for brevity):

<!-- Gateway -->
    <int-http:inbound-gateway id="fruitQuotePOSTGateway"
                              request-channel="fruitQuotePOSTRequests"
                              supported-methods="POST"
                              path="/api/v1/fruit-quote"
                              request-payload-type="java.lang.String"
                              reply-timeout="10000"
                              reply-channel="fruitQuotePOSTResponses"
                              error-channel="applicationErrors">
        <int-http:request-mapping consumes="application/xml" produces="application/xml"/>
    </int-http:inbound-gateway>

    <!--
    - Generate fruit quote request JAXB from the incoming request
    - Create a header "requestUUID" by reading it from fruit quote request JAXB
    - Generate fruit quote acknowledgement response for the calling client
    -->

    <int:transformer input-channel="fruitQuotePOSTRequests"
               ref="fruitQuoteTransformation"
               method="generateFruitQuoteRequestJAXB"/>

    <int:header-enricher input-channel="requestUUIDEnrichment" output-channel="orderIDGeneration">
        <int:header name="requestUUID" expression="payload.getFruitQuoteRequestJAXB().getFRUITQUOTEREQUESTDATA().getUuid()"/>
    </int:header-enricher>

    <int:transformer input-channel="fruitQuoteAcknowledgementGeneration"
                     ref="fruitQuoteTransformation"
                     method="generateFruitQuoteAcknowledgement"
                     output-channel="fruitQuotePOSTResponses"/>

    <!-- Error handling -->
    <int:transformer input-channel="applicationErrors"
                     ref="fruitQuoteTransformation"
                     method="generateFruitQuoteAcknowledgementWithError"
                     output-channel="fruitQuotePOSTResponses"/>

    <!-- Channels -->
    <int:channel id="fruitQuotePOSTRequests"/>
    <int:channel id="requestUUIDEnrichment"/>
    <int:channel id="fruitQuotePOSTResponses"/>
    <int:channel id="fruitQuoteAcknowledgementGeneration"/>
    <int:channel id="applicationErrors"/>

The payload flowing from one step to another in the application is a custom Builder Object as follows (omitted the package name):

import static java.util.Objects.nonNull;

public class FruiteQuoteComposite {
    private final FRUITQUOTEREQUEST fruitQuoteRequestJAXB;
    private final FruitQuoteApplicationException fruitQuoteApplicationException;
    private final Integer orderID;
    private final ErrorInformation errorInformation;

    private FruiteQuoteComposite(FruiteQuoteCompositeBuilder fruiteQuoteCompositeBuilder) {
        this.fruitQuoteRequestJAXB = fruiteQuoteCompositeBuilder.fruitQuoteRequestJAXB;
        this.fruitQuoteApplicationException = fruiteQuoteCompositeBuilder.fruitQuoteApplicationException;
        this.orderID = fruiteQuoteCompositeBuilder.orderID;
        this.errorInformation = fruiteQuoteCompositeBuilder.errorInformation;
    }

    public FruitQuoteApplicationException getFruitQuoteApplicationException() {
        return fruitQuoteApplicationException;
    }

    public FRUITQUOTEREQUEST getFruitQuoteRequestJAXB() {
        return fruitQuoteRequestJAXB;
    }

    public Integer getOrderID() {
        return orderID;
    }

    public ErrorInformation getErrorInformation() {
        return errorInformation;
    }

    public static class FruiteQuoteCompositeBuilder {
        private FRUITQUOTEREQUEST fruitQuoteRequestJAXB;
        private FruitQuoteApplicationException fruitQuoteApplicationException;
        private Integer orderID;
        private ErrorInformation errorInformation;

        public FruiteQuoteCompositeBuilder() {
        }

        public FruiteQuoteCompositeBuilder setFruitQuoteRequestJAXB(FRUITQUOTEREQUEST fruitQuoteRequestJAXB) {
            if (nonNull(fruitQuoteRequestJAXB)) {
                this.fruitQuoteRequestJAXB = fruitQuoteRequestJAXB;
            }

            return this;
        }

        public FruiteQuoteCompositeBuilder setFruitQuoteApplicationException(FruitQuoteApplicationException fruitQuoteApplicationException) {
            if (nonNull(fruitQuoteApplicationException)) {
                this.fruitQuoteApplicationException = fruitQuoteApplicationException;
            }

            return this;
        }

        public FruiteQuoteCompositeBuilder setOrderID(Integer orderID) {
            if(nonNull(orderID)) {
                this.orderID = orderID;
            }

            return this;
        }

        public FruiteQuoteCompositeBuilder setErrorInformation(ErrorInformation errorInformation) {
            if (nonNull( errorInformation )) {
                this.errorInformation = errorInformation;
            }
            return this;
        }

        public FruiteQuoteComposite build() {
            return new FruiteQuoteComposite(this);
        }
    }
}

The reason why I didn't use "output-channel" on the transformers is because I wanted to explicitly choose the replyChannel/outgoing route inside the java logic running the transformation.

For instance, inside the FruitQuoteTransformation.generateFruitQuoteRequestJAXB method, I set one route for success and another route for exceptions/errors as follows:

public Message<FruiteQuoteComposite> generateFruitQuoteRequestJAXB(Message<String> fruitQuoteRequestMessage) {
        String fruitQuoteRequest = fruitQuoteRequestMessage.getPayload();
        Unmarshaller unmarshaller;
        FRUITQUOTEREQUEST fruitQuoteRequestJAXB;

        try {
            unmarshaller = requireNonNull(fruitQuoteRequestJaxbContext).createUnmarshaller();
            fruitQuoteRequestJAXB = (FRUITQUOTEREQUEST) requireNonNull(unmarshaller)
                    .unmarshal(new StringReader(fruitQuoteRequest));
        } catch (JAXBException jaxbException) {
            logger.error("JAXB Unmarshalling exception occurred with error code :: " + ERR_FRUIT_QUOTE_REQUEST_JAXB_TRANSFORMATION, jaxbException);
            FruitQuoteApplicationException fruitQuoteApplicationException = generateFruitQuoteApplicationException(ERR_FRUIT_QUOTE_REQUEST_JAXB_TRANSFORMATION, MESSAGE_FRUIT_QUOTE_INTERNAL_SYSTEM_ERROR);

            FruiteQuoteComposite outboundFruitQuoteComposite = new FruiteQuoteComposite.FruiteQuoteCompositeBuilder()
                    .setFruitQuoteApplicationException(fruitQuoteApplicationException)
                    .build();

            return withPayload(requireNonNull(outboundFruitQuoteComposite))
                    .setHeader(MessageHeaders.REPLY_CHANNEL, "applicationErrors")
                    .build();
        }

        FruiteQuoteComposite outboundFruitQuoteComposite = new FruiteQuoteComposite.FruiteQuoteCompositeBuilder()
                .setFruitQuoteRequestJAXB(fruitQuoteRequestJAXB)
                .build();

        return withPayload(requireNonNull(outboundFruitQuoteComposite))
                .setHeader(MessageHeaders.REPLY_CHANNEL, "requestUUIDEnrichment")
                .build();
    }
  • My 1st question For some reason, the .setHeader invocation isn't working as expected and the message isn't going to the next channel. Is there something I am missing? The result is the same even when I use .setReplyChannelName.
  • My 2nd question In case there is a solution to question 1), keeping the overall SI configuration as XML-based, is there an alternate approach to setting customized reply channels? The only option that came to my mind were using a router after every single transformer but this seemed too verbose.

Can you please help?


Solution

  • You must never mess with the framework's replyChannel header; it is not intended for routing purposes.

    The replyChannel is an internal channel, unique to each message used to correlate a reply to a request. You generally don't need an explicit reply-channel on the inbound gateway; if you do, it is simply bridged at runtime to the message's replyChannel header.

    Error conditions should be handled on the gateway's error-channel instead by throwing an exception. Different errors can be signaled by different exception types.