From: Gustavo Martin Morcuende Date: Thu, 8 Dec 2016 20:20:57 +0000 (+0100) Subject: Playing with asynchronous requests in Spring X-Git-Url: https://git.gumartinm.name/?a=commitdiff_plain;h=f0d3518d5ab2f5c68fe4a90d130dd4328fd7b845;p=JavaForFun Playing with asynchronous requests in Spring Possibilites: Callable Deferrable + CompletableFuture Deferrable + ListenableFutureAdapter RxJava --- diff --git a/SpringJava/RxJava/pom.xml b/SpringJava/RxJava/pom.xml new file mode 100644 index 0000000..b8e47a1 --- /dev/null +++ b/SpringJava/RxJava/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + de.spring.webservices + web-services-spring-rxjava + 1.0-SNAPSHOT + pom + SPRING RxJava + https://gumartinm.name/ + RxJava with Spring Framework + + Gustavo Martin Morcuende + https://gumartinm.name/ + + + scm:git:https://git.gumartinm.name/JavaForFun/SpringJava/RxJava + https://git.gumartinm.name/JavaForFun/SpringJava/RxJava + + + + web-services-spring-rxjava-bom + web-services-spring-rxjava-global + web-services-spring-rxjava-server + + + diff --git a/SpringJava/RxJava/web-services-spring-rxjava-bom/pom.xml b/SpringJava/RxJava/web-services-spring-rxjava-bom/pom.xml new file mode 100644 index 0000000..0703a71 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-bom/pom.xml @@ -0,0 +1,296 @@ + + 4.0.0 + de.spring.webservices + web-services-spring-rxjava-bom + pom + 1.0-SNAPSHOT + web-services-spring-rxjava-bom + http://gumartinm.name + Web Services Spring Framework, RxJava. BOM + + Gustavo Martin Morcuende + http://www.gumartinm.name + + + scm:git:http://git.gumartinm.name/JavaForFun/SpringJava/RxJava + http://git.gumartinm.name/JavaForFun/SpringJava/RxJava + + + UTF-8 + UTF-8 + 4.2.4.RELEASE + + + + release + + release + + + true + + + + + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.3 + + + + + org.apache.logging.log4j + log4j-core + 2.3 + + + + + org.slf4j + jcl-over-slf4j + 1.7.12 + + + + + javax.inject + javax.inject + 1 + + + + + cglib + cglib + 2.2.2 + + + + + + org.springframework + spring-core + ${spring.version} + + + + commons-logging + commons-logging + + + + + org.springframework + spring-webmvc + ${spring.version} + + + + commons-logging + commons-logging + + + + + org.springframework + spring-oxm + ${spring.version} + + + commons-logging + commons-logging + + + + + org.springframework.data + spring-data-commons + 1.12.5.RELEASE + + + + commons-logging + commons-logging + + + + + + + javax.servlet + javax.servlet-api + 4.0.0-b01 + provided + + + + + com.fasterxml.jackson.core + jackson-databind + 2.6.4 + + + + + + javax.validation + validation-api + 1.1.0.Final + + + org.hibernate + hibernate-validator + 5.2.2.Final + + + + + junit + junit + 4.12 + test + + + org.springframework + spring-test + ${spring.version} + test + + + org.mockito + mockito-core + 2.0.11-beta + test + + + + + org.hamcrest + hamcrest-core + 1.3 + test + + + org.hamcrest + hamcrest-library + 1.3 + test + + + com.jayway.jsonpath + json-path + 2.1.0 + test + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.18.1 + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.18.1 + + + org.apache.maven.plugins + maven-war-plugin + 2.6 + + + + true + src/main/webapp + + WEB-INF/web.xml + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.8 + 1.8 + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-resources-plugin + 2.7 + + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + ${project.description} + ${project.version} + ${project.organization.name} + ${project.description} + ${project.version} + ${project.organization.name} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*IntegrationTest.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + **/*IntegrationTest.java + + + + + + diff --git a/SpringJava/RxJava/web-services-spring-rxjava-global/pom.xml b/SpringJava/RxJava/web-services-spring-rxjava-global/pom.xml new file mode 100644 index 0000000..ce9a818 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-global/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + + web-services-spring-rxjava-bom + de.spring.webservices + 1.0-SNAPSHOT + + web-services-spring-rxjava-global + web-services-spring-rxjava-global + http://gumartinm.name + diff --git a/SpringJava/RxJava/web-services-spring-rxjava-global/src/main/java/de/spring/webservices/domain/Car.java b/SpringJava/RxJava/web-services-spring-rxjava-global/src/main/java/de/spring/webservices/domain/Car.java new file mode 100644 index 0000000..cdfaa07 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-global/src/main/java/de/spring/webservices/domain/Car.java @@ -0,0 +1,58 @@ +package de.spring.webservices.domain; + +public class Car { + + private final Long id; + private final String content; + + // Required by Jackson :/ + public Car() { + this.id = null; + this.content = null; + } + + public Car(Long id, String content) { + this.id = id; + this.content = content; + } + + + public Long getId() { + return id; + } + + public String getContent() { + return content; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((content == null) ? 0 : content.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Car other = (Car) obj; + if (content == null) { + if (other.content != null) + return false; + } else if (!content.equals(other.content)) + return false; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } +} diff --git a/SpringJava/RxJava/web-services-spring-rxjava-server/pom.xml b/SpringJava/RxJava/web-services-spring-rxjava-server/pom.xml new file mode 100644 index 0000000..eb967ca --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-server/pom.xml @@ -0,0 +1,128 @@ + + 4.0.0 + + web-services-spring-rxjava-bom + de.spring.webservices + 1.0-SNAPSHOT + + web-services-spring-rxjava-server + war + web-services-spring-rxjava-server + http://gumartinm.name + + + de.spring.webservices + web-services-spring-rxjava-global + 1.0-SNAPSHOT + + + + io.reactivex + rxjava + 1.2.3 + + + + org.springframework + spring-core + + + org.springframework + spring-webmvc + + + org.springframework.data + spring-data-commons + + + + + javax.servlet + javax.servlet-api + provided + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + org.springframework + spring-oxm + 4.2.4.RELEASE + + + + + + javax.validation + validation-api + + + org.hibernate + hibernate-validator + + + + + junit + junit + test + + + org.springframework + spring-test + test + + + org.mockito + mockito-core + test + + + + + org.hamcrest + hamcrest-core + test + + + org.hamcrest + hamcrest-library + test + + + com.jayway.jsonpath + json-path + test + + + + ${project.artifactId} + + + ${basedir}/src/main/webapp + + **/*.* + + + + ${basedir}/src/main/resources/ + + **/*.* + + + + + + + org.apache.maven.plugins + maven-war-plugin + + + + diff --git a/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/business/service/AwesomeBusinessLogic.java b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/business/service/AwesomeBusinessLogic.java new file mode 100644 index 0000000..261d4d0 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/business/service/AwesomeBusinessLogic.java @@ -0,0 +1,17 @@ +package de.spring.webservices.rest.business.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import de.spring.webservices.domain.Car; + + +public interface AwesomeBusinessLogic { + + public Page findAll(Pageable pageRequest); + + public Car findById(long id); + + public Car create(Car resource); + +} diff --git a/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/business/service/impl/AwesomeBusinessLogicImpl.java b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/business/service/impl/AwesomeBusinessLogicImpl.java new file mode 100644 index 0000000..248caf5 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/business/service/impl/AwesomeBusinessLogicImpl.java @@ -0,0 +1,62 @@ +package de.spring.webservices.rest.business.service.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import de.spring.webservices.domain.Car; +import de.spring.webservices.rest.business.service.AwesomeBusinessLogic; + +@Service("awesomeBusinessLogic") +public class AwesomeBusinessLogicImpl implements AwesomeBusinessLogic { + private static final String TEMPLATE = "Car: %s"; + + private final AtomicLong counter = new AtomicLong(); + + @Override + public Page findAll(Pageable pageRequest) { + final List cars = new ArrayList<>(); + cars.add(new Car(counter.incrementAndGet(), String.format(TEMPLATE, 1))); + cars.add(new Car(counter.incrementAndGet(), String.format(TEMPLATE, 2))); + cars.add(new Car(counter.incrementAndGet(), String.format(TEMPLATE, 3))); + + try { + Thread.sleep(300000); + } catch(InterruptedException ex) { + Thread.currentThread().interrupt(); + } + + return new PageImpl<>(cars); + } + + @Override + public Car findById(long id) { + + try { + Thread.sleep(300000); + } catch(InterruptedException ex) { + Thread.currentThread().interrupt(); + } + + + return new Car(counter.incrementAndGet(), String.format(TEMPLATE, id)); + } + + @Override + public Car create(Car resource) { + long count = counter.incrementAndGet(); + + try { + Thread.sleep(300000); + } catch(InterruptedException ex) { + Thread.currentThread().interrupt(); + } + + return new Car(count, String.format(TEMPLATE, count)); + } +} diff --git a/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/controller/CallableCarController.java b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/controller/CallableCarController.java new file mode 100644 index 0000000..3092943 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/controller/CallableCarController.java @@ -0,0 +1,156 @@ +package de.spring.webservices.rest.controller; + +import java.util.Map; +import java.util.concurrent.Callable; + +import javax.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.WebAsyncTask; + +import de.spring.webservices.domain.Car; +import de.spring.webservices.rest.business.service.AwesomeBusinessLogic; + +@RestController +@RequestMapping("/api/callable/cars/") +public class CallableCarController { + private static final Logger LOGGER = LoggerFactory.getLogger(DeferrableCarController.class); + private static final int PAGE = 2; + private static final int PAGE_SIZE = 10; + + // With no value, we depend on the Tomcat/Jboss/Jetty/etc timeout value for asynchronous requests. + // Spring will answer after 60 secs with an empty response (by default) and HTTP 503 status (by default) when timeout. + private static final long ASYNC_TIMEOUT = 60000; /* milliseconds */ + + + /** + * + * WHEN EXCEPTION FROM CALLABLE, Spring WILL TRIGGER THE Spring Exception Handler AS YOU KNOW IT. + * See: https://spring.io/blog/2012/05/10/spring-mvc-3-2-preview-making-a-controller-method-asynchronous/ + * + * When an Exception is raised by a Callable, it is handled through the + * HandlerExceptionResolver mechanism just like exceptions raised by any + * other controller method. The more detailed explanation is that the + * exception is caught and saved, and the request is dispatched to the + * Servlet container where processing resumes and the + * HandlerExceptionResolver chain invoked. This also means + * that @ExceptionHandler methods will be invoked as usual. + * + * + * SO, YOU COULD HOOK UP THE HANDLER AND RETURN YOUR CUSTOM MESSAGESS (as + * usual) + * + */ + + + private final AwesomeBusinessLogic awesomeBusinessLogic; + + @Inject + public CallableCarController(AwesomeBusinessLogic awesomeBusinessLogic) { + this.awesomeBusinessLogic = awesomeBusinessLogic; + } + + @RequestMapping(produces = { MediaType.APPLICATION_JSON_UTF8_VALUE }, method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public Callable> cars() { + + // Relying on the timeout given by Tomcat/Jboss/Jetty/etc. WebAsyncTask if you want to control your timeout (see below) + +// return new Callable>() { +// @Override +// public Page call() throws Exception { +// return awesomeBusinessLogic.findAll(new PageRequest(PAGE, PAGE_SIZE)); +// } +// }; + + // lambda way :) + return () -> awesomeBusinessLogic.findAll(new PageRequest(PAGE, PAGE_SIZE)); + + } + + @RequestMapping(value = "{id}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public WebAsyncTask car(@RequestHeader(value = "MY_HEADER", required = false) String specialHeader, + @PathVariable("id") long id, + @RequestParam Map params, + @RequestParam(value = "wheel", required = false) String[] wheelParams) { + + if (specialHeader != null) { + LOGGER.info("SPECIAL HEADER: " + specialHeader); + } + + if (params.get("mirror") != null) { + LOGGER.info("MIRROR: " + params.get("mirror")); + } + + if (params.get("window") != null) { + LOGGER.info("WINDOW: " + params.get("window")); + } + + if (wheelParams != null) { + for(String wheel : wheelParams) { + LOGGER.info(wheel); + } + } + + + // If you want to control stuff like timeout you must use WebAsyncTask + Callable :) + +// Callable callable = new Callable() { +// @Override +// public Car call() throws Exception { +// return awesomeBusinessLogic.findById(id); +// } +// }; +// return new WebAsyncTask<>(ASYNC_TIMEOUT, callable); + + // lambda way :) + return new WebAsyncTask<>(ASYNC_TIMEOUT, () -> awesomeBusinessLogic.findById(id)); + } + + @RequestMapping(consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public WebAsyncTask> create(@RequestBody Car car) { + + // If you want to control stuff like timeout you must use WebAsyncTask + Callable :) + +// Callable> callable = new Callable>() { +// @Override +// public ResponseEntity call() throws Exception { +// Car createdCar = awesomeBusinessLogic.create(car); +// +// HttpHeaders headers = new HttpHeaders(); +// headers.add(HttpHeaders.LOCATION, "/api/cars/" + createdCar.getId()); +// return new ResponseEntity<>(createdCar, headers, HttpStatus.CREATED); +// } +// }; + + // lambda way + Callable> callable = () -> { + Car createdCar = awesomeBusinessLogic.create(car); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.LOCATION, "/api/cars/" + createdCar.getId()); + return new ResponseEntity<>(createdCar, headers, HttpStatus.CREATED); + }; + + return new WebAsyncTask<>(ASYNC_TIMEOUT, callable); + } + +} diff --git a/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/controller/CarController.java b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/controller/CarController.java new file mode 100644 index 0000000..2e22f83 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/controller/CarController.java @@ -0,0 +1,86 @@ +package de.spring.webservices.rest.controller; + +import java.util.Map; + +import javax.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import de.spring.webservices.domain.Car; +import de.spring.webservices.rest.business.service.AwesomeBusinessLogic; + +@RestController +@RequestMapping("/api/cars/") +public class CarController { + private static final Logger LOGGER = LoggerFactory.getLogger(DeferrableCarController.class); + private static final int PAGE = 2; + private static final int PAGE_SIZE = 10; + + private final AwesomeBusinessLogic awesomeBusinessLogic; + + @Inject + public CarController(AwesomeBusinessLogic awesomeBusinessLogic) { + this.awesomeBusinessLogic = awesomeBusinessLogic; + } + + @RequestMapping(produces = { MediaType.APPLICATION_JSON_UTF8_VALUE }, method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public Page cars() { + return awesomeBusinessLogic.findAll(new PageRequest(PAGE, PAGE_SIZE)); + } + + @RequestMapping(value = "{id}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public Car car(@RequestHeader(value = "MY_HEADER", required = false) String specialHeader, + @PathVariable("id") long id, + @RequestParam Map params, + @RequestParam(value = "wheel", required = false) String[] wheelParams) { + + if (specialHeader != null) { + LOGGER.info("SPECIAL HEADER: " + specialHeader); + } + + if (params.get("mirror") != null) { + LOGGER.info("MIRROR: " + params.get("mirror")); + } + + if (params.get("window") != null) { + LOGGER.info("WINDOW: " + params.get("window")); + } + + if (wheelParams != null) { + for(String wheel : wheelParams) { + LOGGER.info(wheel); + } + } + + return awesomeBusinessLogic.findById(id); + } + + @RequestMapping(consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity create(@RequestBody Car car) { + Car createdCar = awesomeBusinessLogic.create(car); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.LOCATION, "/api/cars/" + createdCar.getId()); + return new ResponseEntity<>(createdCar, headers, HttpStatus.CREATED); + } + +} diff --git a/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/controller/DeferrableCarController.java b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/controller/DeferrableCarController.java new file mode 100644 index 0000000..6788427 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/controller/DeferrableCarController.java @@ -0,0 +1,150 @@ +package de.spring.webservices.rest.controller; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import javax.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import de.spring.webservices.domain.Car; +import de.spring.webservices.rest.business.service.AwesomeBusinessLogic; + +@RestController +@RequestMapping("/api/deferrable/cars/") +public class DeferrableCarController { + private static final Logger LOGGER = LoggerFactory.getLogger(DeferrableCarController.class); + private static final int PAGE = 2; + private static final int PAGE_SIZE = 10; + + // With no value, we depend on the Tomcat/Jboss/Jetty/etc timeout value for asynchronous requests. + // Spring will answer after 60 secs with an empty response (by default) and HTTP 503 status (by default) when timeout. + private static final long ASYNC_TIMEOUT = 60000; /* milliseconds */ + + /** + * + * WHEN EXCEPTION IN setErrorResult, Spring WILL TRIGGER THE Spring Exception Handler AS YOU KNOW IT (I HOPE) + * SO, YOU COULD HOOK UP THE HANDLER AND RETURN YOUR CUSTOM MESSAGESS (as usual) + * + */ + + private final AwesomeBusinessLogic awesomeBusinessLogic; + + @Inject + public DeferrableCarController(AwesomeBusinessLogic awesomeBusinessLogic) { + this.awesomeBusinessLogic = awesomeBusinessLogic; + } + + @RequestMapping(produces = { MediaType.APPLICATION_JSON_UTF8_VALUE }, method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public DeferredResult> cars() { + + // THIS CODE (I GUESS) SHOULD BE LOCATED IN Service layer. Anyhow this is just an example. + DeferredResult> deferredResult = new DeferredResult<>(ASYNC_TIMEOUT); + CompletableFuture + .supplyAsync(() -> awesomeBusinessLogic.findAll(new PageRequest(PAGE, PAGE_SIZE))) + .thenAcceptAsync(car -> deferredResult.setResult(car)) + .exceptionally(exception -> { + LOGGER.error("findAll error: ", exception); + + // DO NOT FORGET THE EXCEPTIONS. + // It will trigger the Spring Exception Handler as you know it :) + deferredResult.setErrorResult(exception); + + return null; + }); + + return deferredResult; + } + + @RequestMapping(value = "{id}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public DeferredResult car(@RequestHeader(value = "MY_HEADER", required = false) String specialHeader, + @PathVariable("id") long id, + @RequestParam Map params, + @RequestParam(value = "wheel", required = false) String[] wheelParams) { + + if (specialHeader != null) { + LOGGER.info("SPECIAL HEADER: " + specialHeader); + } + + if (params.get("mirror") != null) { + LOGGER.info("MIRROR: " + params.get("mirror")); + } + + if (params.get("window") != null) { + LOGGER.info("WINDOW: " + params.get("window")); + } + + if (wheelParams != null) { + for(String wheel : wheelParams) { + LOGGER.info(wheel); + } + } + + // THIS CODE (I GUESS) SHOULD BE LOCATED IN Service layer. Anyhow this is just an example. + DeferredResult deferredResult = new DeferredResult<>(ASYNC_TIMEOUT); + CompletableFuture + .supplyAsync(() -> awesomeBusinessLogic.findById(id)) + .thenAcceptAsync(car -> deferredResult.setResult(car)) + .exceptionally(exception -> { + + LOGGER.error("findById error: ", exception); + + // DO NOT FORGET THE EXCEPTIONS. + // It will trigger the Spring Exception Handler as you know it :) + deferredResult.setErrorResult(exception); + + return null; + }); + + return deferredResult; + } + + @RequestMapping(consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public DeferredResult> create(@RequestBody Car car) { + + // THIS CODE (I GUESS) SHOULD BE LOCATED IN Service layer. Anyhow this is just an example. + DeferredResult> deferredResult = new DeferredResult<>(ASYNC_TIMEOUT); + CompletableFuture + .supplyAsync(() -> { + Car createdCar = awesomeBusinessLogic.create(car); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.LOCATION, "/api/cars/" + createdCar.getId()); + return new ResponseEntity<>(createdCar, headers, HttpStatus.CREATED); + }) + .thenAcceptAsync(response -> deferredResult.setResult(response)) + .exceptionally(exception -> { + + LOGGER.error("create error: ", exception); + + // DO NOT FORGET THE EXCEPTIONS. + // It will trigger the Spring Exception Handler as you know it :) + deferredResult.setErrorResult(exception); + + return null; + }); + + return deferredResult; + } + +} diff --git a/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/controller/RxJavaCarController.java b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/controller/RxJavaCarController.java new file mode 100644 index 0000000..a6d3b08 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/java/de/spring/webservices/rest/controller/RxJavaCarController.java @@ -0,0 +1,150 @@ +package de.spring.webservices.rest.controller; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import javax.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import de.spring.webservices.domain.Car; +import de.spring.webservices.rest.business.service.AwesomeBusinessLogic; + +@RestController +@RequestMapping("/api/rxjava/cars/") +public class RxJavaCarController { + private static final Logger LOGGER = LoggerFactory.getLogger(RxJavaCarController.class); + private static final int PAGE = 2; + private static final int PAGE_SIZE = 10; + + // With no value, we depend on the Tomcat/Jboss/Jetty/etc timeout value for asynchronous requests. + // Spring will answer after 60 secs with an empty response (by default) and HTTP 503 status (by default) when timeout. + private static final long ASYNC_TIMEOUT = 60000; /* milliseconds */ + + /** + * + * WHEN EXCEPTION IN setErrorResult, Spring WILL TRIGGER THE Spring Exception Handler AS YOU KNOW IT (I HOPE) + * SO, YOU COULD HOOK UP THE HANDLER AND RETURN YOUR CUSTOM MESSAGESS (as usual) + * + */ + + private final AwesomeBusinessLogic awesomeBusinessLogic; + + @Inject + public RxJavaCarController(AwesomeBusinessLogic awesomeBusinessLogic) { + this.awesomeBusinessLogic = awesomeBusinessLogic; + } + + @RequestMapping(produces = { MediaType.APPLICATION_JSON_UTF8_VALUE }, method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public DeferredResult> cars() { + + // THIS CODE (I GUESS) SHOULD BE LOCATED IN Service layer. Anyhow this is just an example. + DeferredResult> deferredResult = new DeferredResult<>(ASYNC_TIMEOUT); + CompletableFuture + .supplyAsync(() -> awesomeBusinessLogic.findAll(new PageRequest(PAGE, PAGE_SIZE))) + .thenAcceptAsync(car -> deferredResult.setResult(car)) + .exceptionally(exception -> { + LOGGER.error("findAll error: ", exception); + + // DO NOT FORGET THE EXCEPTIONS. + // It will trigger the Spring Exception Handler as you know it :) + deferredResult.setErrorResult(exception); + + return null; + }); + + return deferredResult; + } + + @RequestMapping(value = "{id}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public DeferredResult car(@RequestHeader(value = "MY_HEADER", required = false) String specialHeader, + @PathVariable("id") long id, + @RequestParam Map params, + @RequestParam(value = "wheel", required = false) String[] wheelParams) { + + if (specialHeader != null) { + LOGGER.info("SPECIAL HEADER: " + specialHeader); + } + + if (params.get("mirror") != null) { + LOGGER.info("MIRROR: " + params.get("mirror")); + } + + if (params.get("window") != null) { + LOGGER.info("WINDOW: " + params.get("window")); + } + + if (wheelParams != null) { + for(String wheel : wheelParams) { + LOGGER.info(wheel); + } + } + + // THIS CODE (I GUESS) SHOULD BE LOCATED IN Service layer. Anyhow this is just an example. + DeferredResult deferredResult = new DeferredResult<>(ASYNC_TIMEOUT); + CompletableFuture + .supplyAsync(() -> awesomeBusinessLogic.findById(id)) + .thenAcceptAsync(car -> deferredResult.setResult(car)) + .exceptionally(exception -> { + + LOGGER.error("findById error: ", exception); + + // DO NOT FORGET THE EXCEPTIONS. + // It will trigger the Spring Exception Handler as you know it :) + deferredResult.setErrorResult(exception); + + return null; + }); + + return deferredResult; + } + + @RequestMapping(consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public DeferredResult> create(@RequestBody Car car) { + + // THIS CODE (I GUESS) SHOULD BE LOCATED IN Service layer. Anyhow this is just an example. + DeferredResult> deferredResult = new DeferredResult<>(ASYNC_TIMEOUT); + CompletableFuture + .supplyAsync(() -> { + Car createdCar = awesomeBusinessLogic.create(car); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.LOCATION, "/api/cars/" + createdCar.getId()); + return new ResponseEntity<>(createdCar, headers, HttpStatus.CREATED); + }) + .thenAcceptAsync(response -> deferredResult.setResult(response)) + .exceptionally(exception -> { + + LOGGER.error("create error: ", exception); + + // DO NOT FORGET THE EXCEPTIONS. + // It will trigger the Spring Exception Handler as you know it :) + deferredResult.setErrorResult(exception); + + return null; + }); + + return deferredResult; + } + +} diff --git a/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/resources/log4j2.xml b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/resources/log4j2.xml new file mode 100644 index 0000000..ee36b97 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/resources/log4j2.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/resources/spring-configuration/mvc/rest/rest-config.xml b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/resources/spring-configuration/mvc/rest/rest-config.xml new file mode 100644 index 0000000..54b8540 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/resources/spring-configuration/mvc/rest/rest-config.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/resources/spring-configuration/spring-config.xml b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/resources/spring-configuration/spring-config.xml new file mode 100644 index 0000000..e50129b --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/resources/spring-configuration/spring-config.xml @@ -0,0 +1,13 @@ + + + + diff --git a/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/webapp/WEB-INF/web.xml b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..6c9c1d0 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-server/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,41 @@ + + + + Spring REST Services: example + + + + org.springframework.web.context.ContextLoaderListener + + + + + spring.profiles.active + ${environment.profile} + contextConfigLocation + + classpath*:spring-configuration/*.xml + + + + + + spring-rest + org.springframework.web.servlet.DispatcherServlet + 1 + true + + contextConfigLocation + classpath*:spring-configuration/mvc/rest/*.xml + + + + + spring-rest + + /* + + + diff --git a/SpringJava/RxJava/web-services-spring-rxjava-server/src/test/java/de/spring/webservices/rest/controller/CarControllerIntegrationTest.java b/SpringJava/RxJava/web-services-spring-rxjava-server/src/test/java/de/spring/webservices/rest/controller/CarControllerIntegrationTest.java new file mode 100644 index 0000000..06ce410 --- /dev/null +++ b/SpringJava/RxJava/web-services-spring-rxjava-server/src/test/java/de/spring/webservices/rest/controller/CarControllerIntegrationTest.java @@ -0,0 +1,108 @@ +package de.spring.webservices.rest.controller; + +import static org.hamcrest.Matchers.any; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.spring.webservices.domain.Car; +import de.spring.webservices.rest.business.service.AwesomeBusinessLogic; + + +@Ignore +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration({ "classpath*:spring-configuration/mvc/rest/*.xml"}) +public class CarControllerIntegrationTest { + private static final int PAGE = 2; + private static final int PAGE_SIZE = 10; + private static final String TEMPLATE = "Car: %s"; + + private AwesomeBusinessLogic awesomeBusinessLogic; + private CarController controller; + private MockMvc mockMvc; + + @Before + public void setup() { + awesomeBusinessLogic = mock(AwesomeBusinessLogic.class); + controller = new CarController(awesomeBusinessLogic); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + public void testWhenGetAllCarsThenRetrieveJsonValues() throws Exception { + final List cars = new ArrayList<>(); + cars.add(new Car(1L, String.format(TEMPLATE, 1))); + cars.add(new Car(2L, String.format(TEMPLATE, 2))); + cars.add(new Car(3L, String.format(TEMPLATE, 3))); + + given(awesomeBusinessLogic.findAll(new PageRequest(PAGE, PAGE_SIZE))).willReturn(new PageImpl<>(cars)); + + mockMvc.perform(get("/api/cars/") + .accept(MediaType.APPLICATION_JSON_UTF8)) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", any(Integer.class))) + .andExpect(jsonPath("$[0].content", is("Car: 1"))) + .andExpect(jsonPath("$[1].content", is("Car: 2"))) + .andExpect(jsonPath("$[1].id", any(Integer.class))) + .andExpect(jsonPath("$[2].content", is("Car: 3"))) + .andExpect(jsonPath("$[2].id", any(Integer.class))) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)); + } + + @Test + public void testWhenGetOneCarThenRetrieveJsonValue() throws Exception { + mockMvc.perform(get("/api/cars/{id}", 1L) + .accept(MediaType.APPLICATION_JSON_UTF8)) + + .andExpect(status().isOk()) + .andExpect(jsonPath("id", any(Integer.class))) + .andExpect(jsonPath("content", is("Car: 1"))) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)); + } + + @Test + public void testWhenCreateNewCarThenRetrieveJsonValue() throws Exception { + Car car = new Car(2L, "nothing"); + mockMvc.perform(post("/api/cars/") + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(asJsonString(car)) + .accept(MediaType.APPLICATION_JSON_UTF8)) + + .andExpect(status().isCreated()) + .andExpect(jsonPath("id", any(Integer.class))) + .andExpect(jsonPath("content", is("Car: 1"))) + .andExpect(header().string(HttpHeaders.LOCATION, "/api/cars/1")) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)); + } + + private static String asJsonString(final Object obj) throws JsonProcessingException { + final ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(obj); + } +}