View and View Resolvers
Like the traditional Spring MVC, Spring WebFlux also includes the capability to rendering views by the integrated template engine.
ThymeLeaf
Thymeleaf is the most popular template engine in Spring ecosystem.
Add thymeleaf related dependencies.
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
Configure Thymeleaf in a Spring web application.
@Configuration
@EnableWebFlux
class WebConfig implements ApplicationContextAware, WebFluxConfigurer {
private ApplicationContext ctx;
@Override
public void setApplicationContext(ApplicationContext context) {
this.ctx = context;
}
@Bean
public SpringResourceTemplateResolver thymeleafTemplateResolver() {
final SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setApplicationContext(this.ctx);
resolver.setPrefix("classpath:/templates/");
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCacheable(false);
resolver.setCheckExistence(false);
return resolver;
}
@Bean
public ISpringWebFluxTemplateEngine thymeleafTemplateEngine() {
// We override here the SpringTemplateEngine instance that would otherwise be
// instantiated by
// Spring Boot because we want to apply the SpringWebFlux-specific context
// factory, link builder...
final SpringWebFluxTemplateEngine templateEngine = new SpringWebFluxTemplateEngine();
templateEngine.setTemplateResolver(thymeleafTemplateResolver());
return templateEngine;
}
@Bean
public ThymeleafReactiveViewResolver thymeleafChunkedAndDataDrivenViewResolver() {
final ThymeleafReactiveViewResolver viewResolver = new ThymeleafReactiveViewResolver();
viewResolver.setTemplateEngine(thymeleafTemplateEngine());
// viewResolver.setOrder(1);
// viewResolver.setViewNames(new String[]{"home"});
viewResolver.setResponseMaxChunkSizeBytes(8192); // OUTPUT BUFFER size limit
return viewResolver;
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.viewResolver(thymeleafChunkedAndDataDrivenViewResolver());
}
}
Thymeleaf provides reactive support in the template engine and allow you render a Flux stream dynamically.
An example of Thymeleaf template.
<!DOCTYPE html>
<html>
<head>
<title>Simple Blog Posts</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>All posts</h1>
<div>
<table>
<thead>
<tr>
<th> ID</th>
<th>Title </th>
<th>Content</th>
</tr>
</thead>
<tbody>
<tr th:each="e : ${posts}">
<td th:text="${e.id}"></td>
<td th:text="${e.title}"></td>
<td th:text="${e.content}"></td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
Source codes: spring-reactive-sample/mvc-thymeleaf and spring-reactive-sample/boot-mvc-thymeleaf .
Freemarker
Spring 5 provides built-in support to Freemarker template engine.
Add Freemarker dependencies.
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
Add Freemarker configuration.
@Configuration
@EnableWebFlux
class WebConfig implements ApplicationContextAware, WebFluxConfigurer {
private ApplicationContext ctx;
@Override
public void setApplicationContext(ApplicationContext context) {
this.ctx = context;
}
@Bean
public FreeMarkerConfigurer freeMarkerConfig() {
Map<String, Object> variables = new HashMap<>();
variables.put("xml_escape", new XmlEscape());
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setPreferFileSystemAccess(false);
configurer.setTemplateLoaderPath("classpath:/templates");
configurer.setResourceLoader(this.ctx);
configurer.setFreemarkerVariables(variables);
return configurer;
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
}
An example of Freemarker template.
<!DOCTYPE html>
<html>
<head>
<title>Simple Blog Posts</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<h1>All posts</h1>
<div>
<table>
<thead>
<tr>
<th> ID</th>
<th>Title </th>
<th>Content</th>
</tr>
</thead>
<tbody>
<#list posts as post>
<tr>
<td>${post.id}</td>
<td>${post.title}</td>
<td>${post.content}</td>
</tr>
<#else>
nothing
</#list>
</tbody>
</table>
</div>
</body>
</html>
Source codes: spring-reactive-sample/mvc-freemarker and spring-reactive-sample/boot-mvc-freemarker .
Mustache
Mustache is a popular template engine in the Ruby and NodeJS world, it also provides Java implementation.
Spring framework does not integrate Mustache as Freemarker. But you can create your own Veiw
and ViewResolver
to adapt the Mustache compiler to the Spring world.
Add Mustache java into dependencies, we use jmustache
here.
<dependency>
<groupId>com.samskivert</groupId>
<artifactId>jmustache</artifactId>
</dependency>
Then create a MustacheView
to render the tempalte.
public class MustacheView extends AbstractUrlBasedView {
private Compiler compiler;
private String charset;
/**
* Set the JMustache compiler to be used by this view. Typically this property is not
* set directly. Instead a single {@link Compiler} is expected in the Spring
* application context which is used to compile Mustache templates.
* @param compiler the Mustache compiler
*/
public void setCompiler(Compiler compiler) {
this.compiler = compiler;
}
/**
* Set the charset used for reading Mustache template files.
* @param charset the charset to use for reading template files
*/
public void setCharset(String charset) {
this.charset = charset;
}
@Override
public boolean checkResourceExists(Locale locale) throws Exception {
return resolveResource() != null;
}
@Override
protected Mono<Void> renderInternal(Map<String, Object> model, MediaType contentType,
ServerWebExchange exchange) {
Resource resource = resolveResource();
if (resource == null) {
return Mono.error(new IllegalStateException(
"Could not find Mustache template with URL [" + getUrl() + "]"));
}
DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer();
try (Reader reader = getReader(resource)) {
Template template = this.compiler.compile(reader);
Charset charset = getCharset(contentType).orElse(getDefaultCharset());
try (Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream(),
charset)) {
template.execute(model, writer);
writer.flush();
}
}
catch (Exception ex) {
DataBufferUtils.release(dataBuffer);
return Mono.error(ex);
}
return exchange.getResponse().writeWith(Flux.just(dataBuffer));
}
private Resource resolveResource() {
Resource resource = getApplicationContext().getResource(getUrl());
if (resource == null || !resource.exists()) {
return null;
}
return resource;
}
private Reader getReader(Resource resource) throws IOException {
if (this.charset != null) {
return new InputStreamReader(resource.getInputStream(), this.charset);
}
return new InputStreamReader(resource.getInputStream());
}
private Optional<Charset> getCharset(MediaType mediaType) {
return Optional.ofNullable(mediaType != null ? mediaType.getCharset() : null);
}
}
Create a MustacheViewResolver
to resolve a view.
public class MustacheViewResolver extends UrlBasedViewResolver {
private final Compiler compiler;
private String charset;
/**
* Create a {@code MustacheViewResolver} backed by a default instance of a
* {@link Compiler}.
*/
public MustacheViewResolver() {
this.compiler = Mustache.compiler();
setViewClass(requiredViewClass());
}
/**
* Create a {@code MustacheViewResolver} backed by a custom instance of a
* {@link Compiler}.
*
* @param compiler the Mustache compiler used to compile templates
*/
public MustacheViewResolver(Compiler compiler) {
this.compiler = compiler;
setViewClass(requiredViewClass());
}
/**
* Set the charset.
*
* @param charset the charset
*/
public void setCharset(String charset) {
this.charset = charset;
}
@Override
protected Class<?> requiredViewClass() {
return MustacheView.class;
}
@Override
protected AbstractUrlBasedView createView(String viewName) {
MustacheView view = (MustacheView) super.createView(viewName);
view.setCompiler(this.compiler);
view.setCharset(this.charset);
return view;
}
}
Configure MustacheViewResolver
.
@Configuration
@EnableWebFlux
class WebConfig implements ApplicationContextAware, WebFluxConfigurer {
private ApplicationContext ctx;
@Override
public void setApplicationContext(ApplicationContext context) {
this.ctx = context;
}
@Bean
public ViewResolver mustacheViewResolver() {
String prefix = "classpath:/templates/";
String suffix = ".mustache";
Mustache.TemplateLoader loader = new MustacheResourceTemplateLoader(prefix, suffix);
MustacheViewResolver mustacheViewResolver = new MustacheViewResolver(Mustache.compiler().withLoader(loader));
mustacheViewResolver.setPrefix(prefix);
mustacheViewResolver.setSuffix(suffix);
return mustacheViewResolver;
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.viewResolver(mustacheViewResolver());
}
}
The MustacheResourceTemplateLoader
is a Spring aware template resource loader.
public class MustacheResourceTemplateLoader implements TemplateLoader, ResourceLoaderAware {
private String suffix = "classpath:templates/";
private String prefix = ".mustache";
private String charset = "UTF-8";
private ResourceLoader resourceLoader = new DefaultResourceLoader();
public MustacheResourceTemplateLoader(String prefix, String suffix) {
this.prefix = prefix;
this.suffix = suffix;
}
/**
* Set the charset.
*
* @param charset the charset
*/
public void setCharset(String charset) {
this.charset = charset;
}
/**
* Set the resource loader.
*
* @param resourceLoader the resource loader
*/
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public Reader getTemplate(String name) throws IOException {
return new InputStreamReader(this.resourceLoader
.getResource(this.prefix + name + this.suffix).getInputStream(),
this.charset);
}
}
An example of Mustache template file.
Source codes: spring-reactive-sample/mvc-mustache and spring-reactive-sample/boot-mvc-mustache .