
In 2016 I wrote a series of articles in which I developed a REST service which used Jersey’s asynchronous response processing and RxJava in a Spring Boot application. Since then I have used Spring WebFlux, which is the reactive, non-blocking and event-driven counterpart of Spring MVC. I have also had the opportunity to at least scratch on the surface of reactive programming with Reactor.
With these experiences and some time to spare, I thought it would be interesting to update the old REST example to use Spring WebFlux and Reactor in order to learn about the differences.
Dependencies
First of all I updated the dependency versions, removed dependencies that were no longer needed and added new dependencies.
- Updated Spring Boot Starter parent to version 2.2.1.RELEASE.
- Updated Java version used in the project to version 11.
- Updated Gatling version to 3.3.1.
- Updated Gatling Maven plug-in version to 3.0.5.
- Updated TestNG to version 7.0.0.
- Updated RestAssured to version 4.1.2.
- Removed the spring-boot-starter-jersey dependency.
- Added
spring-boot-starter-actuator dependency.
Not strictly necessary for this article. - Removed the rxjava dependency.
- Added the spring-boot-starter-webflux dependency.
Removed Classes
The following classes were removed since they had been replaced or made obsolete:
- JerseyConfig
No longer needed since Jersey is replaced with Spring WebFlux. Spring WebFlux does not need any configuration classes but relies on auto-discovery. - RestResourceBaseRxJava
Replaced with RestResourceBaseReactor since RxJava is replaced by Reactor. - AbstractServiceBaseRxJava
Replaced with AbstractServiceBaseReactor since RxJava is replaced by Reactor.
Added Classes
There is a total of two new classes, none of which are completely new but I nevertheless chose to add new classes since the names of previous classes reflected used technology (RxJava), which the new ones also do (Recator).
AbstractServiceBaseReactor
The first added class is AbstractServiceBaseReactor and it looks like this:
package se.ivankrizsan.restexample.services; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import se.ivankrizsan.restexample.domain.LongIdEntity; import se.ivankrizsan.restexample.repositories.customisation.JpaRepositoryCustomisations; import javax.persistence.EntityNotFoundException; import java.util.List; import java.util.Optional; /** * Abstract base class for services that has operations for creating, reading, * updating and deleting entities. * This implementation uses Reactor. * * @param <E> Entity type. * @author Ivan Krizsan */ @Transactional public abstract class AbstractServiceBaseReactor<E extends LongIdEntity> { /* Constant(s): */ /* Instance variable(s): */ protected JpaRepositoryCustomisations<E> mRepository; /** * Creates a service instance that will use the supplied repository for entity persistence. * * @param inRepository Entity repository. */ public AbstractServiceBaseReactor(final JpaRepositoryCustomisations<E> inRepository) { mRepository = inRepository; } /** * Saves the supplied entity. * * @param inEntity Entity to save. * @return Mono that will receive the saved entity, or exception if error occurs. */ public Mono<E> save(final E inEntity) { return Mono.create(theMonoSink -> { try { final E theSavedEntity = mRepository.save(inEntity); theMonoSink.success(theSavedEntity); } catch (final Throwable theException) { theMonoSink.error(theException); } }); } /** * Updates the supplied entity. * * @param inEntity Entity to update. * @return Mono that will receive the updated entity, or exception if error occurs. */ public Mono<E> update(final E inEntity) { return Mono.create(theMonoSink -> { try { final E theSavedEntity = mRepository.persist(inEntity); theMonoSink.success(theSavedEntity); } catch (final Throwable theException) { theMonoSink.error(theException); } }); } /** * Finds the entity having supplied id. * * @param inEntityId Id of entity to retrieve. * @return Mono that will receive the found entity or error if error occurs or no * entity with supplied id is found. */ @Transactional(readOnly = true) public Mono<E> find(final Long inEntityId) { return Mono.create(theMonoSink -> { try { final Optional<E> theFoundEntity = mRepository.findById(inEntityId); if (theFoundEntity.isPresent()) { theMonoSink.success(theFoundEntity.get()); } else { theMonoSink.error( new EntityNotFoundException("Entity with id " + inEntityId + " not found")); } } catch (final Throwable theException) { theMonoSink.error(theException); } }); } /** * Finds all the entities. * * @return Flux that will receive the found entities or error. */ @Transactional(readOnly = true) public Flux<E> findAll() { return Flux.create(theFluxSink -> { try { final List<E> theAllEntities = mRepository.findAll(); for (final E theEntity : theAllEntities) { theFluxSink.next(theEntity); } theFluxSink.complete(); } catch (final Throwable theException) { theFluxSink.error(theException); } }); } /** * Deletes the entity having supplied id. * * @param inEntityId Id of entity to delete. * @return Mono that will receive completion or error. */ public Mono<Void> delete(final Long inEntityId) { return Mono.create(theMonoSink -> { try { mRepository.deleteById(inEntityId); theMonoSink.success(); } catch (final Throwable theException) { theMonoSink.error(theException); } }); } /** * Deletes all entities. * * @return Mono that will receive completion or error. */ public Mono<Void> deleteAll() { return Mono.create(theMonoSink -> { try { mRepository.deleteAll(); theMonoSink.success(); } catch (final Throwable theException) { theMonoSink.error(theException); } }); } }
Note that:
- There are only
minor differences between the RxJava version and the Reactor
version.
Class and method names differ slightly but migration was trivial.
RestResourceBaseReactor
The second new, at least to the name, class is RestResourceBaseReactor which is implemented like this:
package se.ivankrizsan.restexample.restadapter; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import se.ivankrizsan.restexample.domain.LongIdEntity; import se.ivankrizsan.restexample.services.AbstractServiceBaseReactor; import javax.validation.constraints.NotNull; import java.util.List; /** * Abstract base class for REST resources exposing operations on an entity type. * All operations will return HTTP status 500 with a plain text body containing an * error message if an error occurred during request processing. * * @param <E> Entity type. * @author Ivan Krizsan */ @RestController public abstract class RestResourceBaseReactor<E extends LongIdEntity> { /* Constant(s): */ /* Instance variable(s): */ protected AbstractServiceBaseReactor<E> mService; /** * Retrieves all entities. */ @GetMapping(produces = "application/json;charset=UTF-8") public Flux<E> getAll() { return mService.findAll(); } /** * Deletes the entity with supplied id. * * @param inEntityId Id of entity to delete. * @return Void mono. */ @DeleteMapping(path = "{id}", produces = "application/json;charset=UTF-8") public Mono<Void> deleteEntityById( @PathVariable("id") @NotNull final Long inEntityId) { return mService.delete(inEntityId); } /** * Deletes all entities. * Will return HTTP status 500 if error occurred during request processing. * * @return Void mono. */ @DeleteMapping(produces = "application/json;charset=UTF-8") public Mono<Void> deleteAllEntities() { return mService.deleteAll(); } /** * Retrieves entity with supplied id. * * @param inEntityId Id of entity to retrieve. * @return Mono containing entity, or empty mono if no matching entity. */ @GetMapping(path = "{id}", produces = "application/json;charset=UTF-8") public Mono<E> getEntityById(@PathVariable("id") final Long inEntityId) { return mService.find(inEntityId); } /** * Updates the entity with supplied id by overwriting it with the supplied entity. * * @param inEntity Entity data to write. * @param inEntityId Id of entity to update. * @return Mono containing updated entity. */ @PutMapping( path = "{id}", produces = "application/json;charset=UTF-8", consumes = "application/json") public Mono<E> updateEntity(@RequestBody final E inEntity, @PathVariable("id") @NotNull final Long inEntityId) { inEntity.setId(inEntityId); return mService.update(inEntity); } /** * Creates a new entity using the supplied entity data. * * @param inEntity Entity data to use when creating new entity. */ @PostMapping( produces = "application/json;charset=UTF-8", consumes = "application/json") public Mono<E> createEntity(@RequestBody final E inEntity) { if (inEntity.getId() != null) { throw new IllegalArgumentException("A new entity must not have an id"); } return mService.save(inEntity); } /** * Creates an array containing the entities in the supplied list. * * @param inEntityList List of entities. * @return Array containing the entities from the list. */ protected abstract E[] entityListToArray(List<E> inEntityList); public AbstractServiceBaseReactor<E> getService() { return mService; } public void setService(final AbstractServiceBaseReactor<E> inService) { mService = inService; } }
Note that:
- The above
Reactor version requires significantly less code compared to the
RxJava version.
The reason for this is that Spring WebFlux allows for returning a Flux or Mono from the handler methods while Jersey’s asynchronous response processing require transferring data from the RxJava’s Observable to the Jersey AsynchResponse. - The class is
annotated with @RestController.
This is the annotation used by both Spring MVC and Spring WebFlux to annotate REST controllers. - The handler
methods returns either a Flux or a Mono.
In the Jersey version, the handler methods took a parameter of the type AsynchResponse which was used to populate the responses. With Spring WebFlux, the resulting Flux or Mono from the service can be returned as-is. - The annotations
on the handler methods differ.
The annotations used here are the same as used in Spring MVC.
Further Modifications
In addition to the above mentioned removed and added classes, the following changes were also made:
- Changing the
parent class of all REST controllers.
From RestResourceBaseRxJava, which has been deleted, to RestResourceBaseReactor. - Changing the
parent class of all services.
From AbstractServiceBaseRxJava, which was deleted, to AbstractServiceBaseReactor. - Swapping the annotations in all REST controllers to the Spring WebFlux annotations.
Apart from the necessary modifications above, I also added a few tests.
Running the Load Test
With the above changes in place, I wanted to run the load test to see how the Spring WebFlux-and-Reactor version compares to the Jersey-and-RxJava version. I decided to re-run the load test on the Jersey-and-RxJava version and then on the Spring WebFlux-and-Reactor version.
- Start the example program.
Right-click the RestExampleApplication class in your IDE and run it. - Run the Gatling load test in a terminal window:
mvn gatling:test
Running the load test on the RxJava version yields the following result, which is the text-report from Gatling:
================================================================================ ---- Global Information -------------------------------------------------------- > request count 2691 (OK=2691 KO=0 ) > min response time 59 (OK=59 KO=- ) > max response time 21617 (OK=21617 KO=- ) > mean response time 3329 (OK=3329 KO=- ) > std deviation 3182 (OK=3182 KO=- ) > response time 50th percentile 2001 (OK=2001 KO=- ) > response time 75th percentile 4532 (OK=4532 KO=- ) > response time 95th percentile 9703 (OK=9703 KO=- ) > response time 99th percentile 14768 (OK=14768 KO=- ) > mean requests/sec 76.886 (OK=76.886 KO=- ) ---- Response Time Distribution ------------------------------------------------ > t < 800 ms 315 ( 12%) > 800 ms < t < 1200 ms 473 ( 18%) > t > 1200 ms 1903 ( 71%) > failed 0 ( 0%) ================================================================================
Running the load test on the Spring WebFlux and Reactor version yields the following result:
================================================================================ ---- Global Information -------------------------------------------------------- > request count 2675 (OK=2675 KO=0 ) > min response time 53 (OK=53 KO=- ) > max response time 22868 (OK=22868 KO=- ) > mean response time 3302 (OK=3302 KO=- ) > std deviation 3127 (OK=3127 KO=- ) > response time 50th percentile 2059 (OK=2059 KO=- ) > response time 75th percentile 4738 (OK=4738 KO=- ) > response time 95th percentile 9852 (OK=9852 KO=- ) > response time 99th percentile 13615 (OK=13615 KO=- ) > mean requests/sec 76.429 (OK=76.429 KO=- ) ---- Response Time Distribution ------------------------------------------------ > t < 800 ms 417 ( 16%) > 800 ms < t < 1200 ms 382 ( 14%) > t > 1200 ms 1876 ( 70%) > failed 0 ( 0%) ================================================================================
The results are very similar. Given that the load tests is very simple and short, no discernible differences in performance were detected between the two versions.
Final Words
For me the choice is simple: I will always chose code that is simpler to read and easier to understand. Spring WebFlux and Reactor offers this combination. Admittedly the difference is not very large in this trivial example project.
Another important factor for me is that Spring WebFlux were developed using Reactor and so I have to assume that they fit very well together. Not saying that Jersey and RxJava does not fit well together, they do, but they were not made for each other, so to speak.
The entire project is available in this GitHub repository, in the master branch.
Happy coding!