Get familiar with Spring Boot and WebFlux features. Time: 20 minutes.
Code Snippet Manager App
We are going to make our Code Snippet Manager reactive. You will use the spring-boot-starter-webflux starter. Remember that Spring WebFlux provides two ways to create a reactive apps: Functional and Annotation-Based; in this lab will explore both.
-
Open a browser and hit the url: http://start.spring.io
-
Click the Switch to the full version link.
-
Fill out the Code Snippet Manager App Project metadata with (See Figure 1.0):
Table 1. Code Snippet Manager App App - metadata Property Value Group:
io.pivotal.workshop
Artifact:
code-snippet-manager-flux
Name:
code-snippet-manager-flux
Package Name:
io.pivotal.workshop.snippet
Dependencies:
Reactive Web, DevTools, MongoDB, Actuator
Spring Boot:
2.0.0.M7
Figure 1.0: Spring Initializr - http://start.spring.ioTipYou can choose either Maven or Gradle project types. -
Type Reactive Web, DevTools, MongoDB, Reactive MongoDB and Actuator in the Dependencies field and press Enter.
-
Click the Generate Project button.
-
Unzip the file in any directory you want.
-
Import your project in any IDE you want.
-
Add the following domain classes: io.pivotal.workshop.snippet.domain.Language and io.pivotal.workshop.snippet.domain.Snippet, practically are the same as before, just a small modifications:
src/main/java/io/pivotal/workshop/snippet/domain/Language.javapackage io.pivotal.workshop.snippet.domain; import org.bson.types.ObjectId; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; @Document public class Language { @Id private String id; private String name; private String syntax = "text"; public Language() { } public Language(String name) { this(); this.name = name; } public Language(String name, String syntax) { this(name); this.syntax = syntax; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSyntax() { return syntax; } public void setSyntax(String syntax) { this.syntax = syntax; } @Override public String toString() { return "Language{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", syntax='" + syntax + '\'' + '}'; } }
src/main/java/io/pivotal/workshop/snippet/domain/Snippet.javapackage io.pivotal.workshop.snippet.domain; import org.bson.types.ObjectId; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; import java.util.Date; @Document public class Snippet { @Id private String id; private String title; private String keywords = ""; private String description = ""; @DBRef private Language lang; private String code; private Date created; private Date modified; public Snippet() { this.created = new Date(); this.modified = new Date(); } public Snippet(String title, String keywords, String description, Language lang, String code) { this(); this.title = title; this.keywords = keywords; this.description = description; this.lang = lang; this.code = code; } public Snippet(String title, Language lang, String code) { this(title, "", "", lang, code); } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Language getLang() { return lang; } public void setLang(Language lang) { this.lang = lang; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getKeywords() { return keywords; } public void setKeywords(String keywords) { this.keywords = keywords; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public Date getCreated() { return created; } public void setCreated(Date created) { this.created = created; } public Date getModified() { return modified; } public void setModified(Date modified) { this.modified = modified; } @Override public String toString() { return "Snippet{" + "id='" + id + '\'' + ", title='" + title + '\'' + ", keywords='" + keywords + '\'' + ", description='" + description + '\'' + ", lang=" + lang + ", code='" + code + '\'' + ", created=" + created + ", modified=" + modified + '}'; } }
-
Add the Repositories. Create/Copy the io.pivotal.workshop.snippet.repository.LanguageRespository.java and io.pivotal.workshop.snippet.repository.SnippetRespository.java interfaces.
src/main/java/io/pivotal/workshop/snippet/repository/LanguageRepository.javapackage io.pivotal.workshop.snippet.repository; import io.pivotal.workshop.snippet.domain.Language; import io.pivotal.workshop.snippet.domain.Snippet; import org.springframework.data.mongodb.repository.MongoRepository; import reactor.core.publisher.Mono; public interface LanguageRepository extends MongoRepository<Language, String> { Language findByName(String name); }
src/main/java/io/pivotal/workshop/snippet/repository/SnippetRepository.javapackage io.pivotal.workshop.snippet.repository; import io.pivotal.workshop.snippet.domain.Snippet; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Query; import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; public interface SnippetRepository extends MongoRepository<Snippet, String> { @Query("{}") Stream<Snippet> findAllAsStream(); }
As you can see in the SnippetRepository interface declares a findAllAsStream method that returns all the snippets from MongoDB.
-
Next, lets create the Reative Server and the Functional Router for the app. Create the io.pivotal.workshop.snippet.config.SnippetReactive class:
src/main/java/io/pivotal/workshop/snippet/config/SnippetReactiveConfig.javapackage io.pivotal.workshop.snippet.config; import io.pivotal.workshop.snippet.domain.Snippet; import io.pivotal.workshop.snippet.reactive.SnippetHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.EmitterProcessor; import reactor.ipc.netty.http.server.HttpServer; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.web.reactive.function.server.RequestPredicates.*; @Configuration public class SnippetReactiveConfig { @Bean public HttpServer httpServer(RouterFunction<ServerResponse> router){ //(1) HttpServer server = HttpServer.create("localhost", 8080); server.newHandler(new ReactorHttpHandlerAdapter(RouterFunctions.toHttpHandler(router))).block(); return server; } @Bean public RouterFunction<ServerResponse> router(SnippetHandler handler){ //(2) return RouterFunctions .route(GET("/snippets").and(accept(APPLICATION_JSON)), handler::findAll) .andRoute(GET("/snippets/{id}").and(accept(APPLICATION_JSON)),handler::findById) .andRoute(POST("/snippets").and(accept(APPLICATION_JSON)),handler::createSnippet); } @Bean public EmitterProcessor snippetStream(){ //(3) return EmitterProcessor.<Snippet>create(); } }
-
This method returns a HttpServer bean class that belongs to the Netty package, meaning that the app will run in a Reactive Server. This method is receiving a router function.
-
This method returns a RouterFunction bean as a ServerResponse type. It defines what are the endpoints and how they will be handled. In this case we are using the SnippetHandler class that will have all the logic for handling all incoming requests.
-
This method returns a EmitterProcessor bean that will be use to stream all the new Snippets created.
-
-
Let’s continue with the SnippetHandler. Create the io.pivotal.workshop.snippet.reactive.SnippetHandler class.
src/main/java/io/pivotal/workshop/snippet/reactive/SnippetHandler.javapackage io.pivotal.workshop.snippet.reactive; import io.pivotal.workshop.snippet.domain.Language; import io.pivotal.workshop.snippet.domain.Snippet; import io.pivotal.workshop.snippet.repository.LanguageRepository; import io.pivotal.workshop.snippet.repository.SnippetRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.EmitterProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Optional; import static org.springframework.web.reactive.function.server.ServerResponse.notFound; import static org.springframework.web.reactive.function.server.ServerResponse.ok; @Component public class SnippetHandler { private EmitterProcessor<Snippet> snippetEmitterProcessor; private SnippetRepository snippetRepository; private LanguageRepository languageRepository; @Autowired //(1) public SnippetHandler(SnippetRepository snippetRepository, LanguageRepository languageRepository, EmitterProcessor<Snippet> snippetEmitterProcessor) { this.snippetRepository = snippetRepository; this.languageRepository = languageRepository; this.snippetEmitterProcessor = snippetEmitterProcessor; } public Mono<ServerResponse> findAll(ServerRequest request){ //(2) return ok().body(BodyInserters.fromPublisher(Flux.fromStream(this.snippetRepository.findAllAsStream()),Snippet.class)); } public Mono<ServerResponse> findById(ServerRequest request) { String snippetId = request.pathVariable("id"); Optional<Snippet> result = this.snippetRepository.findById(snippetId); if(result.isPresent()) return ok().body(BodyInserters.fromPublisher(Mono.just(result.get()),Snippet.class)); else return notFound().build(); } public Mono<ServerResponse> createSnippet(ServerRequest request) { Mono<Snippet> snippetMono = request.bodyToMono(Snippet.class); return ok().build(snippetMono.doOnNext(snippetConsumer -> { Language language = new Language("Unknown","txt"); if(snippetConsumer.getLang()!=null){ language = this.languageRepository.findByName(snippetConsumer.getLang().getName()); if(language == null) { language = this.languageRepository.save(snippetConsumer.getLang()); } }else language = this.languageRepository.save(language); snippetConsumer.setLang(language); Snippet result = this.snippetRepository.save(snippetConsumer); this.snippetEmitterProcessor.onNext(result); //(3) }).then()); } }
-
The constructor will autowired all the repositories and the EmitterProcessor.
-
Take a look that all the methods receive a ServerRequest and the response is a Mono<ServerResponse> type.
-
The createSnippet method will handle all the POST requests (/snippets) and create the necessary Language and Snippet, but the most important part here is that it will emit the snippet saved as stream.
-
-
Now, let’s create a rest controller that will stream all the new snippets. Create the io.pivotal.workshop.snippet.reactive.SnippetController class.
src/main/java/io/pivotal/workshop/snippet/reactive/SnippetController.javapackage io.pivotal.workshop.snippet.reactive; import io.pivotal.workshop.snippet.domain.Snippet; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.EmitterProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import static org.springframework.web.reactive.function.server.ServerResponse.ok; @RestController public class SnippetController { private EmitterProcessor snippetStream; public SnippetController(EmitterProcessor snippetStream) { //(1) this.snippetStream = snippetStream; } @GetMapping(path = "/logs", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<Snippet> snippetLog(){ //(2) return snippetStream.doOnSubscribe(subscription -> { snippetStream.next().subscribe(); }); } }
-
The constructor injects the EmitterProcessor.
-
This method will handle the stream publish in /logs endpoint. See that it’s subscribe to the stream.
-
-
In the src/main/resources/application.properties file add the following properties:
src/main/resources/application.propertiesspring.data.mongodb.database=snippets-[YOUR INITIALS] spring.data.mongodb.host=[PROVIDED BY THE INSTRUCTOR] spring.data.mongodb.port=27017 spring.data.mongodb.username=[PROVIDED BY THE INSTRUCTOR] spring.data.mongodb.password=[PROVIDED BY THE INSTRUCTOR]
REPLACE the values accordingly.
-
Run the application and go to your browser and hit the http://localhost:8080/logs and browser should be waiting for incoming data.
-
Open a new browser tab or window and point to the http://localhost:8080/snippets and you will see an empty json collection.
-
Open a Terminal window and add some snippets using the cUrl command:
curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{"title":"Hello World","code":"println \"This is awesome\".upperCase()","lang":{"name": "Groovy", "syntax":"groovy"}}' http://localhost:8080/snippets
TipIf you are a Windows user, you can use the POSTMAN (https://www.getpostman.com/) application to execute the POST. Add more snippets and see how the http://localhost:8080/logs start receiving some Snippet streams.
-
Review the snippets by hitting again the http://localhost:8080/snippets
Challenges
-
Add an Embedded MongoDB and run the same application. You must add the following dependency to your pom.xml or gradle:
pom.xml<dependency> <groupId>de.flapdoodle.embed</groupId> <artifactId>de.flapdoodle.embed.mongo</artifactId> <scope>runtime</scope> </dependency>
or
build.gradleruntime('de.flapdoodle.embed:de.flapdoodle.embed.mongo')
HOMEWORK
-
Use the annotation-based WebFlux variant for the Code Snippet Manager App for all the endpoints.