- Notifications
You must be signed in to change notification settings - Fork0
pgilad/spring-boot-demo
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
A guide to bootstrapping a basic Spring Boot Webflux Application, with Reactive Mongo
- Prerequisites
- Setup
- Hello World
- Adding logs
- Changing the port
- Health checks
- Info APIs
- Reactive Vs. Imperative, or why Reactive?
- Adding Reactive Mongo
- Validation
- Last play - Server Sent Events (SSE)
- Where to go from here
- IntelliJ IDEA 2018.2.3 (Ultimate Edition) or newer
- Gradle 4.10.1 or newer (Use Sdkman to install)
- Java 10.0.2 or newer (Use Sdkman to install)
- Lombok plugin for IntelliJ
githttpie(brew install httpie) (Optional)mongodbfor later
- https://start.spring.io/
- Gradle, Java 10
- Add dependencies:
- Reactive Web
- Actuator
- DevTools
- Lombok
- Download & unzip archive into ~/repos/spring-boot-demo
- Start IntelliJ
- Setup + Add
Annotation Processors -> Enable annotation processing - Run application (hint: currently it does nothing)
- Enable IntelliJ Auto-Import of dependencies (Editor -> General -> Auto Import -> Add unambiguous imports on the fly
- Copy-Paste code directly into package (project explorer) to create component automatically !!
Let's add a simple controller with mapping to respond to anhello request from the client:
Create a new file namedHelloController.java:
@RestControllerpublicclassHelloController {@GetMapping("/hello")publicMono<String>sayHello() {returnMono.just("Hello"); }}
Restart the application, and now let's get our first response back from the server:
$ http localhost:8080/helloHTTP/1.1 200 OKContent-Length: 6Content-Type: text/plain;charset=UTF-8HelloWe have a simple Hello World application. 😄
Annotate any class with@Sl4f and add a log:
log.info("Here");
This usesLombok which does annotation processing which re-writes your source code behind the screens.
This actually adds a line like this to your class:
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);
We will see howLombok will actually aid us in saving a lot of boilerplate code (Which Java is known for).
The easiest way to change the port, is by overridingserver.port inapplication.properties:
server.port=9090
Go ahead and re-run your application, now it runs on9090.
A basic health check is setup due tospring-boot-actuator. Let's give it a test:
$ http localhost:9090/actuator/healthHTTP/1.1 200 OKContent-Length: 15Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8{"status":"UP"}
We can easily extend this health check to include our custom health checks:
Add another filehealth/CustomHealthCheck.java:
@ComponentpublicclassCustomHealthCheckimplementsReactiveHealthIndicator {@OverridepublicMono<Health>health() {returnMono.just(Health.up().withDetail("Service","Good!").build()); }}
In order to see full-details on health API, we need to add the following to ourapplication.properties:
management.endpoint.health.show-details=ALWAYSNow let's try our health API again:
$ http localhost:9090/actuator/healthHTTP/1.1 200 OKContent-Length: 195Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8{"details": {"customHealthCheck": {"details": {"Service":"Good!" },"status":"UP" },"diskSpace": {"details": {"free": 190510637056,"threshold": 10485760,"total": 499963174912 },"status":"UP" } },"status":"UP"}
Let's assume we want to show various information about our app. This is revealed in the followingactuator API:
$ http localhost:9090/actuator/infoHTTP/1.1 200 OKContent-Length: 2Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8{}Currently we are exposing nothing. But let's add some general info. Add the following to yourapplication.properties:
info.coolness.spring=agileIf we run the samehttpie request now:
$ http localhost:9090/actuator/infoHTTP/1.1 200 OKContent-Length: 31Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8{"coolness": {"spring":"agile" }}
That was easy. Now let's add some info with logic:
Add a new file namedinfo/InfoContrib.java:
@ComponentpublicclassInfoContribimplementsInfoContributor {@Overridepublicvoidcontribute(Info.Builderbuilder) {builder.withDetail("contributor","foo"); }}
Now when we run a request:
$ http localhost:9090/actuator/infoHTTP/1.1 200 OKContent-Length: 51Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8{"contributor":"foo","coolness": {"spring":"agile" }}
And off course there are easy ways to auto add application information (such as git, version, etc...)
Enough with all this setup, it's cool, but where's the real stuff?
Let's implement a simple dictionary word count algorithm, twice as imperative (with naive assumptions) and once as reactive.Reactive is meant for observable and async operations, but it's also awesome for regular stream manipulations because of the operators.
First, our basic story, which we'll extract the top N word count frequencies, with words bounded by a space, and lowercase.
We'll implement this in ourHelloController, but you can use any other controller as well.
privatestaticStringstory ="the quick brown fox jumped over the lazy fence and then noticed another quick black fox " +"that was much quicker than the original fox but the original fox was able to jump higher over the fence";
First, our initial naive implementation:
@GetMapping("/word-count/v1")publicFlux<Tuple2<String,Long>>wordCount1(@RequestParam(defaultValue ="2")Integerlimit) {String[]words =story.split(" ");HashMap<String,Long>counts =newHashMap<>();for (Stringword :words) {Longcount =counts.getOrDefault(word,0L) +1;counts.put(word.toLowerCase(),count); }Set<Long>values =newHashSet<>(counts.values());finalArrayList<Long>sortedCounts =newArrayList<>(values);sortedCounts.sort(Collections.reverseOrder());ArrayList<Tuple2<String,Long>>response =newArrayList<>();for (vari =0;i <Math.min(limit,sortedCounts.size());i++) {Longcount =sortedCounts.get(i);for (Map.Entry<String,Long>entry :counts.entrySet()) {if (entry.getValue().equals(count)) {response.add(Tuples.of(entry.getKey(),count)); } } }returnFlux.fromIterable(response);}
Let's verify that it works:http :9090/word-count/v1. You should see a list of tuples of words and their frequency.
Now, our second improved implementation usingTreeMap. The details here don't really matter, as we're not comparing performance, but ease of implementations.
@GetMapping("/word-count/v2")publicFlux<Tuple2<String,Long>>wordCount2(@RequestParam(defaultValue ="2")Integerlimit) {String[]words =story.split(" ");HashMap<String,Long>counts =newHashMap<>();TreeMap<String,Long>sortedCounts =newTreeMap<String,Long>(Comparator.comparing(counts::get,Comparator.reverseOrder()));for (Stringword :words) {counts.merge(word.toLowerCase(),1L,Math::addExact); }sortedCounts.putAll(counts);ArrayList<Tuple2<String,Long>>response =newArrayList<>();for (vari =0;i <limit;i++) {Map.Entry<String,Long>entry =sortedCounts.pollFirstEntry();if (entry ==null) {// no more entries in mapbreak; }response.add(Tuples.of(entry.getKey(),entry.getValue())); }returnFlux.fromIterable(response);}
Now finally, let's see how it's done using Reactive Streams (1 possible solution):
@GetMapping("/word-count/v3")publicFlux<Tuple2<String,Long>>wordCount3(@RequestParam(defaultValue ="2")Integerlimit) {returnFlux.fromArray(story.split(" ")) .map(String::toLowerCase) .collect(Collectors.groupingBy(Function.identity(),Collectors.counting())) .flatMapIterable(Map::entrySet) .sort((a,b) ->b.getValue().compareTo(a.getValue())) .map(a ->Tuples.of(a.getKey(),a.getValue())) .take(limit);}
Sweet right? Now imagine you need to do async, or multi-threading, or timeouts or buffering. Adding that to the reactive impl. is as easy as adding a new line.Adding those to the imperative impl. is hard.
Add the following line to yourbuild.gradle underdependencies:
compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')
And let's add the annotations to support Reactive Mongo:
@EnableReactiveMongoRepositories and@EnableMongoAuditing under main app.
Let's create our domain document,Project:
@Document@DatapublicclassProject {@IdprivateStringid;privateStringname;privateStringdescription;@CreatedDateprivateLocalDateTimecreatedAt;}
@Document is a mongo annotation, while@Data is a magicLombok annotation which auto creates lots of stuff for us(getters, setters, all args constructor and some other magic).
Everything is now in place to wire up our controller with mongo.
First let's create a repository interface, almost everything we need to work with mongo (or reactive mongo in our case):
repository/ProjectMongoRepository.java:
@RepositorypublicinterfaceProjectMongoRepositoryextendsReactiveMongoRepository<Project,String> {}
Let's create our very owncontroller/ProjectController.java:
@RestController@RequestMapping("/api/projects")publicclassProjectsController {privatefinalProjectMongoRepositorymongo;@AutowiredpublicProjectsController(ProjectMongoRepositorymongo) {this.mongo =mongo; }@GetMappingprivateFlux<Project>getProjects() {returnmongo.findAll(); }@PostMappingprivateMono<Project>createProject(@RequestBody@ValidProjectproject) {returnmongo.save(project); }@GetMapping("/{id}")privateMono<ResponseEntity<Project>>getProject(@PathVariableStringid) {returnmongo.findById(id) .map(ResponseEntity::ok) .defaultIfEmpty(ResponseEntity.notFound().build()); }@PutMapping("/{id}")privateMono<ResponseEntity<Project>>updateProject(@PathVariableStringid,@RequestBodyProjectproject) {returnmongo.findById(id) .flatMap(existingProject -> {existingProject.setName(project.getName());existingProject.setDescription(project.getDescription());returnmongo.save(existingProject); }) .map(ResponseEntity::ok) .defaultIfEmpty(ResponseEntity.notFound().build()); }@DeleteMapping("/{id}")privateMono<ResponseEntity<Object>>deleteProject(@PathVariableStringid) {returnmongo.findById(id) .flatMap(project -> {returnmongo .delete(project) .thenReturn(ResponseEntity.noContent().build()); }) .defaultIfEmpty(ResponseEntity.notFound().build()); }}
This is a pretty simple CRUD example using Reactive Mongo. Notice how all of the Mongo operations:
- Are already defined by our Repository interface (and ready to use)
- Return a
Flux | Mono
This means that we can run any stream operation on them because they act as an Publisher.
One thing that comes to mind, is what about validation? How do we ensure that the client sends us the expected data?
Well, we already handle missing entities, but we haven't handled properties validation. (Permissions and security will be on a different guide).
Let's decide thatproject.name cannot be null or an empty string. This makes sense as we don't want to save projects without namesWe do allow emptyproject.description though, but we want to limit it to maximum of 100 characters. We also want to limitproject.name length to 50 characters.
This is easily added usingJSR-303 bean validation, already included in Spring Boot Web(flux).
Let's revisit ourProject model:
@Data@DocumentclassProject {@IdprivateStringid;@NotBlank@Length(min =1,max =30)privateStringname;@Length(max =100)privateStringdescription;@CreatedDate@JsonProperty(access =JsonProperty.Access.READ_ONLY)privateLocalDateTimecreatedAt;}
Notice how we add annotation over the fields. This annotation based validation makes our life very easy. We can alsocreate custom validations (using annotations) but we'll leave that for another lesson.
Notice also how we added@JsonProperty(access = JsonProperty.Access.READ_ONLY) to thecreatedAt field - this ensuresthat this field is only serialized (when being field is being read) but never when writing to class. This prevents the userfrom feeding his owncreatedAt data, interfering our mongo auditing.
One last thing we will need to add is a proper error validation formatting (the default is way too verbose). Add the followingmethod to theProjectsController:
@ExceptionHandler(WebExchangeBindException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)publicMono<List<String>>handleWebExchangeBindException(WebExchangeBindExceptione) {returnFlux .fromIterable(e.getFieldErrors()) .map(field ->String.format("%s.%s %s",e.getObjectName(),field.getField(),field.getDefaultMessage())) .collectList();}
And that's it. Give it a try with:
$ http POST :9090/api/projects/ name='' description='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'HTTP/1.1 400 Bad RequestContent-Length: 136Content-Type: application/json;charset=UTF-8["project.name length must be between 1 and 30","project.description length must be between 0 and 100","project.name must not be blank"]
Yep we got sweet validation.
Let's add another method to our controller, that can stream projects back to the client:
@GetMapping(value ="/stream",produces =MediaType.APPLICATION_STREAM_JSON_VALUE)privateFlux<Project>getProjectsStream() {returnmongo.findAll().delayElements(Duration.ofSeconds(1));}
We simulate a delay in responding to the client. In order to request the data make sure you client is not waiting for the stream to end, for example:
$ http -S :9090/api/projects/stream
And that's how we added SSE support!
We have only just glimpsed the surface of Spring Boot. Off course there are many other modules and extension to it.Several key concepts come to mind:
- Security (
spring-boot-security) - Permissions
- Pagination
- HATEOS
- Testing!!
- Packaging and releasing
- Dockerizing the app
- Monitoring (and metrics)
- Scale
- Configuration, development and production
Much much more...
Keep learning and having fun, and share your success (or frustrations) with Spring Boot!!!
MIT
About
A guide to understanding Spring Boot WebFlux with Reactive Mongo
Topics
Resources
Uh oh!
There was an error while loading.Please reload this page.