Spring-Integration: how to inspect a http response within an AbstractRequestHandlerAdvice with webflux? - spring-integration

This question pertains directly to another question: Spring-Integration AbstractRequestHandlerAdvice with webflux/reactive: does this introduce a synchronous process?
The return result retrieved by the call from the advice handler to an http:outbound-gateway is either a Message<?> or a MessageBuilder (or perhaps only the latter).
public class MyLogger extends AbstractRequestHandlerAdvice {
// ...
#Override
protected Object doInvoke(ExecutionCallback callback, Object target, Message<?> message(
{
// ... logging before the call ...
Object result = callback.execute();
// ... logging after the call ...
return result;
}
}
When migrating to webflux and hence to webflux:outbound-gateway, the advice handler is now retrieving a MonoMapFuseable type in the result. Is it possible to read the information from MonoMapFuseable in order to log the return payload without consuming definitively the result? I'm a bit of a loss at how to do this.
Thanks for any insights.

I am not a reactor expert, but you should be able to call share() on the returned mono; then subscribe to that mono to get a copy of the result.
/**
* Prepare a {#link Mono} which shares this {#link Mono} result similar to {#link Flux#shareNext()}.
* This will effectively turn this {#link Mono} into a hot task when the first
* {#link Subscriber} subscribes using {#link #subscribe()} API. Further {#link Subscriber} will share the same {#link Subscription}
* and therefore the same result.
* It's worth noting this is an un-cancellable {#link Subscription}.
* <p>
* <img class="marble" src="doc-files/marbles/shareForMono.svg" alt="">
*
* #return a new {#link Mono}
*/
public final Mono<T> share() {
I just tried it with this...
#Bean
ApplicationRunner runner() {
return args -> {
One<Object> one = Sinks.one();
Mono<Object> mono = one.asMono();
Mono<Object> mono1 = mono.share();
Mono<Object> mono2 = mono.share();
subsMono(mono1, 1);
subsMono(mono2, 2);
subsMono(mono, 0);
one.emitValue("foo", null);
};
}
private void subsMono(Mono mono, int i) {
mono.doOnNext(obj -> System.out.println(obj.toString() + i))
.subscribe();
}
foo1
foo2
foo0

Related

Spring integration - how to initiate a database transaction on the flow?

I have an integration flow that sinks DML queries at the end.
I have a config file to log transactions:
logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.transaction.support=DEBUG
The sinkSql method is called but there is no transaction log.
If I just call e.transactional(true) I get an error because there are two transaction managers (one is from the source database).
#Bean(name = SYBASE_TRAN_MANAGER)
public PlatformTransactionManager transactionManager(#Qualifier(SYBASE_DS) final DataSource sybaseDataSource) {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(sybaseDataSource);
return dataSourceTransactionManager;
}
#Bean
public TransactionInterceptor sybaseTransactionInterceptor(#Qualifier(SYBASE_TRAN_MANAGER) final TransactionManager tm) {
return new TransactionInterceptorBuilder(true)
.transactionManager(tm)
.isolation(Isolation.READ_COMMITTED)
.propagation(Propagation.REQUIRES_NEW)
.readOnly(false)
.build();
}
#Bean
public IntegrationFlow sinkSqlFlow(final TransactionInterceptor sybaseTransactionInterceptor) {
return IntegrationFlows.from(SYBASE_SINK_SQL)
.enrichHeaders(h -> h.header(MessageHeaders.ERROR_CHANNEL, SYBASE_ERROR))
.handle(this::sinkSql, e -> e.transactional(sybaseTransactionInterceptor))
.get();
}
public void sinkSql(final Message<?> message) {
//jdbcTemplate logic here
}
Not sure why is the question since TransactionAspectSupport does just this plain fallback if we don't provide an explicit TransactionManager to the interceptor configuration:
if (defaultTransactionManager == null) {
defaultTransactionManager = this.beanFactory.getBean(TransactionManager.class);
this.transactionManagerCache.putIfAbsent(
DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
}
where that getBean(Class aClass) indeed is going to fail if several beans of type is present in the application context. So, what you did so far with a sybaseTransactionInterceptor bean definition is OK.
You can use an overloaded Java DSL method though:
/**
* Specify a {#link TransactionInterceptor} {#link Advice} with the provided
* {#code PlatformTransactionManager} and default
* {#link org.springframework.transaction.interceptor.DefaultTransactionAttribute}
* for the {#link MessageHandler}.
* #param transactionManager the {#link TransactionManager} to use.
* #param handleMessageAdvice the flag to indicate the target {#link Advice} type:
* {#code false} - regular {#link TransactionInterceptor}; {#code true} -
* {#link org.springframework.integration.transaction.TransactionHandleMessageAdvice}
* extension.
* #return the spec.
*/
public S transactional(TransactionManager transactionManager, boolean handleMessageAdvice) {
Although having your sink contract as void sinkSql(final Message<?> message), there is no need in that true: the transaction is going to be applied for a handleMessage() method which is really the end of your flow anyway.

DelayHandler messageGroupId

From the Spring Integration documentation (https://docs.spring.io/spring-integration/docs/5.1.7.RELEASE/reference/html/#delayer) it is not clear to me what the messageGroupId in the DelayHandler means exactly and which value I have to set there exactly (is it arbitrary?). This value does not exist in the xml configuration, but does in the Java configuration.
#ServiceActivator(inputChannel = "input")
#Bean
public DelayHandler delayer() {
DelayHandler handler = new DelayHandler("delayer.messageGroupId"); // THIS constructor parameter is not clear to me
handler.setDefaultDelay(3_000L);
handler.setDelayExpressionString("headers['delay']");
handler.setOutputChannelName("output");
return handler;
}
It is explained in the JavaDocs of that constructor:
/**
* Create a DelayHandler with the given 'messageGroupId' that is used as 'key' for
* {#link MessageGroup} to store delayed Messages in the {#link MessageGroupStore}.
* The sending of Messages after the delay will be handled by registered in the
* ApplicationContext default
* {#link org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler}.
* #param messageGroupId The message group identifier.
* #see #getTaskScheduler()
*/
public DelayHandler(String messageGroupId) {
It is not required because the groupId is based on the required id attribute:
String id = element.getAttribute(ID_ATTRIBUTE);
if (!StringUtils.hasText(id)) {
parserContext.getReaderContext().error("The 'id' attribute is required.", element);
}
...
builder.addConstructorArgValue(id + ".messageGroupId");
It is really mentioned and explained a little bit in the docs: https://docs.spring.io/spring-integration/docs/current/reference/html/messaging-endpoints.html#delayer-namespace.
The value indeed is arbitrary, but it must be unique per your application, so different delayers don't steal messages from each other.

spring integration adviceChain on ServiceActivator is not executing the beforeReceive of an advice

I defined my poller with a service activator with adviceChain like this:
#ServiceActivator(inputChannel = EVENT_CHANNEL,
adviceChain = {"globalLockAdvice"},
poller = #Poller(
maxMessagesPerPoll = "${event.poller.maxMessagesPerPoll:1}",
fixedDelay = "${event.poller.fixedDelay:1000}",
receiveTimeout = "${event.poller.receiveTimeout:1000}"))
public void handleEventMessage(Message<String> message) throws MessagingException {
...
}
#Component
public class GlobalLockAdvice implements ReceiveMessageAdvice {
private final LockConfiguration lockConfiguration;
public GlobalLockAdvice(LockConfiguration lockConfiguration) {
this.lockConfiguration = lockConfiguration;
}
#Override
public boolean beforeReceive(Object source) {
return lockConfiguration.isLeader();
}
#Override
public Message<?> afterReceive(Message<?> result, Object source) {
return result;
}
}
But beforeReceive is not called.
When debugging I see 'target' is a ServiceActivator class, resulting in skipping calling the beforeReceive:
#FunctionalInterface
public interface ReceiveMessageAdvice extends MethodInterceptor {
#Override
#Nullable
default Object invoke(MethodInvocation invocation) throws Throwable {
Object target = invocation.getThis();
if (!(target instanceof MessageSource) && !(target instanceof PollableChannel)) {
return invocation.proceed();
}
...
What am I doing wrong?
The advice chain on the service activator advises the message handler, not the channel polled by the poller.
To add an advice chain to the poller, you must define the poller as a PollerMetadata bean instead of using the #Poller annotation attributes. #Poller("metadata").
See the javadocs for #Poller.
/**
* Provides the {#link org.springframework.integration.scheduling.PollerMetadata} options
* for the Messaging annotations for polled endpoints. It is an analogue of the XML
* {#code <poller/>} element, but provides only simple attributes. If the
* {#link org.springframework.integration.scheduling.PollerMetadata} requires more options
* (e.g. Transactional and other Advices) or {#code initialDelay} etc, the
* {#link org.springframework.integration.scheduling.PollerMetadata} should be configured
* as a generic bean and its bean name can be specified as the {#code value} attribute of
* this annotation. In that case, the other attributes are not allowed.
* <p>
* Non-reference attributes support Property Placeholder resolutions.

Handle a method return

In my project I will use a process method in the handle method and based on the return type that will be boolean I will have to notify the advice success for true or failure for false.
Can someone please let me know how that could be done in the integration flow? is there any other implementation probably.
Code:
#Bean
public IntegrationFlow ftpInboundFlow() {
return IntegrationFlows
.from(Ftp.inboundAdapter(ftpSessionFactory())
.preserveTimestamp(true)
.remoteDirectory(appProperties.getFtp().getRemoteDirectory())
.patternFilter(appProperties.getFtp().getFilter())
.deleteRemoteFiles(true)
.localDirectory(new File("inbound"))
.temporaryFileSuffix(appProperties.getFtp().getTemporaryFileSuffix()),
e -> e.id("ftpInboundAdapter")
.poller(Pollers.fixedDelay(appProperties.getFtp().getPollerDelay()))
.autoStartup(true))
.transform(new FileToByteArrayTransformer())
.<byte[]>handle((p, h) -> {
log.info("After transform " + p);
log.info("Headers " + h);
CustomerFile customerFile = CustomerFile.builder()
.content(p)
.customerFileType(CustomerFileType.ORDER_BOOK)
.filename(appProperties.ftp.getFileName())
.build();
customerFileDataService.save(customerFile);
boolean isProssed = orderBookFileProcessor.process(customerFile);
log.info("is Processed : " + isProssed);
return isProssed;
})
.route(Boolean.class, p -> p.equals(false) ? "historyChannel" : "errorChannel")
.get();
}
#Bean
public IntegrationFlow history(){
return IntegrationFlows.from("historyChannel")
.transform("genericMessage.headers['file_originalFile']")
.handle(Ftp.outboundAdapter(ftpSessionFactory(), FileExistsMode.REPLACE)
.useTemporaryFileName(true)
.autoCreateDirectory(true)
.remoteDirectory("/ftp/ge/inbound/history"))
.get();
}
error:
Caused by: org.springframework.messaging.MessageHandlingException: Expression evaluation failed: genericMessage.headers['file_originalFile']; nested exception is org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'genericMessage' cannot be found on object of type 'org.springframework.messaging.support.GenericMessage' - maybe not public or not valid?, failedMessage=GenericMessage [payload=false, headers={file_remoteHostPort=bey-notes-fs.bey.ei:21, file_name=OA_ex_PK_2020_2023.csv, file_remoteDirectory=//ftp/GE/Inbound, file_originalFile=inbound\OA_ex_PK_2020_2023.csv, id=3a6ecf72-e9cd-43e6-bf0c-25020c0ab30d, file_relativePath=OA_ex_PK_2020_2023.csv, file_remoteFile=OA_ex_PK_2020_2023.csv, timestamp=1640030927539}]
I think you need to take a look to different handle() variant:
/**
* Populate a {#link ServiceActivatingHandler} for the
* {#link org.springframework.integration.handler.MethodInvokingMessageProcessor}
* to invoke the provided {#link GenericHandler} at runtime.
* Typically used with a Java 8 Lambda expression:
* <pre class="code">
* {#code
* .<Integer>handle((p, h) -> p / 2)
* }
* </pre>
* Use {#link #handle(Class, GenericHandler)} if you need to access the entire
* message.
* #param handler the handler to invoke.
* #param <P> the payload type to expect.
* #return the current {#link IntegrationFlowDefinition}.
* #see org.springframework.integration.handler.LambdaMessageProcessor
*/
public <P> B handle(GenericHandler<P> handler) {
The one you have chosen comes with plain void handle(Message<?>) contract. So, no any return are expected from this call. Since you have a result from the process() method call and you want to process it, then you need to change your flow behavior from the consumer to function at this point:
.<byte[]>handle( (p, h) -> {
log.info("After transform " + p);
CustomerFile customerFile = CustomerFile.builder()
.content(p)
.customerFileType(CustomerFileType.ORDER_BOOK)
.build();
customerFileService.saveCustomerFile(customerFile);
return orderBookFileProcessor.process(customerFile);
})

How to automatically escape variables in a Zend Framework 2 view

A lot of times in a Zend Framework 2 view I'll be calling $this->escapeHtml() to make sure my data is safe. Is there a way to switch this behaviour from a blacklist to a whitelist?
PS: Read an article from Padraic Brady that suggests that automatic escaping is a bad idea. Additional thoughts?
You could write your own ViewModel class which escapes data when variables are assigned to it.
Thanks to Robs comment, I extended the ZF2 ViewModel as follows:
namespace Application\View\Model;
use Zend\View\Model\ViewModel;
use Zend\View\Helper\EscapeHtml;
class EscapeViewModel extends ViewModel
{
/**
* #var Zend\View\Helper\EscapeHtml
*/
protected $escaper = null;
/**
* Proxy to set auto-escape option
*
* #param bool $autoEscape
* #return ViewModel
*/
public function autoEscape($autoEscape = true)
{
$this->options['auto_escape'] = (bool) $autoEscape;
return $this;
}
/**
* Property overloading: get variable value;
* auto-escape if auto-escape option is set
*
* #param string $name
* #return mixed
*/
public function __get($name)
{
if (!$this->__isset($name)) {
return;
}
$variables = $this->getVariables();
if($this->getOption('auto_escape'))
return $this->getEscaper()->escape($variables[$name]);
return $variables[$name];
}
/**
* Get instance of Escaper
*
* #return Zend\View\Helper\EscapeHtml
*/
public function getEscaper()
{
if (null === $this->escaper) {
$this->escaper = new EscapeHtml;
}
return $this->escaper;
}
}
In a Controller it could be used like this:
public function fooAction()
{
return new EscapeViewModel(array(
'foo' => '<i>bar</i>'
));
//Turn off auto-escaping:
return new EscapeViewModel(array(
'foo' => '<i>bar</i>'
),['auto_escape' => false]);
}
Question:
I would appreciate it if soemebody would comment, if this is best practice or if there is a better and ecp. more efficient and resource-saving way?

Resources