Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

A guide to understanding Spring Boot WebFlux with Reactive Mongo

NotificationsYou must be signed in to change notification settings

pgilad/spring-boot-demo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A guide to bootstrapping a basic Spring Boot Webflux Application, with Reactive Mongo

Prerequisites

  • 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
  • git
  • httpie (brew install httpie) (Optional)
  • mongodb for later

Setup

  • 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 + AddAnnotation Processors -> Enable annotation processing
  • Run application (hint: currently it does nothing)

Hot Tips

  • 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 !!

Hello World

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-8Hello

We have a simple Hello World application. 😄

Adding logs

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).

Changing the port

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.

Health checks

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=ALWAYS

Now 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"}

Info APIs

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=agile

If 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?

Reactive Vs. Imperative, or why Reactive?

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.

Adding Reactive Mongo

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:

  1. Are already defined by our Repository interface (and ready to use)
  2. Return aFlux | 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).

Validation

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.

Last play - Server Sent Events (SSE)

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!

Where to go from here

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:

  1. Security (spring-boot-security)
  2. Permissions
  3. Pagination
  4. HATEOS
  5. Testing!!
  6. Packaging and releasing
  7. Dockerizing the app
  8. Monitoring (and metrics)
  9. Scale
  10. Configuration, development and production

Much much more...

Keep learning and having fun, and share your success (or frustrations) with Spring Boot!!!

License

MIT

About

A guide to understanding Spring Boot WebFlux with Reactive Mongo

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages


[8]ページ先頭

©2009-2025 Movatter.jp