I have a method which queries a remote service. This service returns a single payload which holds many items.
How do I get those items out using a Flux and a flatMapMany?
At the moment my "fetch from service" method looks like:
public Flux<Stack> listAll() {
return this.webClient
.get()
.uri("/projects")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMapMany(response -> response.bodyToFlux(Stack.class));
}
a Stack is just a POJO which looks like:
public class Stack {
String id;
String name;
String title;
String created;
}
Nothing special here, but I think my deserializer is wrong:
protected Stack deserializeObject(JsonParser jsonParser, DeserializationContext deserializationContext, ObjectCodec objectCodec, JsonNode jsonNode) throws IOException {
log.info("JsonNode {}", jsonNode);
return Stack.builder()
.id(nullSafeValue(jsonNode.findValue("id"), String.class))
.name(nullSafeValue(jsonNode.findValue("name"), String.class))
.title(nullSafeValue(jsonNode.findValue("title"), String.class))
.created(nullSafeValue(jsonNode.findValue("created"), String.class))
.build();
}
What I've noticed happening is the first object is serialized correctly, but then it seems to get serialized again, rather than the next object in the payload.
The payload coming in follows standard JSON API spec and looks like:
{
"data":[
{
"type":"stacks",
"id":"1",
"attributes":{
"name":"name_1",
"title":"title_1",
"created":"2017-03-31 12:27:59",
"created_unix":1490916479
}
},
{
"type":"stacks",
"id":"2",
"attributes":{
"name":"name_2",
"title":"title_2",
"created":"2017-03-31 12:28:00",
"created_unix":1490916480
}
},
{
"type":"stacks",
"id":"3",
"attributes":{
"name":"name_3",
"title":"title_3",
"created":"2017-03-31 12:28:01",
"created_unix":1490916481
}
}
]
}
I've based this pattern on the spring-reactive-university
Any help as to where I've gone wrong would be awesome;
Cheers!
I think I solved it, still using a Flux.
public Flux<Stack> listAllStacks() {
return this.webClient
.get()
.uri("/naut/projects")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.toEntity(String.class))
.flatMapMany(this::transformPayloadToStack);
}
Converts the incoming payload to a String where I can then parse it using a jsonapi library
private Flux<Stack> transformPayloadToStack(ResponseEntity<String> payload) {
ObjectMapper objectMapper = new ObjectMapper();
ResourceConverter resourceConverter = new ResourceConverter(objectMapper, Stack.class);
List<Stack> stackList = resourceConverter.readDocumentCollection(payload.getBody().getBytes(), Stack.class).get();
return Flux.fromIterable(stackList);
}
Which returns a Flux. Thanks to the library, I don't need to create a bunch of domains either, I can still work with my simple Stack POJO
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonIgnoreProperties(ignoreUnknown = true)
#Type("stacks")
public class Stack {
#com.github.jasminb.jsonapi.annotations.Id
String id;
String name;
String title;
String created;
}
And this in turn is called from the controller
#GetMapping("/stacks")
#ResponseBody
public Flux<Stack> findAll() {
return this.stackService.listAllStacks();
}
I've not tested if this is blocking or not yet, but seems to work okay.
You json doesn't exactly match with your model class i.e. Stack. Together with Stack create another class like this
public class Data {
List<Stack> data;
// Getters and Setters....
}
Now in your webclient you can do like this
Mono<Data> listMono = webClient
.get()
.uri("/product/projects")
.exchange()
.flatMap(clientResponse -> clientResponse.bodyToMono(Data.class));
Now if you do listMono.block() you will get Data object which will have all Stack objects.
Related
i'm trying to send a POST request with body data as described here: https://scrapyrt.readthedocs.io/en/stable/api.html#post.
Here's what i've tried to do but it gives me HTTP code 500
String uri = "http://localhost:3000";
WebClient webClient = WebClient.builder()
.baseUrl(uri)
.build();
LinkedMultiValueMap map = new LinkedMultiValueMap();
String q = "\"url\": \"https://blog.trendmicro.com/trendlabs-security-intelligence\",\"meta\":{\"latestDate\" : \"18-05-2020\"}}";
map.add("request", q);
map.add("spider_name", "blog");
BodyInserter<MultiValueMap<String, Object>, ClientHttpRequest> inserter2
= BodyInserters.fromMultipartData(map);
Mono<ItemsList> result = webClient.post()
.uri(uriBuilder -> uriBuilder
.path("/crawl.json")
.build())
.body(inserter2)
.retrieve()
.bodyToMono(ItemsList.class);
ItemsList tempItems = result.block();
Here's what i've tried to do but it gives me HTTP code 500
Most likely because you're sending the wrong data in a mixture of wrong formats with the wrong type:
You're using multipart form data, not JSON
You're then setting the request parameter as a JSON string (q)
The JSON string you're using in q isn't even valid (it's at least missing an opening curly brace) - and handwriting JSON is almost universally a bad idea, leverage a framework to do it for you instead.
Instead, the normal thing to do would be to create a POJO structure that maps to your request, so:
public class CrawlRequest {
private CrawlInnerRequest request;
#JsonProperty("spider_name")
private String spiderName;
//....add the getters / setters
}
public class CrawlInnerRequest {
private String url;
private String callback;
#JsonProperty("dont_filter")
private String dontFilter;
//....add the getters / setters
}
...then simply create a CrawlRequest, set the values as you wish, then in your post call use:
.body(BodyInserters.fromValue(crawlRequest))
This is a rather fundamental, basic part of using a WebClient. I'd suggest reading around more widely to give yourself a better understanding of the fundamentals, it will help tremendously in the long run.
For me following code worked:
public String wcPost(){
Map<String, String> bodyMap = new HashMap();
bodyMap.put("key1","value1");
WebClient client = WebClient.builder()
.baseUrl("domainURL")
.build();
String responseSpec = client.post()
.uri("URI")
.headers(h -> h.setBearerAuth("token if any"))
.body(BodyInserters.fromValue(bodyMap))
.exchange()
.flatMap(clientResponse -> {
if (clientResponse.statusCode().is5xxServerError()) {
clientResponse.body((clientHttpResponse, context) -> {
return clientHttpResponse.getBody();
});
return clientResponse.bodyToMono(String.class);
}
else
return clientResponse.bodyToMono(String.class);
})
.block();
return responseSpec;
}
I am using Springs' Webclient to make a HTTP GET call.
How can I validate the response object GetPersonBasicInfoResWrapper's property that I received as a response of my HTTP call.
I am trying to validate the birthDate inside the flatMap by blocking the response object, but it doesn't look like the most functional way of doing it.
Following is the excerpt from my code.
private Mono<GetPersonBasicInfoResWrapper> getPersonBasicInfo(Double personId, LocalDate birthDate,
CallerRequestMetaData callerInfo) {
return middlewareWebClient
.get()
.uri(...)
...
...
.exchange()
.flatMap(client -> {
GetPersonBasicInfoResWrapper block = client.bodyToMono(GetPersonBasicInfoResWrapper.class).block();
LocalDate personBirthDate = LocalDateTime.ofInstant(block.getBirthDate().toInstant(),ZoneId.of(Constants.DEFAULT_TIME_ZOME)).toLocalDate();
if (!personBirthDate.equals(birthDate))
throw new YakeenRowadException(Errors.INCORRECT_ID_BIRTH_DATE_G, birthDate.toString());
else
return client.bodyToMono(GetPersonBasicInfoResWrapper.class);
});
}
Any help is highly appreciated.
Try something like this
private Mono<PersonInfo> getPersonInfo(Double personId) {
return webClient.get()
.uri(...)
.exchange()
.flatMap(response -> {
return response.bodyToMono(PersonInfo.class);
});
}
private LocalDate toLocalDate(Instant instant) {
return LocalDateTime.ofInstant(instant, ZoneId.of(Constants.DEFAULT_TIME_ZOME))
.toLocalDate();
}
public Mono<PersonInfo> doSomething(Double personId, LocalDate birthDate) {
return getPersonInfo(personId)
.flatMap(personInfo -> {
final LocalDate birthDate = toLocalDate(personInfo.getBirthDate().toInstant());
if (!personBirthDate.equals(birthDate)) {
return Mono.error(new YakeenRowadException(Errors.INCORRECT_ID_BIRTH_DATE_G, birthDate.toString()));
}
return Mono.just(personInfo);
});
}
Don't validate during the fetch, validation is business logic, and should be in the layer above.
you fetch and return
you validate.
If validation fails, you return a Mono.error() to the calling client.
I have no idea what "MetaData" was supposed to be. I hope it's not the url, because passing it that way is wrong.
(try to avoid verbose naming)
I'm trying to replace a resttemplate implementation with a webclient one. The tricky stuff here is that I need to modify a property from an input object, when the response resolves. I don't find the way to achieve it...
This is the resttemplate code:
public Instance login(final Instance instancia, final LoginDTO dto) {
String url = instancia.getBalancer() + API_AUTHENTICATE_PATH;
HttpEntity<LoginDTO> request = generateRequest(dto);
ResponseEntity<JWTToken> token = restTemplate.postForEntity(url, request, JWTToken.class);
instancia.setToken(token.getBody().getIdToken());
return instancia;
}
And this is what I have until now:
#Override
public Mono<Instance> login(Instance instancia, LoginDTO dto) {
Mono<JWTToken> monoToken=webClient.post().uri(url).body((BodyInserters.fromObject(dto))).retrieve()
.bodyToMono(JWTToken.class);
return {....};
}
I'm stucked in that part, because I don't find the way to alter the Instance object...
And there is another point: This is injected in another class, because I need to run this request in parallel against multiple targets. So, a block call is not enough.
Does someone have an idea about how to do it?
Thanks a lot in advance!
It can be achieved easily as following:
#Override
public Mono<Instance> login(Instance instancia, LoginDTO dto) {
return webClient
.post()
.uri(url)
.body((BodyInserters.fromObject(dto)))
.retrieve()
.bodyToMono(JWTToken.class)
.map(token -> {
instancia.setToken(token.getBody().getIdToken());
return instancia;
});
}
I need to fetch the entire request body in filter and convert it into String. Below is my code but nothing is getting printed on console.
#Component
public class WebFilter01 implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange,
WebFilterChain webFilterChain) {
Flux<DataBuffer> requestBody = serverWebExchange.getRequest().getBody();
Flux<String> decodedRequest = requestBody.map(databuffer -> {
return decodeDataBuffer(databuffer);
});
decodedRequest.doOnNext(s -> System.out.print(s));
return webFilterChain.filter(serverWebExchange);
}
protected String decodeDataBuffer(DataBuffer dataBuffer) {
Charset charset = StandardCharsets.UTF_8;
CharBuffer charBuffer = charset.decode(dataBuffer.asByteBuffer());
DataBufferUtils.release(dataBuffer);
String value = charBuffer.toString();
return value;
}
}
Nothing is getting printed on console because you did not subscribe to decodedRequest ,
as we know one of the Reactive aspect:
Nothing happens until you subscribe
But if you do that you will see printed body on console but your code will not work, because the next operators cannot read the body and you will get IllegalStateException(Only one connection receive subscriber allowed.)
So, how to resolve it?
Create your own wrapper for ServerWebExchange (please read about this here: How to log request and response bodies in Spring WebFlux)
Log bodies in HttpMessageDecoder. If you see, for instance, AbstractJackson2Decoder you will found code where Spring decode you buffer to object and can log it:
try {
Object value = reader.readValue(tokenBuffer.asParser(getObjectMapper()));
if (!Hints.isLoggingSuppressed(hints)) {
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(value, !traceOn);
return Hints.getLogPrefix(hints) + "Decoded [" + formatted + "]";
});
}
return value;
}
I have an API built with Spring Boot. By default the default JSON structure when an error is thrown by Spring is;
{
"timestamp": 1477425179601,
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/categoriess"
}
This structure is different to error responses returning myself in the API, so I'd like to change Spring to use the same structure as my own for consistency.
My error response are structured like this;
{
"errors": [
{
"code": 999404,
"message": "The resource you were looking for could not be found"
}
]
}
How would I go about doing this? I've tried using an Exception Handler, but I can't figure out the correct exception to set it up for. I'd like to also make sure that the Http status is still correctly returned as 404, or whatever the error is (500 etc).
I had another look at this and did manage to put something together that works for me.
#Bean
public ErrorAttributes errorAttributes() {
return new DefaultErrorAttributes() {
#Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace);
Map<String, Object> error = new HashMap<>();
error.put("code", errorAttributes.get("status"));
error.put("message", errorAttributes.get("error"));
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("errors", error);
return errorResponse;
}
};
}
This returns the following JSON response along with whatever header/http status code spring was going to return.
{
"errors": {
"code": 404,
"message": "Not Found"
}
}
This seems to work great for errors generated by spring, while my own Exceptions I'm handling in Controllers or in a specific ControllerAdmin class with ExceptionHandlers.
A possible way to do something like this is to use the #ExceptionHandler annotation to create a handler method inside your controller.
#RestController
#RequestMapping(produces = APPLICATION_JSON_VALUE)
public class MyController {
#RequestMapping(value = "/find", method = GET)
public Object find() {
throw new UnsupportedOperationException("Not implemented yet!");
}
#ExceptionHandler
public ErrorListModel handleException(Exception exception) {
ExceptionModel exceptionModel = new ExceptionModel(1337, exception.getMessage());
ErrorListModel list = new ErrorListModel();
list.add(exceptionModel);
return list;
}
private class ErrorListModel {
private List<ExceptionModel> errors = new ArrayList<>();
public void add(ExceptionModel exception) {
errors.add(exception);
}
public List<ExceptionModel> getErrors() {
return errors;
}
}
private class ExceptionModel {
private int code;
private String message;
public ExceptionModel(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
}
The private classes ErrorListModel and ExceptionModel just help defining how the resulting JSON body should look, and I assume you already have your own, similar classes.
The find method just throws an exception for us to handle, which gets intercepted by the handleException method because it's annotated with #ExceptionHandler. In here, we create an ExceptionModel, populate it with information from the original exception, and add it to an ErrorListModel, which we then return.
This blog post from 2013 explains the features better than I ever could, and it also mentions an additional option, #ControllerAdvice. It basically allows you to re-use the exception handling in other controllers as well.