This repository hosts a complete workshop on Spring Boot + Spring WebFlux. Just follow this README and create your first WebFlux applications! Each step of this workshop has its companion commit in the git history with a detailed commit message.

We’ll create two applications:

  • stock-quotes is a functional WebFlux app which streams stock quotes

  • trading-service is an annotation-based WebFlux app using a datastore, HTML views, and several browser-related technologies

Reference documentations can be useful while working on those apps:

Stock Quotes application

Create this application

Go to https://start.spring.io and create a Maven project with Spring Boot 2.0.0.M1, with groupId io.spring.workshop and artifactId stock-quotes. Select the Reactive Web Boot starter. Unzip the given file into a directory and import that application into your IDE.

If generated right, you should have a main Application class that looks like this:

stock-quotes/src/main/java/io/spring/workshop/stockquotes/StockQuotesApplication.java
package io.spring.workshop.stockquotes;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StockQuotesApplication {

	public static void main(String[] args) {
		SpringApplication.run(StockQuotesApplication.class, args);
	}
}

Edit your application.properties file to start the server on a specific port.

stock-quotes/src/main/resources/application.properties
server.port=8081

Launching it from your IDE or with mvn spring-boot:run should start a Netty server on port 8081. You should see in the logs something like:

INFO 2208 --- [  restartedMain] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8081
INFO 2208 --- [  restartedMain] i.s.w.s.StockQuotesApplication           : Started StockQuotesApplication in 1.905 seconds (JVM running for 3.075)

Create a Quote Generator

To simulate real stock values, we’ll create a generator that emits such values at a specific interval. Copy the following classes to your project.

stock-quotes/src/main/java/io/spring/workshop/stockquotes/Quote.java
package io.spring.workshop.stockquotes;

import java.math.BigDecimal;
import java.math.MathContext;
import java.time.Instant;

public class Quote {

	private static final MathContext MATH_CONTEXT = new MathContext(2);

	private String ticker;

	private BigDecimal price;

	private Instant instant;

	public Quote() {
	}

	public Quote(String ticker, BigDecimal price) {
		this.ticker = ticker;
		this.price = price;
	}

	public Quote(String ticker, Double price) {
		this(ticker, new BigDecimal(price, MATH_CONTEXT));
	}

	public String getTicker() {
		return ticker;
	}

	public void setTicker(String ticker) {
		this.ticker = ticker;
	}

	public BigDecimal getPrice() {
		return price;
	}

	public void setPrice(BigDecimal price) {
		this.price = price;
	}

	public Instant getInstant() {
		return instant;
	}

	public void setInstant(Instant instant) {
		this.instant = instant;
	}

	@Override
	public String toString() {
		return "Quote{" +
				"ticker='" + ticker + '\'' +
				", price=" + price +
				", instant=" + instant +
				'}';
	}
}
stock-quotes/src/main/java/io/spring/workshop/stockquotes/QuoteGenerator.java
package io.spring.workshop.stockquotes;

import java.math.BigDecimal;
import java.math.MathContext;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.BiFunction;

import reactor.core.publisher.Flux;
import reactor.core.publisher.SynchronousSink;

import org.springframework.stereotype.Component;

@Component
public class QuoteGenerator {

	private final MathContext mathContext = new MathContext(2);

	private final Random random = new Random();

	private final List<Quote> prices = new ArrayList<>();

	/**
	 * Bootstraps the generator with tickers and initial prices
	 */
	public QuoteGenerator() {
		this.prices.add(new Quote("CTXS", 82.26));
		this.prices.add(new Quote("DELL", 63.74));
		this.prices.add(new Quote("GOOG", 847.24));
		this.prices.add(new Quote("MSFT", 65.11));
		this.prices.add(new Quote("ORCL", 45.71));
		this.prices.add(new Quote("RHT", 84.29));
		this.prices.add(new Quote("VMW", 92.21));
	}


	public Flux<Quote> fetchQuoteStream(Duration period) {

		// We use here Flux.generate to create quotes,
    // iterating on each stock starting at index 0
		return Flux.generate(() -> 0,
				(BiFunction<Integer, SynchronousSink<Quote>, Integer>) (index, sink) -> {
					Quote updatedQuote = updateQuote(this.prices.get(index));
					sink.next(updatedQuote);
					return ++index % this.prices.size();
				})
				// We want to emit them with a specific period;
        // to do so, we zip that Flux with a Flux.interval
				.zipWith(Flux.interval(period)).map(t -> t.getT1())
				// Because values are generated in batches,
        // we need to set their timestamp after their creation
				.map(quote -> {
					quote.setInstant(Instant.now());
					return quote;
				})
				.log("io.spring.workshop.stockquotes");
	}

	private Quote updateQuote(Quote quote) {
		BigDecimal priceChange = quote.getPrice()
				.multiply(new BigDecimal(0.05 * this.random.nextDouble()), this.mathContext);
		return new Quote(quote.getTicker(), quote.getPrice().add(priceChange));
	}
}

Because we’re working with java.time.Instant and Jackson, we should import the dedicated module in our app.

stock-quotes/pom.xml
		<dependency>
			<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-jsr310</artifactId>
		</dependency>

Functional web applications with "WebFlux.fn"

Spring WebFlux comes in two flavors of web applications: annotation based and functional. For this first application, we’ll use the functional variant.

Incoming HTTP requests are handled by a HandlerFunction, which is essentially a function that takes a ServerRequest and returns a Mono<ServerResponse>. The annotation counterpart to a handler function would be a Controller method.

But how those incoming requests are routed to the right handler?

We’re using a RouterFunction, which is a function that takes a ServerRequest, and returns a Mono<HandlerFunction>. If a request matches a particular route, a handler function is returned; otherwise it returns an empty Mono. The RouterFunction has a similar purpose as the @RequestMapping annotation in @Controller classes.

Take a look at the code samples in the Spring WebFlux.fn reference documentation

Create your first HandlerFunction + RouterFunction

First, create a QuoteHandler class and mark is as a @Component;this class will have all our handler functions as methods. Now create a hello handler function in that class that always returns "text/plain" HTTP responses with "Hello Spring!" as body.

To route requests to that handler, you need to expose a RouterFunction to Spring Boot. Create a QuoteRouter configuration class (i.e. annotated with @Configuration) that creates a bean of type RouterFunction<ServerResponse>.

Modify that class so that GET requests to "/hello" are routed to the handler you just implemented.

Since QuoteHandler is a component, you can inject it in @Bean methods as a method parameter.

Your application should now behave like this:

$ curl http://localhost:8081/hello -i
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: text/plain;charset=UTF-8

Hello Spring!%

Once done, add another endpoint:

  • with a HandlerFunction echo that echoes the request body in the response, as "text/plain"

  • and an additional route in our existing RouterFunction that accepts POST requests on "/echo" with a "text/plain" body and returns responses with the same content type.

You can also use this new endpoint with:

$ curl http://localhost:8081/echo -i -d "WebFlux workshop" -H "Content-Type: text/plain"
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: text/plain

WebFlux workshop%

Expose the Flux<Quotes> as a web service

First, let’s inject our QuoteGenerator instance in our QuoteHandler, instantiate a Flux<Quote> from it that emits a Quote every 200 msec and can be shared between multiple subscribers (look at the Flux operators for that). This instance should be kept as an attribute for reusability.

Now create a streamQuotes handler that streams those generated quotes with the "application/stream+json" content type. Add the corresponding part in the RouterFunction, on the "/quotes" endpoint.

$ curl http://localhost:8081/quotes -i -H "Accept: application/stream+json"
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/stream+json

{"ticker":"CTXS","price":84.0,"instant":1494841666.633000000}
{"ticker":"DELL","price":67.1,"instant":1494841666.834000000}
{"ticker":"GOOG","price":869,"instant":1494841667.034000000}
{"ticker":"MSFT","price":66.5,"instant":1494841667.231000000}
{"ticker":"ORCL","price":46.13,"instant":1494841667.433000000}
{"ticker":"RHT","price":86.9,"instant":1494841667.634000000}
{"ticker":"VMW","price":93.7,"instant":1494841667.833000000}

Let’s now create a variant of that — instead of streaming all values (with an infinite stream), we can now take the last "n" elements of that Flux and return those as a collection of Quotes with the content type "application/json". Note that you should take the requested number of Quotes from the request itself, with the query parameter named "size" (or pick 10 as the default size if none was provided).

curl http://localhost:8081/quotes -i -H "Accept: application/json"
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/json

[{"ticker":"CTXS","price":85.8,"instant":1494842241.716000000},{"ticker":"DELL","price":64.69,"instant":1494842241.913000000},{"ticker":"GOOG","price":856.5,"instant":1494842242.112000000},{"ticker":"MSFT","price":68.2,"instant":1494842242.317000000},{"ticker":"ORCL","price":47.4,"instant":1494842242.513000000},{"ticker":"RHT","price":85.6,"instant":1494842242.716000000},{"ticker":"VMW","price":96.1,"instant":1494842242.914000000},{"ticker":"CTXS","price":85.5,"instant":1494842243.116000000},{"ticker":"DELL","price":64.88,"instant":1494842243.316000000},{"ticker":"GOOG","price":889,"instant":1494842243.517000000}]%

Integration tests with WebTestClient

Spring WebFlux (actually the spring-test module) includes a WebTestClient that can be used to test WebFlux server endpoints with or without a running server. Tests without a running server are comparable to MockMvc from Spring MVC where mock request and response are used instead of connecting over the network using a socket. The WebTestClient however can also perform tests against a running server.

You can check that your last endpoint is working properly with the following integration test:

stock-quotes/src/test/java/io/spring/workshop/stockquotes/StockQuotesApplicationTests.java
package io.spring.workshop.stockquotes;

import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
//  We create a `@SpringBootTest`, starting an actual server on a `RANDOM_PORT`
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class StockQuotesApplicationTests {

	// Spring Boot will create a `WebTestClient` for you,
	// already configure and ready to issue requests against "localhost:RANDOM_PORT"
	@Autowired
	private WebTestClient webTestClient;

	@Test
	public void fetchQuotes() {
		webTestClient
				// We then create a GET request to test an endpoint
				.get().uri("/quotes?size=20")
				.accept(MediaType.APPLICATION_JSON)
				.exchange()
				// and use the dedicated DSL to test assertions against the response
				.expectStatus().isOk()
				.expectHeader().contentType(MediaType.APPLICATION_JSON)
				.expectBodyList(Quote.class)
				.hasSize(20)
				// Here we check that all Quotes have a positive price value
				.consumeWith(allQuotes ->
						assertThat(allQuotes).allSatisfy(quote -> assertThat(quote.getPrice()).isPositive()));
	}

}

Trading Service application

Create this application

Go to https://start.spring.io and create a Maven project with Spring Boot 2.0.0.M1, with groupId io.spring.workshop and artifactId trading-service. Select the Reactive Web , Devtools, Thymeleaf and Reactive Mongo Boot starters. Unzip the given file into a directory and import that application into your IDE.

Use Tomcat as a web engine

By default, spring-boot-starter-webflux transitively brings spring-boot-starter-reactor-netty and Spring Boot auto-configures Reactor Netty as a web server. For this application, we’ll use Tomcat as an alternative.

trading-service/pom.xml
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
		</dependency>

Note that Spring Boot supports as well Undertow and Jetty.

Use a reactive datastore

In this application, we’ll use a MongoDB datastore with its reactive driver; for this workshop, we’ll use an in-memory instance of MongoDB. So add the following:

trading-service/pom.xml
		<dependency>
			<groupId>de.flapdoodle.embed</groupId>
			<artifactId>de.flapdoodle.embed.mongo</artifactId>
		</dependency>

We’d like to manage TradingUser with our datastore.

trading-service/src/main/java/io/spring/workshop/tradingservice/TradingUser.java
package io.spring.workshop.tradingservice;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Document
public class TradingUser {

	@Id
	private String id;

	private String userName;

	private String fullName;

	public TradingUser() {
	}

	public TradingUser(String id, String userName, String fullName) {
		this.id = id;
		this.userName = userName;
		this.fullName = fullName;
	}

	public TradingUser(String userName, String fullName) {
		this.userName = userName;
		this.fullName = fullName;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getFullName() {
		return fullName;
	}

	public void setFullName(String fullName) {
		this.fullName = fullName;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;

		TradingUser that = (TradingUser) o;

		if (!id.equals(that.id)) return false;
		return userName.equals(that.userName);
	}

	@Override
	public int hashCode() {
		int result = id.hashCode();
		result = 31 * result + userName.hashCode();
		return result;
	}
}

Now create a TradingUserRepository interface that extends ReactiveMongoRepository. Add a findByUserName(String userName) method that returns a single TradingUser in a reactive fashion.

We’d like to insert users in our datastore when the application starts up. For that, create a UsersCommandLineRunner component that implements Spring Boot’s CommandLineRunner. In the run method, use the reactive repository to insert TradingUser instances in the datastore.

Since the run method returns void, it expects a blocking implementation. This is why you should use the blockLast(Duration) operator on the Flux returned by the repository when inserting data. You can also then().block(Duration) to turn that Flux into a Mono<Void> that waits for completion.

Create a JSON web service

We’re now going to expose TradingUser through a Controller. First, create a UserController annotated with @RestController. Then add two new Controller methods in order to handle:

  • GET requests to "/users", returning all TradingUser instances, serializing them with content-type "application/json"

  • GET requests to "/users/{username}", returning a single TradingUser instance, serializing it with content-type "application/json"

You can now validate your implementation with the following test:

trading-service/src/test/java/io/spring/workshop/tradingservice/UserControllerTests.java
package io.spring.workshop.tradingservice;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.BDDMockito;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;

@RunWith(SpringRunner.class)
@WebFluxTest(UserController.class)
public class UserControllerTests {

  @Autowired
  private WebTestClient webTestClient;

  @MockBean
  private TradingUserRepository repository;

  @Test
  public void listUsers() {
    TradingUser juergen = new TradingUser("1", "jhoeller", "Juergen Hoeller");
    TradingUser andy = new TradingUser("2", "wilkinsona", "Andy Wilkinson");

    BDDMockito.given(this.repository.findAll())
        .willReturn(Flux.just(juergen, andy));

    this.webTestClient.get().uri("/users").accept(MediaType.APPLICATION_JSON)
        .exchange()
        .expectBodyList(TradingUser.class)
        .hasSize(2)
        .contains(juergen, andy);

  }

  @Test
  public void showUser() {
    TradingUser juergen = new TradingUser("1", "jhoeller", "Juergen Hoeller");

    BDDMockito.given(this.repository.findByUserName("jhoeller"))
        .willReturn(Mono.just(juergen));

    this.webTestClient.get().uri("/users/jhoeller").accept(MediaType.APPLICATION_JSON)
        .exchange()
        .expectBody(TradingUser.class)
        .isEqualTo(juergen);
  }

}

Use Thymeleaf to render HTML views

We already added the Thymeleaf Boot starter when we created our trading application.

First, let’s add a couple of WebJar dependencies to get static resources for our application:

trading-service/pom.xml
		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>bootstrap</artifactId>
			<version>3.3.7</version>
		</dependency>
		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>highcharts</artifactId>
			<version>5.0.8</version>
		</dependency>

We can now create HTML templates in our src/main/resources/templates folder and map them using controllers.

trading-service/src/main/resources/templates/index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <meta name="description" content="Spring WebFlux Workshop"/>
    <meta name="author" content="Violeta Georgieva and Brian Clozel"/>
    <title>Spring Trading application</title>
    <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap-theme.min.css"/>
    <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap.min.css"/>
</head>
<body>
<nav class="navbar navbar-default">
    <div class="container-fluid">
        <div class="navbar-header">
            <a class="navbar-brand" href="/">Spring Trading application</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li class="active"><a href="/">Home</a></li>
                <li><a href="/quotes">Quotes</a></li>
                <li><a href="/websocket">Websocket</a></li>
            </ul>
        </div>
    </div>
</nav>
<div class="container wrapper">
    <h2>Trading users</h2>
    <table class="table table-striped">
        <thead>
        <tr>
            <th>#</th>
            <th>User name</th>
            <th>Full name</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="user: ${users}">
            <th scope="row" th:text="${user.id}">42</th>
            <td th:text="${user.userName}">janedoe</td>
            <td th:text="${user.fullName}">Jane Doe</td>
        </tr>
        </tbody>
    </table>
</div>
<script type="text/javascript" src="/webjars/jquery/1.11.1/jquery.min.js"></script>
<script type="text/javascript" src="/webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</body>
</html>

As you can see in that template, we loop over the "users" attribute and write a row in our HTML table for each.

Let’s display those users in our application:

  • Create a HomeController Controller

  • Add a Controller method that handles GET requests to "/"

  • Inject the Spring Model on that method and add a "users" attribute to it

Spring WebFlux will resolve automatically Publisher instances before rendering the view, there’s no need to involve blocking code at all!

Use the WebClient to stream JSON to the browser

In this section, we’ll call our remote stock-quotes service to get Quotes from it, so we first need to:

  • copy over the Quote class to this application

  • add the Jackson JSR310 module dependency

Add the following template file to your application:

trading-service/src/main/resources/templates/quotes.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <meta name="description" content="Spring WebFlux Workshop"/>
    <meta name="author" content="Violeta Georgieva and Brian Clozel"/>
    <title>Spring Trading application</title>
    <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap-theme.min.css"/>
    <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap.min.css"/>
    <link rel="stylesheet" href="/webjars/highcharts/5.0.8/css/highcharts.css"/>
</head>
<body>
<nav class="navbar navbar-default">
    <div class="container-fluid">
        <div class="navbar-header">
            <a class="navbar-brand" href="/">Spring Trading application</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="/">Home</a></li>
                <li class="active"><a href="/quotes">Quotes</a></li>
                <li><a href="/websocket">Websocket</a></li>
            </ul>
        </div>
    </div>
</nav>
<div class="container wrapper">
    <div id="chart" style="height: 400px; min-width: 310px"></div>
</div>
<script type="text/javascript" src="/webjars/jquery/1.11.1/jquery.min.js"></script>
<script type="text/javascript" src="/webjars/highcharts/5.0.8/highcharts.js"></script>
<script type="text/javascript" src="/webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script type="text/javascript">

    // Setting up the chart
    var chart = new Highcharts.chart('chart', {
        title: {
            text: 'My Stock Portfolio'
        },
        yAxis: {
            title: {
                text: 'Stock Price'
            }
        },
        legend: {
            layout: 'vertical',
            align: 'right',
            verticalAlign: 'middle'
        },
        xAxis: {
            type: 'datetime',
        },
        series: [{
            name: 'CTXS',
            data: []
        }, {
            name: 'MSFT',
            data: []
        }, {
            name: 'ORCL',
            data: []
        }, {
            name: 'RHT',
            data: []
        }, {
            name: 'VMW',
            data: []
        }, {
            name: 'DELL',
            data: []
        }]
    });

    // This function adds the given data point to the chart
    var appendStockData = function (quote) {
        chart.series
            .filter(function (serie) {
                return serie.name == quote.ticker
            })
            .forEach(function (serie) {
                var shift = serie.data.length > 40;
                serie.addPoint([quote.instant * 1000, quote.price], true, shift);
            });
    };

    // The browser connects to the server and receives quotes using ServerSentEvents
    // those quotes are appended to the chart as they're received
    var stockEventSource = new EventSource("/quotes/feed");
    stockEventSource.onmessage = function (e) {
        appendStockData(JSON.parse(e.data));
    };
</script>
</body>
</html>

As you can see in this template file, loading that HTML page will cause the browser to send a request the server for Quotes using the Server Sent Event transport.

Now create a QuotesController annotated with @Controller and add two methods. One that renders the quotes.html template for incoming "GET /quotes" requests. The other should response to "GET /quotes/feed" requests with the "text/event-stream" content-type, with a Flux<Quote> as the response body. This data is already server by the stock-quotes application , so you can use a WebClient to request the remote web service to retrieve that Flux.

You should avoid making a request to the stock-quotes service for every browser connecting to that page — for that, you can use the Flux.share() operator.

Create and Configure a WebSocketHandler

WebFlux includes functional reactive WebSocket client and server support.

On the server side there are two main components: WebSocketHandlerAdapter will handle the incoming requests by delegating to the configured WebSocketService and WebSocketHandler will be responsible to handle WebSocket session.

Take a look at the code samples in Reactive WebSocket Support documentation

First, create an EchoWebSocketHandler class; it has to implement WebSocketHandler. Now implement handle(WebSocketSession session) method. The handler echoes the incoming messages with a delay of 1s.

To route requests to that handler, you need to map the above WebSocket handler to a specific URL: here, "/websocket/echo". Create a WebSocketRouter configuration class (i.e. annotated with @Configuration) that creates a bean of type HandlerMapping. Create one additional bean of type WebSocketHandlerAdapter which will delegate the processing of the incoming request to the default WebSocketService which is HandshakeWebSocketService.

Now create a WebSocketController annotated with @Controller and add a method that renders the websocket.html template for incoming "GET /websocket" requests.

Add the following template file to your application:

trading-service/src/main/resources/templates/websocket.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <meta name="description" content="Spring WebFlux Workshop"/>
    <meta name="author" content="Violeta Georgieva and Brian Clozel"/>
    <title>Spring Trading application</title>
    <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap-theme.min.css"/>
    <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap.min.css"/>
</head>
<body>
<nav class="navbar navbar-default">
    <div class="container-fluid">
        <div class="navbar-header">
            <a class="navbar-brand" href="/">Spring Trading application</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="/">Home</a></li>
                <li><a href="/quotes">Quotes</a></li>
                <li class="active"><a href="/websocket">Websocket</a></li>
            </ul>
        </div>
    </div>
</nav>
<div class="container wrapper">
    <h2>Websocket Echo</h2>
    <form class="form-inline">
        <div class="form-group">
            <input class="form-control" type="text" id="input" value="type something">
            <input class="btn btn-default" type="submit" id="button" value="Send"/>
        </div>
    </form>
    <div id="output"></div>
</div>
<script type="text/javascript" src="/webjars/jquery/1.11.1/jquery.min.js"></script>
<script type="text/javascript" src="/webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script type="text/javascript">
    $(document).ready(function () {
        if (!("WebSocket" in window)) WebSocket = MozWebSocket;
        var socket = new WebSocket("ws://localhost:8080/websocket/echo");

        socket.onopen = function (event) {
            var newMessage = document.createElement('p');
            newMessage.textContent = "-- CONNECTED";
            document.getElementById('output').appendChild(newMessage);

            socket.onmessage = function (e) {
                var newMessage = document.createElement('p');
                newMessage.textContent = "<< SERVER: " + e.data;
                document.getElementById('output').appendChild(newMessage);
            }

            $("#button").click(function (e) {
                e.preventDefault();
                var message = $("#input").val();
                socket.send(message);
                var newMessage = document.createElement('p');
                newMessage.textContent = ">> CLIENT: " + message;
                document.getElementById('output').appendChild(newMessage);
            });
        }
    });
</script>
</body>
</html>

Integration tests with WebSocketClient

WebSocketClient included in Spring WebFlux can be used to test your WebSocket endpoints.

You can check that your WebSocket endpoint, created in the previous section, is working properly with the following integration test:

trading-service/src/test/java/io/spring/workshop/tradingservice/websocket/EchoWebSocketHandlerTests.java
package io.spring.workshop.tradingservice.websocket;

import static org.junit.Assert.assertEquals;

import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.client.StandardWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;

import reactor.core.publisher.Flux;
import reactor.core.publisher.ReplayProcessor;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class EchoWebSocketHandlerTests {

	@LocalServerPort
	private String port;

	@Test
	public void echo() throws Exception {
		int count = 4;
		Flux<String> input = Flux.range(1, count).map(index -> "msg-" + index);
		ReplayProcessor<Object> output = ReplayProcessor.create(count);

		WebSocketClient client = new StandardWebSocketClient();
		client.execute(getUrl("/websocket/echo"),
				session -> session
						.send(input.map(session::textMessage))
						.thenMany(session.receive().take(count).map(WebSocketMessage::getPayloadAsText))
						.subscribeWith(output)
						.then())
				.block(Duration.ofMillis(5000));

		assertEquals(input.collectList().block(Duration.ofMillis(5000)), output.collectList().block(Duration.ofMillis(5000)));
	}

	protected URI getUrl(String path) throws URISyntaxException {
		return new URI("ws://localhost:" + this.port + path);
	}
}

Additional Resources