515 lines
18 KiB
Plaintext
515 lines
18 KiB
Plaintext
[[webflux-view]]
|
|
= View Technologies
|
|
[.small]#xref:web/webmvc-view.adoc[See equivalent in the Servlet stack]#
|
|
|
|
The rendering of views in Spring WebFlux is pluggable. Whether you decide to
|
|
use Thymeleaf, FreeMarker, or some other view technology is primarily a matter of a
|
|
configuration change. This chapter covers the view technologies integrated with Spring
|
|
WebFlux.
|
|
|
|
For more context on view rendering, please see xref:web/webflux/dispatcher-handler.adoc#webflux-viewresolution[View Resolution].
|
|
|
|
WARNING: The views of a Spring WebFlux application live within internal trust boundaries
|
|
of the application. Views have access to beans in the application context, and as
|
|
such, we do not recommend use the Spring WebFlux template support in applications where
|
|
the templates are editable by external sources, since this can have security implications.
|
|
|
|
|
|
|
|
|
|
[[webflux-view-thymeleaf]]
|
|
== Thymeleaf
|
|
[.small]#xref:web/webmvc-view/mvc-thymeleaf.adoc[See equivalent in the Servlet stack]#
|
|
|
|
Thymeleaf is a modern server-side Java template engine that emphasizes natural HTML
|
|
templates that can be previewed in a browser by double-clicking, which is very
|
|
helpful for independent work on UI templates (for example, by a designer) without the need for a
|
|
running server. Thymeleaf offers an extensive set of features, and it is actively developed
|
|
and maintained. For a more complete introduction, see the
|
|
https://www.thymeleaf.org/[Thymeleaf] project home page.
|
|
|
|
The Thymeleaf integration with Spring WebFlux is managed by the Thymeleaf project. The
|
|
configuration involves a few bean declarations, such as
|
|
`SpringResourceTemplateResolver`, `SpringWebFluxTemplateEngine`, and
|
|
`ThymeleafReactiveViewResolver`. For more details, see
|
|
https://www.thymeleaf.org/documentation.html[Thymeleaf+Spring] and the WebFlux integration
|
|
https://web.archive.org/web/20210623051330/http%3A//forum.thymeleaf.org/Thymeleaf-3-0-8-JUST-PUBLISHED-td4030687.html[announcement].
|
|
|
|
|
|
|
|
|
|
[[webflux-view-freemarker]]
|
|
== FreeMarker
|
|
[.small]#xref:web/webmvc-view/mvc-freemarker.adoc[See equivalent in the Servlet stack]#
|
|
|
|
https://freemarker.apache.org/[Apache FreeMarker] is a template engine for generating any
|
|
kind of text output from HTML to email and others. The Spring Framework has built-in
|
|
integration for using Spring WebFlux with FreeMarker templates.
|
|
|
|
|
|
|
|
[[webflux-view-freemarker-contextconfig]]
|
|
=== View Configuration
|
|
[.small]#xref:web/webmvc-view/mvc-freemarker.adoc#mvc-view-freemarker-contextconfig[See equivalent in the Servlet stack]#
|
|
|
|
The following example shows how to configure FreeMarker as a view technology:
|
|
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebFlux
|
|
public class WebConfig implements WebFluxConfigurer {
|
|
|
|
@Override
|
|
public void configureViewResolvers(ViewResolverRegistry registry) {
|
|
registry.freeMarker();
|
|
}
|
|
|
|
// Configure FreeMarker...
|
|
|
|
@Bean
|
|
public FreeMarkerConfigurer freeMarkerConfigurer() {
|
|
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
|
configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
|
|
return configurer;
|
|
}
|
|
}
|
|
----
|
|
|
|
Kotlin::
|
|
+
|
|
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebFlux
|
|
class WebConfig : WebFluxConfigurer {
|
|
|
|
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
|
registry.freeMarker()
|
|
}
|
|
|
|
// Configure FreeMarker...
|
|
|
|
@Bean
|
|
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
|
|
setTemplateLoaderPath("classpath:/templates/freemarker")
|
|
}
|
|
}
|
|
----
|
|
======
|
|
|
|
Your templates need to be stored in the directory specified by the `FreeMarkerConfigurer`,
|
|
shown in the preceding example. Given the preceding configuration, if your controller
|
|
returns the view name, `welcome`, the resolver looks for the
|
|
`classpath:/templates/freemarker/welcome.ftl` template.
|
|
|
|
|
|
|
|
[[webflux-views-freemarker]]
|
|
=== FreeMarker Configuration
|
|
[.small]#xref:web/webmvc-view/mvc-freemarker.adoc#mvc-views-freemarker[See equivalent in the Servlet stack]#
|
|
|
|
You can pass FreeMarker 'Settings' and 'SharedVariables' directly to the FreeMarker
|
|
`Configuration` object (which is managed by Spring) by setting the appropriate bean
|
|
properties on the `FreeMarkerConfigurer` bean. The `freemarkerSettings` property requires
|
|
a `java.util.Properties` object, and the `freemarkerVariables` property requires a
|
|
`java.util.Map`. The following example shows how to use a `FreeMarkerConfigurer`:
|
|
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebFlux
|
|
public class WebConfig implements WebFluxConfigurer {
|
|
|
|
// ...
|
|
|
|
@Bean
|
|
public FreeMarkerConfigurer freeMarkerConfigurer() {
|
|
Map<String, Object> variables = new HashMap<>();
|
|
variables.put("xml_escape", new XmlEscape());
|
|
|
|
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
|
configurer.setTemplateLoaderPath("classpath:/templates");
|
|
configurer.setFreemarkerVariables(variables);
|
|
return configurer;
|
|
}
|
|
}
|
|
----
|
|
|
|
Kotlin::
|
|
+
|
|
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebFlux
|
|
class WebConfig : WebFluxConfigurer {
|
|
|
|
// ...
|
|
|
|
@Bean
|
|
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
|
|
setTemplateLoaderPath("classpath:/templates")
|
|
setFreemarkerVariables(mapOf("xml_escape" to XmlEscape()))
|
|
}
|
|
}
|
|
----
|
|
======
|
|
|
|
See the FreeMarker documentation for details of settings and variables as they apply to
|
|
the `Configuration` object.
|
|
|
|
|
|
|
|
[[webflux-view-freemarker-forms]]
|
|
=== Form Handling
|
|
[.small]#xref:web/webmvc-view/mvc-freemarker.adoc#mvc-view-freemarker-forms[See equivalent in the Servlet stack]#
|
|
|
|
Spring provides a tag library for use in JSPs that contains, among others, a
|
|
`<spring:bind/>` element. This element primarily lets forms display values from
|
|
form-backing objects and show the results of failed validations from a `Validator` in the
|
|
web or business tier. Spring also has support for the same functionality in FreeMarker,
|
|
with additional convenience macros for generating form input elements themselves.
|
|
|
|
|
|
[[webflux-view-bind-macros]]
|
|
==== The Bind Macros
|
|
[.small]#xref:web/webmvc-view/mvc-freemarker.adoc#mvc-view-bind-macros[See equivalent in the Servlet stack]#
|
|
|
|
A standard set of macros are maintained within the `spring-webflux.jar` file for
|
|
FreeMarker, so they are always available to a suitably configured application.
|
|
|
|
Some of the macros defined in the Spring templating libraries are considered internal
|
|
(private), but no such scoping exists in the macro definitions, making all macros visible
|
|
to calling code and user templates. The following sections concentrate only on the macros
|
|
you need to directly call from within your templates. If you wish to view the macro code
|
|
directly, the file is called `spring.ftl` and is in the
|
|
`org.springframework.web.reactive.result.view.freemarker` package.
|
|
|
|
For additional details on binding support, see xref:web/webmvc-view/mvc-freemarker.adoc#mvc-view-simple-binding[Simple Binding]
|
|
for Spring MVC.
|
|
|
|
|
|
[[webflux-views-form-macros]]
|
|
==== Form Macros
|
|
|
|
For details on Spring's form macro support for FreeMarker templates, consult the following
|
|
sections of the Spring MVC documentation.
|
|
|
|
* xref:web/webmvc-view/mvc-freemarker.adoc#mvc-views-form-macros[Input Macros]
|
|
* xref:web/webmvc-view/mvc-freemarker.adoc#mvc-views-form-macros-input[Input Fields]
|
|
* xref:web/webmvc-view/mvc-freemarker.adoc#mvc-views-form-macros-select[Selection Fields]
|
|
* xref:web/webmvc-view/mvc-freemarker.adoc#mvc-views-form-macros-html-escaping[HTML Escaping]
|
|
|
|
|
|
|
|
[[webflux-view-script]]
|
|
== Script Views
|
|
[.small]#xref:web/webmvc-view/mvc-script.adoc[See equivalent in the Servlet stack]#
|
|
|
|
The Spring Framework has a built-in integration for using Spring WebFlux with any
|
|
templating library that can run on top of the
|
|
{JSR}223[JSR-223] Java scripting engine.
|
|
The following table shows the templating libraries that we have tested on different script engines:
|
|
|
|
[%header]
|
|
|===
|
|
|Scripting Library |Scripting Engine
|
|
|https://handlebarsjs.com/[Handlebars] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
|
|https://mustache.github.io/[Mustache] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
|
|https://facebook.github.io/react/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
|
|https://www.embeddedjs.com/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
|
|https://www.stuartellis.name/articles/erb/[ERB] |https://www.jruby.org[JRuby]
|
|
|https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython]
|
|
|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin]
|
|
|===
|
|
|
|
TIP: The basic rule for integrating any other script engine is that it must implement the
|
|
`ScriptEngine` and `Invocable` interfaces.
|
|
|
|
|
|
|
|
[[webflux-view-script-dependencies]]
|
|
=== Requirements
|
|
[.small]#xref:web/webmvc-view/mvc-script.adoc#mvc-view-script-dependencies[See equivalent in the Servlet stack]#
|
|
|
|
You need to have the script engine on your classpath, the details of which vary by script engine:
|
|
|
|
* The https://openjdk.java.net/projects/nashorn/[Nashorn] JavaScript engine is provided with
|
|
Java 8+. Using the latest update release available is highly recommended.
|
|
* https://www.jruby.org[JRuby] should be added as a dependency for Ruby support.
|
|
* https://www.jython.org[Jython] should be added as a dependency for Python support.
|
|
* `org.jetbrains.kotlin:kotlin-script-util` dependency and a `META-INF/services/javax.script.ScriptEngineFactory`
|
|
file containing a `org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory`
|
|
line should be added for Kotlin script support. See
|
|
https://github.com/sdeleuze/kotlin-script-templating[this example] for more detail.
|
|
|
|
You need to have the script templating library. One way to do that for JavaScript is
|
|
through https://www.webjars.org/[WebJars].
|
|
|
|
|
|
|
|
[[webflux-view-script-integrate]]
|
|
=== Script Templates
|
|
[.small]#xref:web/webmvc-view/mvc-script.adoc#mvc-view-script-integrate[See equivalent in the Servlet stack]#
|
|
|
|
You can declare a `ScriptTemplateConfigurer` bean to specify the script engine to use,
|
|
the script files to load, what function to call to render templates, and so on.
|
|
The following example uses Mustache templates and the Nashorn JavaScript engine:
|
|
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebFlux
|
|
public class WebConfig implements WebFluxConfigurer {
|
|
|
|
@Override
|
|
public void configureViewResolvers(ViewResolverRegistry registry) {
|
|
registry.scriptTemplate();
|
|
}
|
|
|
|
@Bean
|
|
public ScriptTemplateConfigurer configurer() {
|
|
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
|
|
configurer.setEngineName("nashorn");
|
|
configurer.setScripts("mustache.js");
|
|
configurer.setRenderObject("Mustache");
|
|
configurer.setRenderFunction("render");
|
|
return configurer;
|
|
}
|
|
}
|
|
----
|
|
|
|
Kotlin::
|
|
+
|
|
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebFlux
|
|
class WebConfig : WebFluxConfigurer {
|
|
|
|
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
|
registry.scriptTemplate()
|
|
}
|
|
|
|
@Bean
|
|
fun configurer() = ScriptTemplateConfigurer().apply {
|
|
engineName = "nashorn"
|
|
setScripts("mustache.js")
|
|
renderObject = "Mustache"
|
|
renderFunction = "render"
|
|
}
|
|
}
|
|
----
|
|
======
|
|
|
|
The `render` function is called with the following parameters:
|
|
|
|
* `String template`: The template content
|
|
* `Map model`: The view model
|
|
* `RenderingContext renderingContext`: The
|
|
{spring-framework-api}/web/servlet/view/script/RenderingContext.html[`RenderingContext`]
|
|
that gives access to the application context, the locale, the template loader, and the
|
|
URL (since 5.0)
|
|
|
|
`Mustache.render()` is natively compatible with this signature, so you can call it directly.
|
|
|
|
If your templating technology requires some customization, you can provide a script that
|
|
implements a custom render function. For example, https://handlebarsjs.com[Handlerbars]
|
|
needs to compile templates before using them and requires a
|
|
https://en.wikipedia.org/wiki/Polyfill[polyfill] in order to emulate some
|
|
browser facilities not available in the server-side script engine.
|
|
The following example shows how to set a custom render function:
|
|
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebFlux
|
|
public class WebConfig implements WebFluxConfigurer {
|
|
|
|
@Override
|
|
public void configureViewResolvers(ViewResolverRegistry registry) {
|
|
registry.scriptTemplate();
|
|
}
|
|
|
|
@Bean
|
|
public ScriptTemplateConfigurer configurer() {
|
|
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
|
|
configurer.setEngineName("nashorn");
|
|
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
|
|
configurer.setRenderFunction("render");
|
|
configurer.setSharedEngine(false);
|
|
return configurer;
|
|
}
|
|
}
|
|
----
|
|
|
|
Kotlin::
|
|
+
|
|
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebFlux
|
|
class WebConfig : WebFluxConfigurer {
|
|
|
|
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
|
registry.scriptTemplate()
|
|
}
|
|
|
|
@Bean
|
|
fun configurer() = ScriptTemplateConfigurer().apply {
|
|
engineName = "nashorn"
|
|
setScripts("polyfill.js", "handlebars.js", "render.js")
|
|
renderFunction = "render"
|
|
isSharedEngine = false
|
|
}
|
|
}
|
|
----
|
|
======
|
|
|
|
NOTE: Setting the `sharedEngine` property to `false` is required when using non-thread-safe
|
|
script engines with templating libraries not designed for concurrency, such as Handlebars or
|
|
React running on Nashorn. In that case, Java SE 8 update 60 is required, due to
|
|
https://bugs.openjdk.java.net/browse/JDK-8076099[this bug], but it is generally
|
|
recommended to use a recent Java SE patch release in any case.
|
|
|
|
`polyfill.js` defines only the `window` object needed by Handlebars to run properly,
|
|
as the following snippet shows:
|
|
|
|
[source,javascript,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
var window = {};
|
|
----
|
|
|
|
This basic `render.js` implementation compiles the template before using it. A production
|
|
ready implementation should also store and reused cached templates or pre-compiled templates.
|
|
This can be done on the script side, as well as any customization you need (managing
|
|
template engine configuration for example).
|
|
The following example shows how compile a template:
|
|
|
|
[source,javascript,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
function render(template, model) {
|
|
var compiledTemplate = Handlebars.compile(template);
|
|
return compiledTemplate(model);
|
|
}
|
|
----
|
|
|
|
Check out the Spring Framework unit tests,
|
|
{spring-framework-code}/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script[Java], and
|
|
{spring-framework-code}/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script[resources],
|
|
for more configuration examples.
|
|
|
|
|
|
|
|
|
|
[[webflux-view-fragments]]
|
|
== HTML Fragment
|
|
[.small]#xref:web/webmvc-view/mvc-fragments.adoc[See equivalent in the Servlet stack]#
|
|
|
|
https://htmx.org/[HTMX] and https://turbo.hotwired.dev/[Hotwire Turbo] emphasize an
|
|
HTML-over-the-wire approach where clients receive server updates in HTML rather than in JSON.
|
|
This allows the benefits of an SPA (single page app) without having to write much or even
|
|
any JavaScript. For a good overview and to learn more, please visit their respective
|
|
websites.
|
|
|
|
In Spring WebFlux, view rendering typically involves specifying one view and one model.
|
|
However, in HTML-over-the-wire a common capability is to send multiple HTML fragments that
|
|
the browser can use to update different parts of the page. For this, controller methods
|
|
can return `Collection<Fragment>`. For example:
|
|
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@GetMapping
|
|
List<Fragment> handle() {
|
|
return List.of(Fragment.create("posts"), Fragment.create("comments"));
|
|
}
|
|
----
|
|
|
|
Kotlin::
|
|
+
|
|
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@GetMapping
|
|
fun handle(): List<Fragment> {
|
|
return listOf(Fragment.create("posts"), Fragment.create("comments"))
|
|
}
|
|
----
|
|
======
|
|
|
|
The same can be done also by returning the dedicated type `FragmentsRendering`:
|
|
|
|
[tabs]
|
|
======
|
|
Java::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@GetMapping
|
|
FragmentsRendering handle() {
|
|
return FragmentsRendering.fragment("posts").fragment("comments").build();
|
|
}
|
|
----
|
|
|
|
Kotlin::
|
|
+
|
|
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
@GetMapping
|
|
fun handle(): FragmentsRendering {
|
|
return FragmentsRendering.fragment("posts").fragment("comments").build()
|
|
}
|
|
----
|
|
======
|
|
|
|
Each fragment can have an independent model, and that model inherits attributes from the
|
|
shared model for the request.
|
|
|
|
HTMX and Hotwire Turbo support streaming updates over SSE (server-sent events).
|
|
A controller can create `FragmentsRendering` with a `Flux<Fragment>`, or with any other
|
|
reactive producer adaptable to a Reactive Streams `Publisher` via `ReactiveAdapterRegistry`.
|
|
It is also possible to return `Flux<Fragment>` directly without the `FragmentsRendering`
|
|
wrapper.
|
|
|
|
|
|
|
|
|
|
[[webflux-view-httpmessagewriter]]
|
|
== JSON and XML
|
|
[.small]#xref:web/webmvc-view/mvc-jackson.adoc[See equivalent in the Servlet stack]#
|
|
|
|
For xref:web/webflux/dispatcher-handler.adoc#webflux-multiple-representations[Content Negotiation] purposes, it is useful to be able to alternate
|
|
between rendering a model with an HTML template or as other formats (such as JSON or XML),
|
|
depending on the content type requested by the client. To support doing so, Spring WebFlux
|
|
provides the `HttpMessageWriterView`, which you can use to plug in any of the available
|
|
xref:web/webflux/reactive-spring.adoc#webflux-codecs[Codecs] from `spring-web`, such as `Jackson2JsonEncoder`, `Jackson2SmileEncoder`,
|
|
or `Jaxb2XmlEncoder`.
|
|
|
|
Unlike other view technologies, `HttpMessageWriterView` does not require a `ViewResolver`
|
|
but is instead xref:web/webflux/config.adoc#webflux-config-view-resolvers[configured] as a default view. You can
|
|
configure one or more such default views, wrapping different `HttpMessageWriter` instances
|
|
or `Encoder` instances. The one that matches the requested content type is used at runtime.
|
|
|
|
In most cases, a model contains multiple attributes. To determine which one to serialize,
|
|
you can configure `HttpMessageWriterView` with the name of the model attribute to use for
|
|
rendering. If the model contains only one attribute, that one is used.
|