Spring Cloud Stream message JSON conversion is not working - java

I followed my previous question Spring Cloud Stream message from/to JSON conversion configuration and configured stream as described, yet, I can't make it work correctly.
My setup is as follows. I have two apps A and B. App A uses input channel one, output two. App B uses input two. Channel two is configured with content type application/json.
App A. Properties.
spring.cloud.stream.bindings.input.destination=one
spring.cloud.stream.bindings.input.group=default
spring.cloud.stream.bindings.output.destination=two
spring.cloud.stream.bindings.output.content-type=application/json
Listener method.
#ServiceActivator(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT)
public Dto handle(byte[] payload) throws IOException {
final Dto dto = new ObjectMapper().readValue(payload, Dto.class);
logger.info("{}", dto);
dto.setId(dto.getId() + 1000);
return dto;
}
App B. Properties.
spring.cloud.stream.bindings.input.destination=two
spring.cloud.stream.bindings.input.group=default
spring.cloud.stream.bindings.input.content-type=application/json
Listener method.
#ServiceActivator(inputChannel = Sink.INPUT)
public void handle(Dto dto) throws IOException {
logger.info("DTO {}", dto);
}
When I manually send a message with proper JSON string to channel one, it is processed correctly and send to channel two as a JSON message (headers exactly the same as described in above mentioned question). After that, it's received on channel two by App B and exception is thrown: Method handle(java.lang.String) cannot be found
Of course when I create both methods, handling Dto and String as an input, it works, but always String method is invoked and have to deserialize the payload by myself.
Am I mistaken somewhere? How do I setup method with such signature: public Dto handle(Dto incoming)?

You should change the content-type declaration of input of AppB to
application/x-java-object;type=your.package.Dto.
As it is specified in your question, of course you accept JSON strings only.

if use #StreamListener you don't have to use the answer way, but you have to remove (don't specify anything, otherwise it will be a json string):
spring.cloud.stream.bindings.input.content-type=application/json
from AppB properties
source (old docs but still valid):
https://docs.spring.io/spring-cloud-stream/docs/Brooklyn.RELEASE/reference/html/contenttypemanagement.html#__literal_streamlistener_literal_and_message_conversion

Related

Why need to then Mono.empty() in the default implementation of generated Open API Spring code?

Here is the default implementation of an API generated by the openapi-generator-maven-plugin using Spring Boot as library:
default Mono<ResponseEntity<Void>> testAPI(
#Parameter(hidden = true) final ServerWebExchange exchange
) {
Mono<Void> result = Mono.empty();
exchange.getResponse().setStatusCode(HttpStatus.NOT_IMPLEMENTED);
return result.then(Mono.empty());
}
Being new to this, there are several things I don't understand:
There are two Mono.empty(), one being the result, one inside the then(Mono.empty()), why is it done like that?
Why can't it just returns one? e.g. return Mono.empty();
Or better yet, remove also the pass in exchange and just do:
return Mono.just(ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build());
The default implementation is more like a template that gives you a hint how to complete this API controller. For an API controller usually you need to create a response in at least two steps: first fetch data from some source and then make it a valid response. The template code can give you a start point to write such code. For example, I can write the following code using the template:
public class UsersApiController implements UsersApi {
#Override
public Mono<ResponseEntity<String>> usersGet(
#Parameter(hidden = true) final ServerWebExchange exchange
) {
var client = WebClient.create("http://calapi.inadiutorium.cz/");
Mono<String> result = client.get().uri("/api/v0/en/calendars/general-en/today").retrieve().bodyToMono(String.class);
return result.map(rep -> ResponseEntity.status(HttpStatus.OK).body(rep));
}
}
The first Mono.empty becomes the WebClient that gets data from another API, and the second Mono.empty is replaced by a map operation that transforms the API result to ResponseEntity object. If the generator only generates a Mono.empty, newcomers may feel difficult to start writing the controller.

How can I handle a situation where I need to send JSON and multipart/form-data in Spring boot

I would like to know a simple solution for receiving images and simple data in a single post using Spring. I am a beginner in Java so I would like to know the easy way. I've used several backend frameworks and I've encountered this problem in all of them.
I have the following problem:
I was receiving a multipart/form-data like this
public CasaVenda storeCasaVendaOld(#RequestParam("dormitorios") Integer dormitorios, #RequestParam("preco") Double preco, #RequestParam("foto_1") MultipartFile foto_1){
I receive some numbers along with an image. This is a typical first attempt of beginner's implementation.Validate will require code to be writeen in the controller and I have to receive far more parameters than described here, so it's a bad implementation.
I thought about receiving a model
public CasaVenda storeCasaVenda(#Valid #RequestBody CasaVenda casa)
Now I can validate using annotations and so. The problem is with the file. Is there a simple solution to receive the file in one post request or should I split the process of seding the overall data and the files spareted? I mean I can make the process of the resource creation two steps, first it enters the overall data and afterwards it includes the photos.
Its pretty easy to define an object:
public class MyObject {
private Integer dormitorios;
private Double preco;
...
getters/setters/constructors/etc.
...
// I'm not sure whether you can place a MultipartFile here as well to process image,
// however it doesn't make sense to validate it anyway
}
Then you can use this object in the controller, it will map all the query params to the fields of the object automatically by spring:
public CasaVenda storeCasaVendaOld(MyObject myObject) {
}
Now, you can place Validation annotations inside MyObject and it will be validated, just do not use #RequestParam annotation before the object...

Camel routing based on object value

I have two simple Camel routes working for writing to a jms queue and reading from it. I am putting a serialized object to the queue. I am able to deserialize it and covert it to json successfully.
Route for writing:
from("direct:message").to("jms:myqueu")
My route for reading:
from("jms:myqueu")
.marshal()
.json(JsonLibrary.Gson).
.to("file://cc")
Now i want to check a field within the object and route based on that.Also that field should not be part of the final json.
Can i check the value within the object and route based on that( like write to different files?). I can add the annotation in the pojo to avoid the field in final json
I thought of converting object to json, and then sending to queue. Then i can use jsonpath for conditional routing. But then how can i omit a field from final json?
Yes, you can use content based routing to check any field in incoming object and do the routing based on that.
ref: http://camel.apache.org/content-based-router.html
To ignore a field during json marshalling , you can use #JsonIgnore - Jackson annotation.
For reference, i was able to get it working. I added a custom filter class and checked the bean value within that. Also linked it within the choose option for routing.
My route is now:
from("jms:myqueu")
.choice()
.when()
.method(Exp.class,"checkfieldA")
.json(JsonLibrary.Gson).
.to("file://cc")
.when()
.method(Exp.class,"checkfieldB")
.json(JsonLibrary.Gson).
.to("file://dd")
.endChoice()
Here Exp is a normal class and checkfieldA and checkfieldB are two methods returning boolean values.
class Exp{
public boolean checkfieldA(Message message){
myobj obj = (myobj)message.getBody() // this is the object is put to queue. Need to cast to my object type .
if(myobj.isFieldA()){
return true;
}
}
}

Camel Rest DSL response encoding

I am currently working on a REST based java application using the new Camel REST DSL as the foundation.
It mostly works except that I noticed when calling the URLs through a REST client (instead of say a browser) the JSON response is "garbled" and comes through with what I assume is the wrong encoding
MyRouteBuilder.java
#Component
public class MyRouteBuilder extends RouteBuilder{
#Autowired
LocalEnvironmentBean environmentBean;
#Override
public void configure() throws Exception {
restConfiguration().component("jetty").host("0.0.0.0").port(80)
.bindingMode(RestBindingMode.auto);
rest("/testApp")
.get("/data").route()
.to("bean:daoService?method=getData")
.setProperty("viewClass", constant(CustomeJsonViews.class))
.marshal("customDataFormat").endRest()
.get("/allData").route()
.to("bean:daoService?method=getDatas")
.setProperty("viewClass", constant(CustomeJsonViews.class))
.marshal("customDataFormat").endRest();
}
}
CustomeDataFormat.java
public class CustomDataFormat implements DataFormat{
private ObjectMapper jacksonMapper;
public CustomDataFormat(){
jacksonMapper = new ObjectMapper();
}
#Override
public void marshal(Exchange exchange, Object obj, OutputStream stream) throws Exception {
Class view = (Class) exchange.getProperty("viewClass");
if (view != null)
{
ObjectWriter w = jacksonMapper.writerWithView(view);
w.writeValue(stream, obj);
}
else
stream.write(jacksonMapper.writeValueAsBytes(obj));
}
#Override
public Object unmarshal(Exchange exchange, InputStream stream) throws Exception {
return null;
}
}
A full working version can be found here:
https://github.com/zwhitten/camel-rest-test
When going to the URL, {host}/testApp/data, in Chrome for example the response comes through as:
{
data: "Sherlock",
value: "Holmes",
count: 10
}
However using the Postman browser plugin as the client returns:
"W3siZGF0YSI6ImRhdGE6OjAiLCJ2YWx1ZSI6InZhbHVlOjowIiwiY291bnQiOjB9LHsiZGF0YSI6ImRhdGE6OjEiLCJ2YWx1ZSI6InZhbHVlOjoxIiwiY291bnQiOjF9LHsiZGF0YSI6ImRhdGE6OjIiLCJ2YWx1ZSI6InZhbHVlOjoyIiwiY291bnQiOjJ9LHsiZGF0YSI6ImRhdGE6OjMiLCJ2YWx1ZSI6InZhbHVlOjozIiwiY291bnQiOjN9LHsiZGF0YSI6ImRhdGE6OjQiLCJ2YWx1ZSI6InZhbHVlOjo0IiwiY291bnQiOjR9LHsiZGF0YSI6ImRhdGE6OjUiLCJ2YWx1ZSI6InZhbHVlOjo1IiwiY291bnQiOjV9LHsiZGF0YSI6ImRhdGE6OjYiLCJ2YWx1ZSI6InZhbHVlOjo2IiwiY291bnQiOjZ9LHsiZGF0YSI6ImRhdGE6OjciLCJ2YWx1ZSI6InZhbHVlOjo3IiwiY291bnQiOjd9LHsiZGF0YSI6ImRhdGE6OjgiLCJ2YWx1ZSI6InZhbHVlOjo4IiwiY291bnQiOjh9LHsiZGF0YSI6ImRhdGE6OjkiLCJ2YWx1ZSI6InZhbHVlOjo5IiwiY291bnQiOjl9XQ=="
The problem seems to be with the REST bind mode being "auto" and using a custom marshaller.
If I set the binding mode to "json" then both the browser and client responses get garbled.
If I set the binding mode to "json" and bypass the custom marshallers everything works correctly.
Is there a way to configure the route to use a custom marshaller and encode the responses correctly regardless of the client?
I think the solution is to use the default binding option(off) since you are using custom marshallers.
You have two ways to achieve it:
Turn off the RestBindingMode, because otherwise the RestBindingMarshalOnCompletion in RestBindingProcessor will be registered and manually (un)marshal.
Register your own DataFormat and use it within the RestBinding automatically. You configure the REST configuration via jsonDataFormat to set the custom data format.
Map<String, DataFormatDefinition> dataFormats = getContext().getDataFormats();
if (dataFormats == null) {
dataFormats = new HashMap<>();
}
dataFormats.put("yourFormat", new DataFormatDefinition(new CustomDataFormat()));
restConfiguration()....jsonDataFormat("yourFormat")
You can also create your own dataformat like so:
in your restconfiguration it will look sthg like this (see json-custom)
builder.restConfiguration().component("jetty")
.host(host(propertiesResolver))
.port(port(propertiesResolver))
.bindingMode(RestBindingMode.json)
.jsonDataFormat("json-custom")
;
You must create a file "json-custom"
that's the name of the file and that file should contain the class name that implements your own way to marshal and unmarshal...
it must be located in your jar : META-INF\services\org\apache\camel\dataformat
so the content of the file should be:
class=packageofmyclass.MyOwnDataformatter
The response you were receiving is JSON, but it had been encoded to base64. Taking the String from your post, I was able to decode it as:
[{"data":"data::0","value":"value::0","count":0},{"data":"data::1","value":"value::1","count":1},{"data":"data::2","value":"value::2","count":2},{"data":"data::3","value":"value::3","count":3},{"data":"data::4","value":"value::4","count":4},{"data":"data::5","value":"value::5","count":5},{"data":"data::6","value":"value::6","count":6},{"data":"data::7","value":"value::7","count":7},{"data":"data::8","value":"value::8","count":8},{"data":"data::9","value":"value::9","count":9}]
The answers above stop the response body being encoded to base64. The documentation from Apache Camel on bindingMode is illusive as to why it behaves that way when combined with explicit marshalling. Removing the explicit marshalling will return a JSON body, but you may also notice that it contains the any class names in the body. The documentation suggests that bindingMode is more for the transportation of classes and that you specifiy a type(Pojo.class) and optionally outType(Pojo.class) of your requests/responses. See http://camel.apache.org/rest-dsl.html (section Binding to POJOs Using) for more details.
Base64 is the safest way of transferring JSON across networks to ensure it is received exactly as the server sent it, according to some posts I've read. It is then the responsibility of the client to then decode the response.
The answers above do solve the problem. However, I'm not entirely convinced that mixing the data format in the service routes is such as good thing and should ideally be at a higher level of abstraction. This would then allow the data format to be changed in one place, rather than having to change it on every route that produces JSON. Though, I must admit, I've never seen a service that has change data format in its lifetime, so this really is a mute point.
We were also facing the same issue.
Our DataFormat was Json .Once we implented our own custom marshaller. Camel was encoding the data to base64.I tried the approach provided by Cshculz but our CustomDataFormatter was not getting called for some reason which i couldn't figure out.
So We added .marshal(YourDataFormatter) after every Bean call.This provided us with the formatted json but in the encoded form so in the end of the route we added .unmarshal().json(JsonLibrary.Jackson) to return a raw json to the client.
sample snippet :-
.to("xxxxxxx").marshal(YourDataFormatterBeanRef)
.to("xxxxxxx").marshal(YourDataFormatterBeanRef)
.to("xxxxxxx").marshal(YourDataFormatterBeanRef)
.to("xxxxxxx").marshal(YourDataFormatterBeanRef)
.end().unmarshal().json(JsonLibrary.Jackson)

Receive two types of data from a request payload with resteasy / jax-rs

I'm sending json-formatted data to my Java server via http requests. I've had great success in receiving the requests with functions like
Boolean deleteUsers(List<Long> userIds) {
// ...
return true;
}
I'm using RESTEasy on a Java server, and it cheerfully converts the payload of the request into this List<Long> that's so convenient.
Now I want to send a String and a list of numbers! Ideally, my receiving function would look something like
Boolean deleteUsers(String string, List<Long>userIds) {
// ....
return true;
}
Alas, RESTEasy doesn't seem to know what I mean, and chokes on the payload.
How can I receive multiple types of data from a payload?
Depending on your specific use case, you might simply add the first parameter ("string") to the #Path annotation such as
#POST
#Path("{string:.*}")
#Consume(MediaType.APPLICATION_JSON)
#Produce(MediaType.APPLICATION_JSON)
Boolean deleteUsers(#PathParam("string") String string, List<Long>userIds) {
...
}
This would result in the following URL form:
/service/<string>/
with the payload containing your userid list (as json).
As you might realize, #POST method type is suggested since the service construction method won't be idempotent. Otherwise, #DELETE would have been favoured.
It seems that a wrapper object is needed.
class TwoObjectDTO {
String string;
List<Long> listOfNumbers;
}
#POST
Boolean deleteUsers(TwoObjectDTO object) {
...
}

Categories