├── .gitignore ├── README.md ├── pom.xml ├── quote-app ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── example │ │ ├── QuoteApplication.java │ │ ├── domain │ │ ├── User.java │ │ └── UserRepository.java │ │ └── web │ │ ├── MainController.java │ │ └── Quote.java │ └── resources │ └── templates │ ├── index.html │ └── quotes.html └── quote-service ├── pom.xml └── src └── main ├── kotlin └── com │ └── example │ └── quote │ ├── Quote.kt │ ├── QuoteGenerator.kt │ ├── QuoteHandler.kt │ ├── QuoteRoutes.kt │ └── QuoteService.kt └── resources └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web flux streaming demo 2 | 3 | This demo application showcases some of the features of the reactive support in Spring 4 | Framework 5. 5 | 6 | `quote-service` is a functional-based kotlin service that emits a "random" quote every 7 | 200ms, either as server-sent events (SSE) or json stream. 8 | 9 | `quote-app` uses embedded MongoDB and the upcoming reactive MongoDB support to showcase 10 | how a template engine can integrate with this new paradigm. It also has a UI that uses 11 | display the value of a set of actions live. For that, it uses the new `WebClient` API on 12 | the server side to get the values from the `quote-service` in a reactive fashion. 13 | 14 | To run the demo, simply start the two apps and go to `http://localhost:8080`. -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.0.0.M1 10 | 11 | 12 | com.example 13 | demo-webflux-streaming 14 | 0.0.1-SNAPSHOT 15 | pom 16 | WebFlux streaming demo 17 | 18 | 19 | UTF-8 20 | UTF-8 21 | 1.8 22 | 23 | 24 | 25 | quote-app 26 | quote-service 27 | 28 | 29 | 30 | 31 | 32 | org.webjars 33 | bootstrap 34 | 3.3.7 35 | 36 | 37 | org.webjars 38 | highcharts 39 | 5.0.8 40 | 41 | 42 | 43 | 44 | 45 | 46 | spring-snapshots 47 | Spring Snapshots 48 | https://repo.spring.io/snapshot 49 | 50 | true 51 | 52 | 53 | 54 | spring-milestones 55 | Spring Milestones 56 | https://repo.spring.io/milestone 57 | 58 | false 59 | 60 | 61 | 62 | 63 | 64 | spring-snapshots 65 | Spring Snapshots 66 | https://repo.spring.io/snapshot 67 | 68 | true 69 | 70 | 71 | 72 | spring-milestones 73 | Spring Milestones 74 | https://repo.spring.io/milestone 75 | 76 | false 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /quote-app/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.example 8 | demo-webflux-streaming 9 | 0.0.1-SNAPSHOT 10 | 11 | quote-app 12 | jar 13 | Quote App 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-webflux 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-mongodb-reactive 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-thymeleaf 27 | 28 | 29 | 30 | com.fasterxml.jackson.datatype 31 | jackson-datatype-jsr310 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-devtools 37 | true 38 | 39 | 40 | 41 | de.flapdoodle.embed 42 | de.flapdoodle.embed.mongo 43 | runtime 44 | 45 | 46 | 47 | org.webjars 48 | bootstrap 49 | runtime 50 | 51 | 52 | org.webjars 53 | highcharts 54 | runtime 55 | 56 | 57 | 58 | 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-maven-plugin 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /quote-app/src/main/java/com/example/QuoteApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import java.time.Duration; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | import com.example.domain.User; 8 | import com.example.domain.UserRepository; 9 | 10 | import org.springframework.boot.CommandLineRunner; 11 | import org.springframework.boot.SpringApplication; 12 | import org.springframework.boot.autoconfigure.SpringBootApplication; 13 | import org.springframework.context.annotation.Bean; 14 | 15 | @SpringBootApplication 16 | public class QuoteApplication { 17 | 18 | public static void main(String[] args) { 19 | SpringApplication.run(QuoteApplication.class, args); 20 | } 21 | 22 | @Bean 23 | public CommandLineRunner createUsers(UserRepository userRepository) { 24 | return strings -> { 25 | List users = Arrays.asList( 26 | new User("sdeleuze", "Sebastien Deleuze"), 27 | new User("bclozel", "Brian Clozel"), 28 | new User("rstoyanchev", "Rossen Stoyanchev"), 29 | new User("smaldini", "Stephane Maldini"), 30 | new User("sbasle", "Simon Basle"), 31 | new User("snicoll", "Stephane Nicoll") 32 | ); 33 | userRepository.saveAll(users).blockLast(Duration.ofSeconds(3)); 34 | }; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /quote-app/src/main/java/com/example/domain/User.java: -------------------------------------------------------------------------------- 1 | package com.example.domain; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.mapping.Document; 5 | 6 | @Document 7 | public class User { 8 | 9 | @Id 10 | private String id; 11 | 12 | private String github; 13 | 14 | private String name; 15 | 16 | public User() { 17 | } 18 | 19 | public User(String github, String name) { 20 | this.github = github; 21 | this.name = name; 22 | } 23 | 24 | public String getId() { 25 | return id; 26 | } 27 | 28 | public void setId(String id) { 29 | this.id = id; 30 | } 31 | 32 | public String getGithub() { 33 | return github; 34 | } 35 | 36 | public void setGithub(String github) { 37 | this.github = github; 38 | } 39 | 40 | public String getName() { 41 | return name; 42 | } 43 | 44 | public void setName(String name) { 45 | this.name = name; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /quote-app/src/main/java/com/example/domain/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.domain; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | import org.springframework.data.mongodb.repository.ReactiveMongoRepository; 6 | 7 | public interface UserRepository extends ReactiveMongoRepository { 8 | 9 | Mono findUserByGithub(String github); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /quote-app/src/main/java/com/example/web/MainController.java: -------------------------------------------------------------------------------- 1 | package com.example.web; 2 | 3 | import com.example.domain.UserRepository; 4 | import reactor.core.publisher.Flux; 5 | 6 | import org.springframework.http.MediaType; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.Model; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.ResponseBody; 11 | import org.springframework.web.reactive.function.client.WebClient; 12 | 13 | @Controller 14 | public class MainController { 15 | 16 | private final UserRepository userRepository; 17 | 18 | private final WebClient webClient; 19 | 20 | private final Flux quotesFeed; 21 | 22 | public MainController(UserRepository userRepository) { 23 | this.userRepository = userRepository; 24 | this.webClient = WebClient.create("http://localhost:8081"); 25 | this.quotesFeed = this.webClient.get() 26 | .uri("/quotes") 27 | .accept(MediaType.APPLICATION_STREAM_JSON) 28 | .retrieve() 29 | .bodyToFlux(Quote.class) 30 | .share() 31 | .log(); 32 | } 33 | 34 | @GetMapping("/") 35 | public String home(Model model) { 36 | model.addAttribute("users", this.userRepository.findAll()); 37 | return "index"; 38 | } 39 | 40 | @GetMapping("/quotes") 41 | public String quotes() { 42 | return "quotes"; 43 | } 44 | 45 | @GetMapping(path = "/quotes/feed", produces = MediaType.TEXT_EVENT_STREAM_VALUE) 46 | @ResponseBody 47 | public Flux fetchQuotesStream() { 48 | return quotesFeed; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /quote-app/src/main/java/com/example/web/Quote.java: -------------------------------------------------------------------------------- 1 | package com.example.web; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.time.Instant; 6 | 7 | class Quote { 8 | 9 | private static final MathContext MATH_CONTEXT = new MathContext(2); 10 | 11 | private String ticker; 12 | 13 | private BigDecimal price; 14 | 15 | private Instant instant; 16 | 17 | public Quote() { 18 | } 19 | 20 | public Quote(String ticker, BigDecimal price) { 21 | this.ticker = ticker; 22 | this.price = price; 23 | } 24 | 25 | public Quote(String ticker, Double price) { 26 | this(ticker, new BigDecimal(price, MATH_CONTEXT)); 27 | } 28 | 29 | public String getTicker() { 30 | return ticker; 31 | } 32 | 33 | public void setTicker(String ticker) { 34 | this.ticker = ticker; 35 | } 36 | 37 | public BigDecimal getPrice() { 38 | return price; 39 | } 40 | 41 | public void setPrice(BigDecimal price) { 42 | this.price = price; 43 | } 44 | 45 | public Instant getInstant() { 46 | return instant; 47 | } 48 | 49 | public void setInstant(Instant instant) { 50 | this.instant = instant; 51 | } 52 | 53 | public void setInstant(long epoch) { 54 | this.instant = Instant.ofEpochSecond(epoch); 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return "Quote{" + 60 | "ticker='" + ticker + '\'' + 61 | ", price=" + price + 62 | ", instant=" + instant + 63 | '}'; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /quote-app/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Spring WebFlux Streaming 10 | 11 | 12 | 13 | 14 | 27 |
28 |

List of Spring developers

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
#GithubName
42githubUserJane Doe
45 |
46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /quote-app/src/main/resources/templates/quotes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Spring WebFlux Streaming 10 | 11 | 12 | 13 | 14 | 15 | 28 |
29 |
30 |
31 | 32 | 33 | 34 | 88 | 89 | -------------------------------------------------------------------------------- /quote-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.example 8 | demo-webflux-streaming 9 | 0.0.1-SNAPSHOT 10 | 11 | quote-service 12 | jar 13 | Quote service 14 | 15 | 16 | 1.1.2 17 | 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-webflux 23 | 24 | 25 | 26 | com.fasterxml.jackson.datatype 27 | jackson-datatype-jsr310 28 | 29 | 30 | 31 | org.jetbrains.kotlin 32 | kotlin-stdlib-jre8 33 | ${kotlin.version} 34 | 35 | 36 | org.jetbrains.kotlin 37 | kotlin-reflect 38 | ${kotlin.version} 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-devtools 44 | true 45 | 46 | 47 | 48 | 49 | ${project.basedir}/src/main/kotlin 50 | ${project.basedir}/src/test/kotlin 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-maven-plugin 55 | 56 | 57 | org.jetbrains.kotlin 58 | kotlin-maven-plugin 59 | ${kotlin.version} 60 | 61 | 62 | spring 63 | 64 | 1.8 65 | 66 | 67 | 68 | compile 69 | compile 70 | 71 | compile 72 | 73 | 74 | 75 | test-compile 76 | test-compile 77 | 78 | test-compile 79 | 80 | 81 | 82 | 83 | 84 | org.jetbrains.kotlin 85 | kotlin-maven-allopen 86 | ${kotlin.version} 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /quote-service/src/main/kotlin/com/example/quote/Quote.kt: -------------------------------------------------------------------------------- 1 | package com.example.quote 2 | 3 | import java.math.BigDecimal 4 | import java.time.Instant 5 | 6 | data class Quote(val ticker: String, val price: BigDecimal, val instant: Instant = Instant.now()) 7 | -------------------------------------------------------------------------------- /quote-service/src/main/kotlin/com/example/quote/QuoteGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.example.quote 2 | 3 | import org.springframework.stereotype.Component 4 | import reactor.core.publisher.Flux 5 | import reactor.core.publisher.SynchronousSink 6 | import java.math.BigDecimal 7 | import java.math.MathContext 8 | import java.time.Duration 9 | import java.time.Instant 10 | import java.util.* 11 | 12 | @Component 13 | class QuoteGenerator { 14 | 15 | val mathContext = MathContext(2) 16 | 17 | val random = Random() 18 | 19 | val prices = listOf( 20 | Quote("CTXS", BigDecimal(82.26, mathContext)), 21 | Quote("DELL", BigDecimal(63.74, mathContext)), 22 | Quote("GOOG", BigDecimal(847.24, mathContext)), 23 | Quote("MSFT", BigDecimal(65.11, mathContext)), 24 | Quote("ORCL", BigDecimal(45.71, mathContext)), 25 | Quote("RHT", BigDecimal(84.29, mathContext)), 26 | Quote("VMW", BigDecimal(92.21, mathContext)) 27 | ) 28 | 29 | 30 | fun fetchQuoteStream(period: Duration) = Flux.generate({ 0 }, 31 | { index, sink: SynchronousSink -> 32 | sink.next(updateQuote(prices[index])) 33 | (index + 1) % prices.size 34 | }).zipWith(Flux.interval(period)) 35 | .map { it.t1.copy(instant = Instant.now()) } 36 | .share() 37 | .log() 38 | 39 | 40 | private fun updateQuote(quote: Quote) = quote.copy( 41 | price = quote.price.add(quote.price.multiply( 42 | BigDecimal(0.05 * random.nextDouble()), mathContext)) 43 | ) 44 | 45 | } 46 | -------------------------------------------------------------------------------- /quote-service/src/main/kotlin/com/example/quote/QuoteHandler.kt: -------------------------------------------------------------------------------- 1 | package com.example.quote 2 | 3 | import org.springframework.http.MediaType.APPLICATION_STREAM_JSON 4 | import org.springframework.http.MediaType.TEXT_EVENT_STREAM 5 | import org.springframework.stereotype.Component 6 | import org.springframework.web.reactive.function.server.ServerRequest 7 | import org.springframework.web.reactive.function.server.ServerResponse.ok 8 | import org.springframework.web.reactive.function.server.body 9 | import java.time.Duration.ofMillis 10 | 11 | @Component 12 | class QuoteHandler(val quoteGenerator: QuoteGenerator) { 13 | 14 | fun fetchQuotesSSE(req: ServerRequest) = ok() 15 | .contentType(TEXT_EVENT_STREAM) 16 | .body(quoteGenerator.fetchQuoteStream(ofMillis(200)), Quote::class.java) 17 | 18 | fun fetchQuotes(req: ServerRequest) = ok() 19 | .contentType(APPLICATION_STREAM_JSON) 20 | .body(quoteGenerator.fetchQuoteStream(ofMillis(200)), Quote::class.java) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /quote-service/src/main/kotlin/com/example/quote/QuoteRoutes.kt: -------------------------------------------------------------------------------- 1 | package com.example.quote 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.http.MediaType.APPLICATION_STREAM_JSON 6 | import org.springframework.http.MediaType.TEXT_EVENT_STREAM 7 | import org.springframework.web.reactive.function.server.router 8 | 9 | @Configuration 10 | class QuoteRoutes(val quoteHandler: QuoteHandler) { 11 | 12 | @Bean 13 | fun quoteRouter() = router { 14 | GET("/quotes").nest { 15 | accept(TEXT_EVENT_STREAM, quoteHandler::fetchQuotesSSE) 16 | accept(APPLICATION_STREAM_JSON, quoteHandler::fetchQuotes) 17 | } 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /quote-service/src/main/kotlin/com/example/quote/QuoteService.kt: -------------------------------------------------------------------------------- 1 | package com.example.quote 2 | 3 | import org.springframework.boot.SpringApplication 4 | import org.springframework.boot.autoconfigure.SpringBootApplication 5 | 6 | @SpringBootApplication 7 | class QuoteService 8 | 9 | fun main(args: Array) { 10 | SpringApplication.run(QuoteService::class.java, *args) 11 | } -------------------------------------------------------------------------------- /quote-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8081 2 | 3 | spring.jackson.serialization.write-date-timestamps-as-nanoseconds=false --------------------------------------------------------------------------------