You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
514 lines
18 KiB
514 lines
18 KiB
[[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.
|
|
|