├── .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 | Github |
34 | Name |
35 |
36 |
37 |
38 |
39 | 42 |
40 | githubUser |
41 | Jane Doe |
42 |
43 |
44 |
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 |
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
--------------------------------------------------------------------------------