spring-integration MockIntegrationContext ReactiveStreamConsumer: IllegalArgumentException: 'subscriber' must not be null in the "...endpoint" - spring-boot

My test is failing because of the missing 'subscriber' field IllegalArgumentException: 'subscriber' must not be null in the "...endpoint"
#Test
public void test() throws InterruptedException {
ArgumentCaptor<Message<?>> captor = messageArgumentCaptor();
CountDownLatch receiveLatch = new CountDownLatch(1);
MessageHandler mockMessageHandler = mockMessageHandler(captor).handleNext(m -> receiveLatch.countDown());
this.mockIntegrationContext
.substituteMessageHandlerFor(
"test2.org.springframework.integration.config.ConsumerEndpointFactoryBean#1",
mockMessageHandler);
this.integrationFlowWithReactiveConsumerHandler.getInputChannel().send(new GenericMessage<>("test2"));
assertThat(receiveLatch.await(10, TimeUnit.SECONDS)).isTrue();
verify(mockMessageHandler).handleMessage(any());
assertThat(captor.getValue().getPayload())
.isEqualTo("reactive-message-text");
}
It's failing here in MockIntegrationContext.java, calling substituteMessageHandlerFor method, when endpoint is ReactiveStreamsConsumer
public void substituteMessageHandlerFor(String consumerEndpointId, // NOSONAR - complexity
MessageHandler mockMessageHandler, boolean autoStartup) {
Object endpoint = this.beanFactory.getBean(consumerEndpointId, IntegrationConsumer.class);
if (autoStartup && endpoint instanceof Lifecycle) {
((Lifecycle) endpoint).stop();
}
DirectFieldAccessor directFieldAccessor = new DirectFieldAccessor(endpoint);
Object targetMessageHandler = directFieldAccessor.getPropertyValue(HANDLER);
Assert.notNull(targetMessageHandler, () -> "'handler' must not be null in the: " + endpoint);
if (endpoint instanceof ReactiveStreamsConsumer) {
Object targetSubscriber = directFieldAccessor.getPropertyValue("subscriber");
Assert.notNull(targetSubscriber, () -> "'subscriber' must not be null in the: " + endpoint);
The handler endpoint bean is ...:reactive-outbound-channel-adapter which is being instantiated using ReactiveStreamConsumer constructor, where the 'subsciber' field is null by default.
/**
* Instantiate an endpoint based on the provided {#link MessageChannel} and {#link ReactiveMessageHandler}.
* #param inputChannel the channel to consume in reactive manner.
* #param reactiveMessageHandler the {#link ReactiveMessageHandler} to process messages.
* #since 5.3
*/
public ReactiveStreamsConsumer(MessageChannel inputChannel, ReactiveMessageHandler reactiveMessageHandler) {
Assert.notNull(inputChannel, "'inputChannel' must not be null");
this.inputChannel = inputChannel;
this.handler = new ReactiveMessageHandlerAdapter(reactiveMessageHandler);
this.reactiveMessageHandler = reactiveMessageHandler;
this.publisher = IntegrationReactiveUtils.messageChannelToFlux(inputChannel);
this.subscriber = null;
this.lifecycleDelegate =
reactiveMessageHandler instanceof Lifecycle ? (Lifecycle) reactiveMessageHandler : null;
}
During the test it creates the endpoint bean, and then during the substituteMessageHandlerFor it throws an exception of missing subscriber field
Integration flow is simple with single DB reactive handler.
Any ideas? Thanks a lot.

This is a bug in the testing framework. When we introduced a ReactiveMessageHandler support into that ReactiveStreamsConsumer, we just missed to adjust MockIntegrationContext logic respectively.
There is no way to mock a reactive endpoint at the moment as any reasonable workaround. However you can introduce some intermediate endpoint into the flow, e.g. bridge() and mock just this one without any return. So, your test will pass and nothing will be send to the real reactive endpoint in the end.
Feel free to raise a GH issue and we will look into that ASAP.

Related

Spring SQS Message Handler - add custom message attributes to existing message before sending it to Dead-Letter-Queue

Currently, the messages on source queue are sent to dead-letter-queue on every exception. My goal is to add custom attributes to the failed message so that engineers have more information about failure when they monitor the dead-letter-queue.
When I tried to add attributes to existing message, I receive an java.lang.UnsupportedOperationException exception.
Here is the aspect code where I add message attributes to existing message. I am using custom aspect which is triggered before sending the message with AmazonSQSClient.
#Before(value = "execution(* com.amazonaws.services.sqs.AmazonSQS*Client.sendMessage*(com.amazonaws.services.sqs.model.SendMessageRequest,..)) && args(request,..)",
argNames = "request")
public void before(SendMessageRequest request) {
Map<String, MessageAttributeValue> messageAttributes = request.getMessageAttributes();
if (messageAttributes == null) return;
messageAttributes.put("MoveToDlqFlag", createMessageAttribute("true"));
}
This is the code where exception happens.
#SqsListener(value = {"${sqs.queue.source-queue}"}, deletionPolicy = ON_SUCCESS)
public void handleMessageReceived(String rawMessage, #Header("SenderId") String senderId, #Headers Map<String, Object> header) {
var orderMessageWrapper = messageWrapperUtil.create(rawMessage, Order.class);
var order = orderMessageWrapper.getMessage();
var receiveCount = (Integer) header.get("ApproximateReceiveCount");
...
}
Is there a way to add message attributes to existing message before sending to dead-letter-queue? Maybe spring provides a configuration where it is possible.
If messageAttributes map throws java.lang.UnsupportedOperationException, then maybe you could try to create a new mutable map, which is a copy of immutable map:
Map<String, MessageAttributeValue> messageAttributes = new HashMap<>(request.getMessageAttributes());
and then you can use setMessageAttributes() of SendMessageRequest
So I hope that this solution would work (unless setMessageAttributes doesn't throw UnsupportedOperationException too)
Map<String, MessageAttributeValue> messageAttributes = request.getMessageAttributes() == null ? new HashMap<>() : new HashMap<>(request.getMessageAttributes());
messageAttributes.put("MoveToDlqFlag", createMessageAttribute("true"));
request.setMessageAttributes(messageAttributes);

Overriding errorChannel configured in #MessagingGateway

I have configured #MessagingGateway as below to use an error channel, which works as expected.
#MessagingGateway(errorChannel = "DefaultInboundErrorHandlerChannel")
public interface InboundMessagingGateway {
#Gateway(requestChannel = "InboundEntryChannel")
void receive(XferRes response);
}
Within the flow I am passing the object to a transformer as below:
Step 1:
#Transformer(inputChannel = "InboundEntryChannel", outputChannel = "TransmissionLogChannel")
public CassandraEntity createEntity(
org.springframework.messaging.Message<XferRes> message) throws ParseException {
XferRes response = message.getPayload();
CassandraEntity entity = new CassandraEntity();
// ... getters & setter ommitted for brevity
return entity;
}
Next, I update the entity as below:
Step 2:
#ServiceActivator(inputChannel = "TransmissionLogChannel", outputChannel="PublishChannel")
public XferRes updateCassandraEntity(
org.springframework.messaging.Message<XferRes> message) {
XferRes response = message.getPayload();
this.cassandraServiceImpl.update(response);
return response;
}
And last, I post to a Kafka topic as below:
Step 3:
#ServiceActivator(inputChannel = "PublishChannel")
public void publish(org.springframework.messaging.Message<XferRes> message){
XferRes response = message.getPayload();
publisher.post(response);
}
In case of an error I post the message to a service which publishes the error object to log ingestion:
#ServiceActivator(inputChannel="defaultInboundErrorHandlerChannel")
public void handleInvalidRequest(org.springframework.messaging.Message<MessageHandlingException> message) throws ParseException {
XferRes originalRequest = (XferRes) message.getPayload().getFailedMessage().getPayload();
this.postToErrorBoard(originalRequest)
}
If an error occurs at Step 2: in updating the DB, then also I want to invoke Step 3. A trivial way is to remove the Step 2 & make the call to update database from Step 1.
Is there any other way in Spring Integration where I can invoke Step 3 irrespective if an error occurs or not.
This technique called PublishSubscribeChannel. Since I see that you reuse a payload on the second step to send to the third step, then it is definitely a use-case for the PublishSubscribeChannel and two sequential subscribers to it.
I mean you create a PublishSubscribeChannel #Bean and those #ServiceActivators are use the name to this channel.
More info is in the Reference Manual. Pay attention to the ignoreFailures property:
/**
* Specify whether failures for one or more of the handlers should be
* ignored. By default this is <code>false</code> meaning that an Exception
* will be thrown whenever a handler fails. To override this and suppress
* Exceptions, set the value to <code>true</code>.
* #param ignoreFailures true if failures should be ignored.
*/
public void setIgnoreFailures(boolean ignoreFailures) {

Wait for some Spring ListenableFuture

There is a service which uses Spring AsyncRestTemplate for sending rest calls. Each call of AsyncRestTemplate.exchange() returns ListenableFuture. Something like this:
ListenableFuture future1 = new AsyncRestTemplate().exchange(...);
ListenableFuture future2 = new AsyncRestTemplate().exchange(...);
ListenableFuture future3 = new AsyncRestTemplate().exchange(...);
Is there a way for create single ListenableFuture which combines all other calls? Something like Futures.allAsList from Guava.
Step #1: Convert each ListenableFuture to CompletableFuture
public CompletableFuture<T> toCompletableFuture(ListenableFuture<ResponseEntity<String>> listenableFuture) {
final CompletableFuture<T> completableFuture = new CompletableFuture<>();
listenableFuture.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() {
#Override
public void onFailure(Throwable e) {
completableFuture.completeExceptionally(e);
}
}
#Override
public void onSuccess(ResponseEntity<String> result) {
completableFuture.complete(parseResponse(result));
}
}
});
return completableFuture;
}
Step #2: call CompletableFuture.allOf(cF1, cF2, cF3...)
Since Spring 5.0, ListenableFuture implements the method completable() which does exactly what you need:
/**
* Expose this {#link ListenableFuture} as a JDK {#link CompletableFuture}.
* #since 5.0
*/
default CompletableFuture<T> completable() {

Spring AMQP #RabbitListener convert to origin object

I try to send a message based on a flatten MAP using Spring Boot and AMQP. The message should then be received using #RabbitListener and transfer it back to a MAP.
First I have nested json String and flat it and send it using the following code:
// Flatten the JSON String returned into a map
Map<String,Object> jsonMap = JsonFlattener.flattenAsMap(result);
rabbitTemplate.convertAndSend(ApplicationProperties.rmqExchange, ApplicationProperties.rmqTopic, jsonMap, new MessagePostProcessor() {
#Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setHeader("amqp_Key1", "wert1");
message.getMessageProperties().setHeader("amqp_Key2", "Wert2");
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
}
});
So far so good.
On the receiving site I try to use a Listener and convert the message payload back to the Map as it was send before.
The problem ist that I have no idea how to do it.
I receive the message with the following code:
#RabbitListener(queues = "temparea")
public void receiveMessage(Message message) {
log.info("Receiving data from RabbitMQ:");
log.info("Message is of type: " + message.getClass().getName());
log.info("Message: " + message.toString());
}
As I mentioned before I have no idea how I can convert the message to my old MAP. The __ TypeId __ of the Message is: com.github.wnameless.json.flattener.JsonifyLinkedHashMap
I would be more than glad if somebody could assist me how I get this message back to an Java Map.
BR
Update after answer from Artem Bilan:
I added the following code to my configuration file:
#Bean
public SimpleRabbitListenerContainerFactory myRabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setConnectionFactory(connectionFactory());
factory.setMaxConcurrentConsumers(5);
return factory;
}
But still I have no idea how to get the Map out of my message.
The new code block does not change anything.
You have to configure Jackson2JsonMessageConverter bean and Spring Boot will pick it up for the SimpleRabbitListenerContainerFactory bean definition which is used to build listener containers for the #RabbitListener methods.
UPDATE
Pay attention to the Spring AMQP JSON Sample.
There is a bean like jsonConverter(). According Spring Boot auto-configuration this bean is injected to the default:
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
Which is really used for the #RabbitListener by default, when the containerFactory attribute is empty.
So, you need just configure that bean and don't need any custom SimpleRabbitListenerContainerFactory. Or if you do that you should specify its bean name in that containerFactory attribute of your #RabbitListener definitions.
Another option to consider is like Jackson2JsonMessageConverter.setTypePrecedence():
/**
* Set the precedence for evaluating type information in message properties.
* When using {#code #RabbitListener} at the method level, the framework attempts
* to determine the target type for payload conversion from the method signature.
* If so, this type is provided in the
* {#link MessageProperties#getInferredArgumentType() inferredArgumentType}
* message property.
* <p> By default, if the type is concrete (not abstract, not an interface), this will
* be used ahead of type information provided in the {#code __TypeId__} and
* associated headers provided by the sender.
* <p> If you wish to force the use of the {#code __TypeId__} and associated headers
* (such as when the actual type is a subclass of the method argument type),
* set the precedence to {#link TypePrecedence#TYPE_ID}.
* #param typePrecedence the precedence.
* #since 1.6
* #see DefaultJackson2JavaTypeMapper#setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence)
*/
public void setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence typePrecedence) {
So, if you want still to have a Message as a method argument but get a gain of the JSON conversion based on the __TypeId__ header, you should consider to configure Jackson2JsonMessageConverter to be based on the Jackson2JavaTypeMapper.TypePrecedence.TYPE_ID.

Spring WS (DefaultWsdl11Definition) HTTP status code with void

We have a (working) SOAP web service based on Spring WS with DefaultWsdl11Definition.
This is basically what it looks like:
#Endpoint("name")
public class OurEndpoint {
#PayloadRoot(namespace = "somenamespace", localPart = "localpart")
public void onMessage(#RequestPayload SomePojo pojo) {
// do stuff
}
}
It is wired in Spring and it is correctly processing all of our SOAP requests. The only problem is that the method returns a 202 Accepted. This is not what the caller wants, he'd rather have us return 204 No Content (or if that is not possible an empty 200 OK).
Our other endpoints have a valid response object, and do return 200 OK. It seems void causes 202 when 204 might be more appropriate?
Is it possible to change the response code in Spring WS? We can't seem to find the correct way to do this.
Things we tried and didn't work:
Changing the return type to:
HttpStatus.NO_CONTENT
org.w3c.dom.Element <- not accepted
Adding #ResponseStatus <- this is for MVC, not WS
Any ideas?
Instead of what I wrote in the comments it is possibly the easiest to create a delegation kind of solution.
public class DelegatingMessageDispatcher extends MessageDispatcher {
private final WebServiceMessageReceiver delegate;
public DelegatingMessageDispatcher(WebServiceMessageReceiver delegate) {
this.delegate = delegate;
}
public void receive(MessageContext messageContext) throws Exception {
this.delegate.receive(messageContext);
if (!messageContext.hasResponse()) {
TransportContext tc = TransportContextHolder.getTransportContext();
if (tc != null && tc.getConnection() instanceof HttpServletConnection) {
((HttpServletConnection) tc.getConnection()).getHttpServletResponse().setStatus(200);
}
}
}
}
Then you need to configure a bean named messageDispatcher which would wrap the default SoapMessageDispatcher.
#Bean
public MessageDispatcher messageDispatcher() {
return new DelegatingMessageDispatcher(soapMessageDispatcher());
}
#Bean
public MessageDispatcher soapMessageDispatcher() {
return new SoapMessageDispatcher();
}
Something like that should do the trick. Now when response is created (In the case of a void return type), the status as you want is send back to the client.
When finding a proper solutions we've encountered some ugly problems:
Creating custom adapters/interceptors is problematic because the handleResponse method isn't called by Spring when you don't have a response (void)
Manually setting the status code doesn't work because HttpServletConnection keeps a boolean statusCodeSet which doesn't get updated
But luckily we managed to get it working with the following changes:
/**
* If a web service has no response, this handler returns: 204 No Content
*/
public class NoContentInterceptor extends EndpointInterceptorAdapter {
#Override
public void afterCompletion(MessageContext messageContext, Object o, Exception e) throws Exception {
if (!messageContext.hasResponse()) {
TransportContext tc = TransportContextHolder.getTransportContext();
if (tc != null && tc.getConnection() instanceof HttpServletConnection) {
HttpServletConnection connection = ((HttpServletConnection) tc.getConnection());
// First we force the 'statusCodeSet' boolean to true:
connection.setFaultCode(null);
// Next we can set our custom status code:
connection.getHttpServletResponse().setStatus(204);
}
}
}
}
Next we need to register this interceptor, this can be easily done using Spring's XML:
<sws:interceptors>
<bean class="com.something.NoContentInterceptor"/>
</sws:interceptors>
A big thanks to #m-deinum for pointing us in the right direction!
To override the afterCompletion method really helped me out in the exact same situation. And for those who use code based Spring configuration, hereĀ“s how one can add the interceptor for a specific endpoint.
Annotate the custom interceptor with #Component, next register the custom interceptor to a WsConfigurerAdapter like this:
#EnableWs
#Configuration
public class EndpointConfig extends WsConfigurerAdapter {
/**
* Add our own interceptor for the specified WS endpoint.
* #param interceptors
*/
#Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
interceptors.add(new PayloadRootSmartSoapEndpointInterceptor(
new NoContentInterceptor(),
"NAMESPACE",
"LOCAL_PART"
));
}
}
NAMESPACE and LOCAL_PART should correspond to the endpoint.
If someone ever wanted to set custom HTTP status when returning non-void response, here is solution:
Spring Boot WS-Server - Custom Http Status

Resources