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.

  1. Open a browser and hit the url: http://start.spring.io

  2. Click the Switch to the full version link.

  3. 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.io

    SpringInitializr

    Tip
    You can choose either Maven or Gradle project types.
  4. Type Reactive Web, DevTools, MongoDB, Reactive MongoDB and Actuator in the Dependencies field and press Enter.

  5. Click the Generate Project button.

  6. Unzip the file in any directory you want.

  7. Import your project in any IDE you want.

  8. 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.java
    package 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.java
    package 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 +
                    '}';
        }
    }
  9. 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.java
    package 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.java
    package 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.

  10. 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.java
    package 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();
        }
    }
    1. 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.

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

    3. This method returns a EmitterProcessor bean that will be use to stream all the new Snippets created.

  11. Let’s continue with the SnippetHandler. Create the io.pivotal.workshop.snippet.reactive.SnippetHandler class.

    src/main/java/io/pivotal/workshop/snippet/reactive/SnippetHandler.java
    package 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());
        }
    
    
    }
    1. The constructor will autowired all the repositories and the EmitterProcessor.

    2. Take a look that all the methods receive a ServerRequest and the response is a Mono<ServerResponse> type.

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

  12. 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.java
    package 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(); });
    
        }
    }
    1. The constructor injects the EmitterProcessor.

    2. This method will handle the stream publish in /logs endpoint. See that it’s subscribe to the stream.

  13. In the src/main/resources/application.properties file add the following properties:

    src/main/resources/application.properties
    spring.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.

  14. Run the application and go to your browser and hit the http://localhost:8080/logs and browser should be waiting for incoming data.

  15. Open a new browser tab or window and point to the http://localhost:8080/snippets and you will see an empty json collection.

  16. 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
    Tip
    If 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.

    Logs

  17. Review the snippets by hitting again the http://localhost:8080/snippets

    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.gradle
    runtime('de.flapdoodle.embed:de.flapdoodle.embed.mongo')

HOMEWORK

  • Use the annotation-based WebFlux variant for the Code Snippet Manager App for all the endpoints.