Spring Boot WebFlux + Thymeleaf reactive example
In this article, we will show you how to develop a reactive web application.
- Spring Boot 2.1.2.RELEASE
- Spring WebFlux 5.1.4.RELEASE
- Thymeleaf 3.0.11.RELEASE
- Maven 3
Spring Boot will configure everything, and the key is using the Thymeleaf ReactiveDataDriverContextVariable
to enable the data-driven mode in Thymeleaf template.
@RequestMapping("/")
public String index(final Model model) {
// data streaming, data driven mode.
IReactiveDataDriverContextVariable reactiveDataDrivenMode =
new ReactiveDataDriverContextVariable(movieRepository.findAll(), 1);
model.addAttribute("movies", reactiveDataDrivenMode);
return "view";
}
Note
Working on the Spring Boot WebFlux + Thymeleaf + Server-Sent Events (SSE) integeration. To be updated here.
Working on the Spring Boot WebFlux + Thymeleaf + Server-Sent Events (SSE) integeration. To be updated here.
1. Project Directory
2. Maven
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mkyong.spring.reactive</groupId>
<artifactId>webflux-thymeleaf</artifactId>
<version>1.0</version>
<properties>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
</parent>
<dependencies>
<!-- reactive -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- just include the normal thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Display the project dependencies.
$ mvn dependency:tree
[INFO] com.mkyong.spring.webflux:ReactiveApp:jar:1.0-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-webflux:jar:2.1.2.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.1.2.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot:jar:2.1.2.RELEASE:compile
[INFO] | | | \- org.springframework:spring-context:jar:5.1.4.RELEASE:compile
[INFO] | | | +- org.springframework:spring-aop:jar:5.1.4.RELEASE:compile
[INFO] | | | \- org.springframework:spring-expression:jar:5.1.4.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.1.2.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:2.1.2.RELEASE:compile
[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] | | | | \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.11.1:compile
[INFO] | | | | \- org.apache.logging.log4j:log4j-api:jar:2.11.1:compile
[INFO] | | | \- org.slf4j:jul-to-slf4j:jar:1.7.25:compile
[INFO] | | +- javax.annotation:javax.annotation-api:jar:1.3.2:compile
[INFO] | | \- org.yaml:snakeyaml:jar:1.23:runtime
[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:2.1.2.RELEASE:compile
[INFO] | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.9.8:compile
[INFO] | | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.9.0:compile
[INFO] | | | \- com.fasterxml.jackson.core:jackson-core:jar:2.9.8:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.9.8:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.9.8:compile
[INFO] | | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.9.8:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-reactor-netty:jar:2.1.2.RELEASE:compile
[INFO] | | \- io.projectreactor.netty:reactor-netty:jar:0.8.4.RELEASE:compile
[INFO] | | +- io.netty:netty-codec-http:jar:4.1.31.Final:compile
[INFO] | | | \- io.netty:netty-codec:jar:4.1.31.Final:compile
[INFO] | | +- io.netty:netty-codec-http2:jar:4.1.31.Final:compile
[INFO] | | +- io.netty:netty-handler:jar:4.1.31.Final:compile
[INFO] | | | +- io.netty:netty-buffer:jar:4.1.31.Final:compile
[INFO] | | | \- io.netty:netty-transport:jar:4.1.31.Final:compile
[INFO] | | | \- io.netty:netty-resolver:jar:4.1.31.Final:compile
[INFO] | | +- io.netty:netty-handler-proxy:jar:4.1.31.Final:compile
[INFO] | | | \- io.netty:netty-codec-socks:jar:4.1.31.Final:compile
[INFO] | | \- io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.31.Final:compile
[INFO] | | +- io.netty:netty-common:jar:4.1.31.Final:compile
[INFO] | | \- io.netty:netty-transport-native-unix-common:jar:4.1.31.Final:compile
[INFO] | +- org.hibernate.validator:hibernate-validator:jar:6.0.14.Final:compile
[INFO] | | +- javax.validation:validation-api:jar:2.0.1.Final:compile
[INFO] | | +- org.jboss.logging:jboss-logging:jar:3.3.2.Final:compile
[INFO] | | \- com.fasterxml:classmate:jar:1.4.0:compile
[INFO] | +- org.springframework:spring-web:jar:5.1.4.RELEASE:compile
[INFO] | | \- org.springframework:spring-beans:jar:5.1.4.RELEASE:compile
[INFO] | +- org.springframework:spring-webflux:jar:5.1.4.RELEASE:compile
[INFO] | | \- io.projectreactor:reactor-core:jar:3.2.5.RELEASE:compile
[INFO] | | \- org.reactivestreams:reactive-streams:jar:1.0.2:compile
[INFO] | \- org.synchronoss.cloud:nio-multipart-parser:jar:1.1.0:compile
[INFO] | +- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] | \- org.synchronoss.cloud:nio-stream-storage:jar:1.1.3:compile
[INFO] +- org.springframework.boot:spring-boot-starter-thymeleaf:jar:2.1.2.RELEASE:compile
[INFO] | +- org.thymeleaf:thymeleaf-spring5:jar:3.0.11.RELEASE:compile
[INFO] | | \- org.thymeleaf:thymeleaf:jar:3.0.11.RELEASE:compile
[INFO] | | +- org.attoparser:attoparser:jar:2.0.5.RELEASE:compile
[INFO] | | \- org.unbescape:unbescape:jar:1.1.6.RELEASE:compile
[INFO] | \- org.thymeleaf.extras:thymeleaf-extras-java8time:jar:3.0.2.RELEASE:compile
[INFO] \- org.springframework.boot:spring-boot-starter-test:jar:2.1.2.RELEASE:test
[INFO] +- org.springframework.boot:spring-boot-test:jar:2.1.2.RELEASE:test
[INFO] +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.1.2.RELEASE:test
[INFO] +- com.jayway.jsonpath:json-path:jar:2.4.0:test
[INFO] | \- net.minidev:json-smart:jar:2.3:test
[INFO] | \- net.minidev:accessors-smart:jar:1.2:test
[INFO] | \- org.ow2.asm:asm:jar:5.0.4:test
[INFO] +- junit:junit:jar:4.12:test
[INFO] +- org.assertj:assertj-core:jar:3.11.1:test
[INFO] +- org.mockito:mockito-core:jar:2.23.4:test
[INFO] | +- net.bytebuddy:byte-buddy:jar:1.9.7:test
[INFO] | +- net.bytebuddy:byte-buddy-agent:jar:1.9.7:test
[INFO] | \- org.objenesis:objenesis:jar:2.6:test
[INFO] +- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] +- org.hamcrest:hamcrest-library:jar:1.3:test
[INFO] +- org.skyscreamer:jsonassert:jar:1.5.0:test
[INFO] | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] +- org.springframework:spring-core:jar:5.1.4.RELEASE:compile
[INFO] | \- org.springframework:spring-jcl:jar:5.1.4.RELEASE:compile
[INFO] +- org.springframework:spring-test:jar:5.1.4.RELEASE:test
[INFO] \- org.xmlunit:xmlunit-core:jar:2.6.2:test
[INFO] \- javax.xml.bind:jaxb-api:jar:2.3.1:test
[INFO] \- javax.activation:javax.activation-api:jar:1.2.0:test
3. Spring Boot + Spring WebFlux
3.1 Spring WebFlux annotation based controller. Wraps the data with ReactiveDataDriverContextVariable
, it will enable the reactive data-driven model in Thymeleaf template.
MovieController.java
package com.mkyong.reactive.controller;
import com.mkyong.reactive.repository.MovieRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.thymeleaf.spring5.context.webflux.IReactiveDataDriverContextVariable;
import org.thymeleaf.spring5.context.webflux.ReactiveDataDriverContextVariable;
@Controller
public class MovieController {
@Autowired
private MovieRepository movieRepository;
@RequestMapping("/")
public String index(final Model model) {
// loads 1 and display 1, stream data, data driven mode.
IReactiveDataDriverContextVariable reactiveDataDrivenMode =
new ReactiveDataDriverContextVariable(movieRepository.findAll(), 1);
model.addAttribute("movies", reactiveDataDrivenMode);
// classic, wait repository loaded all and display it.
//model.addAttribute("movies", movieRepository.findAll());
return "index";
}
}
3.2 In repository
, return a Flux
object.
MovieRepository.java
package com.mkyong.reactive.repository;
import com.mkyong.reactive.Movie;
import reactor.core.publisher.Flux;
public interface MovieRepository {
Flux<Movie> findAll();
}
ReactiveMovieRepository.java
package com.mkyong.reactive.repository;
import com.mkyong.reactive.Movie;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Repository
public class ReactiveMovieRepository implements MovieRepository {
private static List<Movie> movie = new ArrayList<>();
static {
movie.add(new Movie("Polar (2019)", 64));
movie.add(new Movie("Iron Man (2008)", 79));
movie.add(new Movie("The Shawshank Redemption (1994)", 93));
movie.add(new Movie("Forrest Gump (1994)", 83));
movie.add(new Movie("Glass (2019)", 70));
}
@Override
public Flux<Movie> findAll() {
//Simulate big list of data, streaming it every 2 second delay
return Flux.fromIterable(movie).delayElements(Duration.ofSeconds(2));
}
}
3.3 Movie model.
Movie.java
package com.mkyong.reactive;
public class Movie {
private String name;
private Integer score;
//getter, setter and constructor
}
3.4 Start Spring Boot.
MovieWebApplication.java
package com.mkyong.reactive;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MovieWebApplication {
public static void main(String[] args) {
SpringApplication.run(MovieWebApplication.class, args);
}
}
4. Thymeleaf
There is no special reactive tag in thymeleaf template, just uses the normal loop.
resources/templates/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link data-th-href="@{/css/bootstrap.min.css}" rel="stylesheet">
<link data-th-href="@{/css/main.css}" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div id="title">
<h1>Spring WebFlux + Thymeleaf</h1>
</div>
<table id="allMovies" class="table table-striped">
<thead>
<tr>
<th width="70%">Name</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr class="result" data-th-each="movie : ${movies}">
<td>[[${movie.name}]]</td>
<td>[[${movie.score}]]</td>
</tr>
</tbody>
</table>
</div>
</div>
Defined a chunk size.
application.properties
spring.thymeleaf.reactive.max-chunk-size=8192
Done.
5.Demo
$ mvn spring-boot:run
URL = http://localhost:8080
The data are streaming, and will be displayed every 2 seconds, in a reactive way.
Hello MKyong, thanks a lot for your tutorials, well I try to reply this example but I have a problem on the view when the object IReactiveDataDriverContextVariable try to access to a property of my list of model, the problem says:
org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field ‘myproperty’ cannot be found on object of type ‘org.thymeleaf.spring5.context.webflux.ReactiveDataDriverContextVariable’ – maybe not public or not valid?
Do yo have any idea whats that?
That might be a problem of having hateoas in your dependencies, it includes springweb dependency, which messes with reactor functionality and shows that kind of error in Thymeleaf, at least it is what happened to me. When including hateoas in your dependencies “exclude” spring web.
Change main class
SpringApplication application = new SpringApplication(YourApplication.class);
application.setWebApplicationType(WebApplicationType.REACTIVE);
application.run(args);
Here is only about Flux what about Mono?
@mkyong what about the save.
Excellent example introducing reactive aproach in Spring Boot. Thank you very much