Adding ConversionService to Spring Cloud Stream - spring-integration

I'm using Spring Integration and String Cloud Stream. I have a header that I want my HTTP gateway to use, which has a Long value, but it can't convert from Long to String by default and so displays the error Consider registering a Converter with ConversionService.
Therefore I tried adding my own LongToStringConverter class and the following Bean so that LongToStringConverter can be used:
#Bean
public ConversionService conversionService()
{
DefaultConversionService service = new DefaultConversionService();
service.addConverter( new LongToStringConverter() );
return service;
}
Then then received the following error: Dispatcher has no subscribers.
If I only return an instance of DefaultConversionService from the above bean I still receive the error.
When I remove the above bean and instead simply convert the Long value to String when setting the header value and that works without errors. Is it possible to use ConversionService instead? If so then how?

First of all there is already a ConversionService: https://docs.spring.io/spring-integration/docs/4.3.12.RELEASE/reference/html/messaging-endpoints-chapter.html#payload-type-conversion. And it has some set of predefined converters. So, you should consider to use #IntegrationConverter on the matter.
On the other hand it is unclear why do you need to do that at all. I wonder why Long.toString() isn't enough for you when you declare that header in first place.

Related

No MessageBodyWriter for Single

I'm trying to use resteasy-rxjava2 to provide an XML document using jaxb, within a vertx application (using a non-vertx legacy library we have). But I get:
Could not find MessageBodyWriter for response object of type:
org.jboss.resteasy.rxjava2.propagation.ContextPropagatorOnSingleAssemblyAction$ContextPropagatorSingle of media type:
application/xml;charset=UTF-8
From what I can tell, this comes down to the difference between a MessageBodyWriter and the AsyncResponseProvider that is in the resteasy-rxjava2 dependency for a Single (SingleProvider).
I have the following resteasy service definition
#GET
#Path(FdsnwsPaths.QUERY)
#Produces(MediaType.APPLICATION_XML)
#Stream
// CHECKSTYLE:OFF too many parameters
public Response getQuery(...)
How do I get resteasy to properly serve the data asynchrously, using the SingleProvider or otherwise.
The #Get method must return the Single explicitly or it doesn't work. (Can't use Response or Object). In my case, the Single contains the jaxb xml root element.
#GET
#Path(FdsnwsPaths.QUERY)
#Produces(MediaType.APPLICATION_XML)
#Stream
public Single<FDSNStationXML> getQuery(...)
Then, to make things more complicated, in order to handle specific exception types and map them to specific response status codes, I have to create a custom ExceptionMapper which creates the Response object I used to be able to create directly in the method. (in resteasy-vertx, I found no documentation on how to do this, but in my case, I am creating my own VertxRestEasyDeployment object so I can register the class of the ExceptionMapper(s) like this:
VertxResteasyDeployment deployment = new VertxResteasyDeployment();
deployment.getActualProviderClasses().addAll(providerClasses);
For reference, this is all being done with:
RestEasy 5.0.3.Final (including resteasy-rxjava2)
RxJava 2.2.20
Vertx 3.9.5

Spring Integration - AMQP Inferred Types In Java DSL?

I have been working on a "paved road" for setting up asynchronous messaging between two micro services using AMQP. We want to promote the use of separate domain objects for each service, which means that each service must define their own copy of any objects passed across the queue.
We are using Jackson2JsonMessageConverter on both the producer and the consumer side and we are using the Java DSL to wire the flows to/from the queues.
I am sure there is a way to do this, but it is escaping me: I want the consumer side to ignore the __TypeID__ header that is passed from the producer, as the consumer may have a different representation of that event (and it will likely be in in a different java package).
It appears there was work done such that if using the annotation #RabbitListener, an inferredArgumentTypeargument is derived and will override the header information. This is exactly what I would like to do, but I would like to use the Java DSL to do it. I have not yet found a clean way in which to do this and maybe I am just missing something obvious. It seems it would be fairly straight forward to derive the type when using the following DSL:
return IntegrationFlows
.from(
Amqp.inboundAdapter(factory, queueRemoteTaskStatus())
.concurrentConsumers(10)
.errorHandler(errorHandler)
.messageConverter(messageConverter)
)
.channel(channelRemoteTaskStatusIn())
.handle(listener, "handleRemoteTaskStatus")
.get();
However, this results in a ClassNotFound exception. The only way I have found to get around this, so far, is to set a custom message converter, which requires explicit definition of the type.
public class ForcedTypeJsonMessageConverter extends Jackson2JsonMessageConverter {
ForcedTypeJsonMessageConverter(final Class<?> forcedType) {
setClassMapper(new ClassMapper() {
#Override
public void fromClass(Class<?> clazz, MessageProperties properties) {
//this class is only used for inbound marshalling.
}
#Override
public Class<?> toClass(MessageProperties properties) {
return forcedType;
}
});
}
}
I would really like this to be derived, so the developer does not have to really deal with this.
Is there an easier way to do this?
The simplest way is to configure the Jackson converter's DefaultJackson2JavaTypeMapper with TypeIdMapping (setIdClassMapping()).
On the sending system, map foo:com.one.Foo and on the receiving system map foo:com.two.Foo.
Then, the __TypeId__ header gets foo and the receiving system will map it to its representation of a Foo.
EDIT
Another option would be to add an afterReceiveMessagePostProcessor to the inbound channel adapter's listener container - it could change the __TypeId__ header.

HTTP Outbound Gateway - Passing URI parameters

I need to invoke a REST API via Spring Integration's HTTP outbound gateway. How do I substitute the path variable with a value from payload. Payload has just one String value. The following code snippet is sending the place holder as such. Any help is appreciated.
#Bean
public MessageHandler httpGateway(#Value("http://localhost:8080/api/test-resource/v1/{parameter1}/codes") URI uri) {
HttpRequestExecutingMessageHandler httpHandler = new HttpRequestExecutingMessageHandler(uri);
httpHandler.setExpectedResponseType(Map.class);
httpHandler.setHttpMethod(HttpMethod.GET);
Map<String, Expression> uriVariableExp = new HashMap();
SpelExpressionParser parser = new SpelExpressionParser();
uriVariableExp.put("parameter1", parser.parseExpression("payload.Message"));
httpHandler.setUriVariableExpressions(uriVariableExp);
return httpHandler;
}
Let take a look what #Value is for first of all!
* Annotation at the field or method/constructor parameter level
* that indicates a default value expression for the affected argument.
*
* <p>Typically used for expression-driven dependency injection. Also supported
* for dynamic resolution of handler method parameters, e.g. in Spring MVC.
*
* <p>A common use case is to assign default field values using
* "#{systemProperties.myProp}" style expressions.
Looking to your sample there is nothing to resolve as a dependency injection value.
You can just use:
new HttpRequestExecutingMessageHandler("http://localhost:8080/api/test-resource/v1/{parameter1}/codes");
Although it doesn't matter in this case...
You code looks good, unless we don't know what your payload is.
That payload.Message expression looks odd. From big height it may mean like MyClass.getMessage(), but it can't invoke the getter because you use a property name as capitalized. If you really have there such a getter, so use it like payload.message. Otherwise, please, elaborate more. Some logs, StackTrace, the info about payload etc... It's fully unclear what is the problem.

Omitting fields during serialization for specific Gson serializer

I have a stateless REST API build on Spring Boot 1.4.2. I want to log all the API calls into elk. Requests and responses data (headers, parameters, payload) need to be logged as well. I don't want to log them 1:1 - I want to filter out sensitive data etc.
I made an aspect that is intercepting my #RestController's methods invocation. I created custom annotation for method's parameter that should be logged (I use it on payloads annotated as well by #RequestBody) following this article and it gave me access to my data transfer objects in my #Around advice. I dont care about their type - I would like to call logger.debug(logObject) and send this log to logstash.
As far as I understand log message should be send as JSON with JSONLayout set in Log4j2 appender to ease things on the logstash side. So I serialize my logObject into JSON log message but during this and this only serialization I want to filter sensitive data out. I can not use transient because my controller depends on the same field.
Can I somehow create an #IgnoreForLogging annotation, that will be detected only by my custom Gson serializer that I use within logging advice and will be ignored within standard Spring's infrastructure? Is my logging into logstash approach even correct (I am trying to set it up for the first time)?
I can't believe I missed that in documentation. Here is the link
My custom annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface IgnoreForLogging {
}
Strategy for serializing objects:
public class LoggingExclusionStrategy implements ExclusionStrategy {
#Override
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
return fieldAttributes.getAnnotation(IgnoreForLogging.class) != null;
}
#Override
public boolean shouldSkipClass(Class<?> aClass) {
return false;
}
}
Serializing log message in aspect class:
Gson gson = new GsonBuilder()
.setExclusionStrategies(new LoggingExclusionStrategy())
.create();
String json = gson.toJson(logObject);
This way Spring internally uses default serializer that doesn't know about #IgnoreForLogging and I can take advantage of my annotation in other places.

Design: Spring Integration jdbc best practice

After using Spring Integration in a project, my observation is to use jdbc adapter or gateway only at the start or end of the flow. If we use them in the middle of flow then it will become too verbose and complex.
For example:
<jdbc:outbound-gateway
query="select * from foo where
c1=:headers[c1] AND
c2=:headers[c2] AND
c3=:headers[c3] AND
c4=:headers[c4]"
row-mapper="fooMapper" data-source="myDataSource" max-rows-per-poll="100000" />
<int:service-activator ref="serviceActivator" method="processFoo" />
In the above <jdbc:outbound-gateway>, we need to pass all the placeholders (c1, c2, c3, c4) in the header of Message. We need to look back and forth in java code and xml file for any change in where condition or when there are too many where clauses.
It is also error prone. For example, if we misspelled :headers[c1] to :headers[d1] then it will not throw any exception and replace :headers[d1] with null.
If query does not return any row then it will throw exception by default. So, we have to use requires-reply="false" to change default behaviour.
If we want to proceed when query does not return any value then we have to add advice to gateway, as shown below:
<jdbc:outbound-gateway ... >
<jdbc:request-handler-advice-chain>
<bean class="com.service.NullReplyAdvice" />
</jdbc:request-handler-advice-chain>
</jdbc:outbound-gateway>
Please correct me if there are flaws in understanding of the concept.
We need to look back and forth in java code and xml file for any change in where condition or when there are too many where clauses.
It's true even for raw Java code around the JDBC: if you change the model you, of course, should change the SELECT, because it is just a String. And that's why there is a lot of work to make it type-safe - ORM, QueryDSL, Spring-Data etc.
if we misspelled :headers[c1] to :headers[d1] then it will not throw any exception and replace :headers[d1] with null.
That's because the headers is just a Map and it's truth that you get null, if there is no such a key in the map. To overcome that typo issue you can use POJO payload with getters, or some custom header, and again - the POJO with getters. In this case you end up with exception that there is no such a property against object. Although you'll see that issue only at runtime, not on compile. And again the same is with Hashtable - only at runtime.
So, we have to use requires-reply="false" to change default behaviour.
You should understand it at design time: allow or not to return nothing for the component.
The last idea is good. Wouldn't you mind to share your NullReplyAdvice?
Actually I achieve the same with <filter> before the JDBC gateway: to determine if there is something to fetch by count(*) query. From there I can lead my flow to the different logic, rather than the direct flow, when SELECT returns rows.
UPDATE
When you want to use Model object to keep business-specific values within Message, it's just enough to put this object to the header:
public class Foo {
private String foo1;
private String foo2;
public String getFoo1() {
return foo1;
}
public String getFoo2() {
return foo2;
}
}
...
MessageBuilder.withPayload(payload).setHeader("foo", foo).build();
...
<jdbc:outbound-gateway
query="select * from foo where
c1=:headers[foo].foo1 AND
c1=:headers[foo].foo2"/>

Resources