Can I use Spring WebFlux to implement REST services which get data through Kafka request/response topics? - spring

I'm developing REST service which, in turn, will query slow legacy system so response time will be measured in seconds. We also expect massive load so I was thinking about asynchronous/non-blocking approaches to avoid hundreds of "servlet" threads blocked on calls to slow system.
As I see this can be implemented using AsyncContext which is present in new servlet API specs. I even developed small prototype and it seems to be working.
On the other hand it looks like I can achieve the same using Spring WebFlux.
Unfortunately I did not find any example where custom "backend" calls are wrapped with Mono/Flux. Most of the examples just reuse already-prepared reactive connectors, like ReactiveCassandraOperations.java, etc.
My data flow is the following:
JS client --> Spring RestController --> send request to Kafka topic --> read response from Kafka reply topic --> return data to client
Can I wrap Kafka steps into Mono/Flux and how to do this?
How my RestController method should look like?
Here is my simple implementation which achieves the same using Servlet 3.1 API
//took the idea from some Jetty examples
public class AsyncRestServlet extends HttpServlet {
...
#Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String result = (String) req.getAttribute(RESULTS_ATTR);
if (result == null) { //data not ready yet: schedule async processing
final AsyncContext async = req.startAsync();
//generate some unique request ID
String uid = "req-" + String.valueOf(req.hashCode());
//share it to Kafka receive together with AsyncContext
//when Kafka receiver will get the response it will put it in Servlet request attribute and call async.dispatch()
//This doGet() method will be called again and it will send the response to client
receiver.rememberKey(uid, async);
//send request to Kafka
sender.send(uid, param);
//data is not ready yet so we are releasing Servlet thread
return;
}
//return result as html response
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println(result);
out.close();
}

Here's a short example - Not the WebFlux client you probably had in mind, but at least it would enable you to utilize Flux and Mono for asynchronous processing, which I interpreted to be the point of your question. The web objects should work without additional configurations, but of course you will need to configure Kafka as the KafkaTemplate object will not work on its own.
#Bean // Using org.springframework.web.reactive.function.server.RouterFunction<ServerResponse>
public RouterFunction<ServerResponse> sendMessageToTopic(KafkaController kafkaController){
return RouterFunctions.route(RequestPredicates.POST("/endpoint"), kafkaController::sendMessage);
}
#Component
public class ResponseHandler {
public getServerResponse() {
return ServerResponse.ok().body(Mono.just(Status.SUCCESS), String.class);
}
}
#Component
public class KafkaController {
public Mono<ServerResponse> auditInvalidTransaction(ServerRequest request) {
return request.bodyToMono(TopicMsgMap.class)
// your HTTP call may not return immediately without this
.subscribeOn(Schedulers.single()) // for a single worker thread
.flatMap(topicMsgMap -> {
MyKafkaPublisher.sendMessages(topicMsgMap);
}.flatMap(responseHandler::getServerResponse);
}
}
#Data // model class just to easily convert the ServerRequest (from json, for ex.)
// + ~#constructors
public class TopicMsgMap() {
private Map<String, String> topicMsgMap;
}
#Service // Using org.springframework.kafka.core.KafkaTemplate<String, String>
public class MyKafkaPublisher {
#Autowired
private KafkaTemplate<String, String> template;
#Value("${topic1}")
private String topic1;
#Value("${topic2}")
private String topic2;
public void sendMessages(Map<String, String> topicMsgMap){
topicMsgMap.forEach((top, msg) -> {
if (topic.equals("topic1") kafkaTemplate.send(topic1, message);
if (topic.equals("topic2") kafkaTemplate.send(topic2, message);
});
}
}
Guessing this isn't the use-case you had in mind, but hope you find this general structure useful.

There is several approaches including KafkaReplyingRestTemplate for this problem but continuing your approach in servlet api's the solution will be something like this in spring Webflux.
Your Controller method looks like this:
#RequestMapping(path = "/completable-future", method = RequestMethod.POST)
Mono<Response> asyncTransaction(#RequestBody RequestDto requestDto, #RequestHeader Map<String, String> requestHeaders) {
String internalTransactionId = UUID.randomUUID().toString();
kafkaSender.send(Request.builder()
.transactionId(requestHeaders.get("transactionId"))
.internalTransactionId(internalTransactionId)
.sourceIban(requestDto.getSourceIban())
.destIban(requestDto.getDestIban())
.build());
CompletableFuture<Response> completableFuture = new CompletableFuture();
taskHolder.pushTask(completableFuture, internalTransactionId);
return Mono.fromFuture(completableFuture);
}
Your taskHolder component will be something like this:
#Component
public class TaskHolder {
private Map<String, CompletableFuture> taskHolder = new ConcurrentHashMap();
public void pushTask(CompletableFuture<Response> task, String transactionId) {
this.taskHolder.put(transactionId, task);
}
public Optional<CompletableFuture> remove(String transactionId) {
return Optional.ofNullable(this.taskHolder.remove(transactionId));
}
}
And finally your Kafka ResponseListener looks like this:
#Component
public class ResponseListener {
#Autowired
TaskHolder taskHolder;
#KafkaListener(topics = "reactive-response-topic", groupId = "test")
public void listen(Response response) {
taskHolder.remove(response.getInternalTransactionId()).orElse(
new CompletableFuture()).complete(response);
}
}
In this example I used internalTransactionId as CorrelationId but you can use "kafka_correlationId" that is a known kafka header.

Related

How to mock third party API calls?

I have a spring boot application that has the below AuthFilter added for all rest apis exposed by the application. I want to test the below code that validates authorization token by calling a third party api call. I tried Mockito but how do I inject the mocked HttpPost, HttpClient etc object in the filter class?
Also what value do I pass to thirdPartyAPIUrl property which is configured in application.properties for test class
#Component
public class AuthTokenFilter implements Filter {
public boolean isAuthTokenValid(HttpServletRequest request, HttpServletResponse response) throws IOException {
String authorizationToken = request.getHeader(RequestHeaders.AUTHORIZATION.toString());
TokenRequest validateTokenRequest = new TokenRequest();
validateTokenRequest.setToken(authorizationToken);
try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost(this.thirdPartyAPIUrl); //fetched through application.properties
httpPost.setHeader("Content-type", "application/json");
StringEntity requestBody = new StringEntity(new Gson().toJson(validateTokenRequest));
httpPost.setEntity(requestBody);
try (CloseableHttpResponse validateTokenResponse = httpclient.execute(httpPost)) {
HttpEntity rEntity = validateTokenResponse.getEntity();
TokenResponse tokenResponse = new ObjectMapper().readValue(rEntity.getContent(),
TokenResponse.class);
logger.debug("API Response Object : {}", tokenResponse);
}
}
return false; //temporary
}
}
Thanks!
I would recommend avoiding mocking HttpPost etc and instead just mocking the third-party server. My preferred tool to use for this is wiremock
Here is an example of how it would be used:
(make sure to import this for options, caused me a lot of headaches ;) )
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
... code
static WireMockServer wireMockServer = new WireMockServer(options().port(8080));
#BeforeAll
static void init() {
wireMockServer.start();
}
//this is for the case that you have multiple test suites that mock the server, to avoid conflicts with ports
#AfterAll
static void releaseResource() {
wireMockServer.stop();
}
#Test
void test() {
wireMockServer.stubFor(post("/endpoint").willReturn(aResponse().withStatus(200)));
... more code
filter.isAuthTokenValid(request, response);
}

Return response messages in spring boot

I am working with spring boot with a h2 database. I would like to return a 201 message when the register is inserted succesfully and a 400 when is duplicated. I am using ResponseEntity to achieve this, fot example , the next is my create method from the Service:
#Override
public ResponseEntity<Object> createEvent(EventDTO eventDTO) {
if (eventRepository.findOne(eventDTO.getId()) != null) {
//THis is a test, I am looking for the correct message
return new ResponseEntity(HttpStatus.IM_USED);
}
Actor actor = actorService.createActor(eventDTO.getActor());
Repo repo = repoService.createRepo(eventDTO.getRepo());
Event event = new Event(eventDTO.getId(), eventDTO.getType(), actor, repo, createdAt(eventDTO));
eventRepository.save(event);
return new ResponseEntity(HttpStatus.CREATED);
}
This is my controller:
#PostMapping(value = "/events")
public ResponseEntity addEvent(#RequestBody EventDTO body) {
return eventService.createEvent(body);
}
But I'm not getting any message in the browser, I am doing different tests with postman and when I consult for all the events, the result is correct, but each time that I make a post I dont get any message in the browser, I am not pretty sure what is the cause of this issue. Any ideas?
The ideal way to send Response to the client is to create DTO/DAO with ResponseEntity in Controller
Controller.java
#PostMapping("/test")
public ResponseEntity<Object> testApi(#RequestBody User user)
{
System.out.println("User: "+user.toString());
return assetService.testApi(user);
}
Service.java
public ResponseEntity testApi(User user) {
if(user.getId()==1)
return new ResponseEntity("Created",HttpStatus.CREATED);
else
return new ResponseEntity("Used",HttpStatus.IM_USED);
// for BAD_REQUEST(400) return new ResponseEntity("Bad Request",HttpStatus.BAD_REQUEST);
}
Tested using Postman
Status 201 Created
Status 226 IM Used
Okay, I really don't feel good that service sending the ResponseEntity but not Controller.You could use #ResponseStatus and ExceptionHandler classes for these cases, like below.
Create a class in exception package
GlobalExceptionHandler.java
#ControllerAdvice
public class GlobalExceptionHandler {
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(DataIntegrityViolationException.class) // NOTE : You could create a custom exception class to handle duplications
public void handleConflict() {
}
}
Controller.java
#PostMapping(value = "/events")
#ResponseStatus(HttpStatus.CREATED) // You don't have to return any object this will take care of the status
public void addEvent(#RequestBody EventDTO body) {
eventService.createEvent(body);
}
Now changing the service would look like,
Service.java
#Override
public void createEvent(EventDTO eventDTO) { // No need to return
if (eventRepository.findOne(eventDTO.getId()) != null) {
throw new DataIntegrityViolationException("Already exists"); // you have to throw the same exception which you have marked in Handler class
}
Actor actor = actorService.createActor(eventDTO.getActor());
Repo repo = repoService.createRepo(eventDTO.getRepo());
Event event = new Event(eventDTO.getId(), eventDTO.getType(), actor, repo, createdAt(eventDTO));
eventRepository.save(event);
}

Spring Boot Callback after client receives resource?

I'm creating an endpoint using Spring Boot which executes a combination of system commands (java.lang.Runtime API) to generate a zip file to return to the client upon request, here's the code.
#GetMapping(value = "generateZipFile")
public ResponseEntity<Resource> generateZipFile(#RequestParam("id") Integer id) throws IOException {
org.springframework.core.io.Resource resource = null;
//generate zip file using commandline
resource = service.generateTmpResource(id);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, "application/zip")
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"randomFile.zip\"")
.body(resource);
//somehow delete generated file here after client receives it
}
I cannot keep stacking up the files on the server for obvious disk limit reasons, so I'm looking for a way to delete the files as soon as the client downloads them. Is there a solution in Spring Boot for this? I basically need to hook a callback that would do the cleanup after the user receives the resource.
I'm using Spring Boot 2.0.6
You can create a new thread but a best solution would be create a ThreadPoolExecutor in order to manage threads or also Scheduled annotation helps us.
new Thread() {
#Override
public void run() {
service.cleanup(id);
}
}.start();
UPDATED
A best answer, it would be using a Stack combine with Thread.
Here is the solution that I've done.
https://github.com/jjohxx/example-thread
I ended up using a HandlerInterceptorAdapter, afterCompletion was the callback I needed. The only challenge I had to deal with was passing through the id of the resource to cleanup, which I handled by adding a header in my controller method:
#GetMapping(value = "generateZipFile")
public ResponseEntity<Resource> genereateZipFile(#RequestParam("id") Integer id,
RedirectAttributes redirectAttributes) throws IOException {
org.springframework.core.io.Resource resource = myService.generateTmpResource(id);;
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, "application/zip")
.header(MyInterceptor.TMP_RESOURCE_ID_HEADER, id.toString())
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"someFile.zip\"")
.body(resource);
}
The interceptor code:
#Component
public class MyInterceptor extends HandlerInterceptorAdapter {
public static final String TMP_RESOURCE_ID_HEADER = "Tmp-ID";
#Autowired
private MyService myService;
#Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
if(response == null || !response.containsHeader(TMP_RESOURCE_ID_HEADER)) return;
String tmpFileId = response.getHeader(TMP_RESOURCE_ID_HEADER);
myService.cleanup(tmpFileId);
}
}
For more information about interceptors see here.

Spring cloud stream messaging system(RabbitMQ) implementation using Rest Controller(Restful API)

From past few days i'm trying to implement the Spring cloud stream messaging system using RestController, but it is not happening through the current implementation.
For this sample code i'm going to add RestController
#EnableBinding(Source.class)
#EnableConfigurationProperties(TimeSourceOptionsMetadata.class)
public class TimeSource {
#Autowired
private TimeSourceOptionsMetadata options;
#InboundChannelAdapter(value = Source.OUTPUT)
public String timerMessageSource() {
return new SimpleDateFormat(this.options.getFormat()).format(new Date());
}
}
But the #InboundChannelAdapter cannot accept any parameters from RequestMapping Get Method URL.At the end what i need is to add message to the broker using Restful API Get method from api call. which is the best way to do it?, I couldn't figure out any best process from internet.
spring cloud team already provided a source application that listens for HTTP requests and emits the body as a message payload. If the Content-Type matches text/* or application/json, the payload will be a String, otherwise the payload will be a byte array.
github link
You can go with this or if you want to write it yourself, you can do it like below:
#RestController
#EnableBinding(Source.class)
public class RestSource {
#Autowired
private Source channels;
#RequestMapping(path = "/", method = POST, consumes = {"application/json" })
#ResponseStatus(HttpStatus.ACCEPTED)
public void handleRequest(#RequestBody String body, #RequestHeader(HttpHeaders.CONTENT_TYPE) Object contentType) {
sendMessage(body, contentType);
}
private void sendMessage(Object body, Object contentType) {
channels.output().send(MessageBuilder.createMessage(body,
new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, contentType))));
}
}

Spring MVC + DeferredResult add Hateoas stuff

For the rest interface the Spring MVC + RxJava + DeferredResult returned from controllers is used.
I am thinking about adding Hateoas support to the endpoints. The natural choice would be the Spring Hateoas. The problem is that Spring Hateoas would not work in the asynchronous/multi-threading environment since it uses ThreadLocal.
Is there any way to workaround that constraint? I do not think so but maybe someone has any suggestions.
Has anyone used other APIs to add Hateoas support to the rest endpoints?
Thank you.
So the solution I've used is to closure in the request attributes and then apply them as part of a lift operator
public class RequestContextStashOperator<T> implements Observable.Operator<T, T> {
private final RequestAttributes attributes;
/**
* Spring hateoas requires the request context to be set but observables may happen on other threads
* This operator will reapply the context of the constructing thread on the execution thread of the subscriber
*/
public RequestContextStashOperator() {
attributes = RequestContextHolder.currentRequestAttributes();
}
#Override
public Subscriber<? super T> call(Subscriber<? super T> subscriber) {
return new Subscriber<T>() {
#Override
public void onCompleted() {
subscriber.onCompleted();
}
#Override
public void onError(Throwable e) {
subscriber.onError(e);
}
#Override
public void onNext(T t) {
RequestContextHolder.setRequestAttributes(attributes);
subscriber.onNext(t);
}
};
}
}
which you can then use on an observable like
lift(new RequestContextStashOperator<>())
as long as the object is created in the same thread as the request. You can then use a map after in the observable chain to map your object up to being a resource and add your hateoas links in.
So answer is a bit late, but probably someone will find it useful.
You are right about ThreadLocal - if you generate hateoas links in different thread, then it fails with exception. I found some kind of workaround for this:
#RequestMapping(path = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
DeferredResult<ResponseEntity<ProductResource>> example(#PathVariable("id") final String productId, final HttpServletRequest request) {
final DeferredResult<ResponseEntity<ProductResource>> deferredResult = new DeferredResult<>();
request.setAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE, request.getContextPath());
final RequestAttributes requestAttributes = new ServletRequestAttributes(request);
productByIdQuery.byId(UUID.fromString(productId)).subscribe(productEntity -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
deferredResult.setResult(result, HttpStatus.OK))
}, deferredResult::setErrorResult);
return deferredResult;
}
So as you see, I save RequestAttributes so I can set them later in the callback. This solves just part of the problem - you'll get another exception because you'll loose contextPath attribute. To avoid this save it explicitly:
request.setAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE, request.getContextPath());
After those changes everything seems to work, but looks messy of course. I hope that somebody can provide more elegant solution.

Resources