@Controller("/views")
class ViewsController {
@View("home")
@Get("/")
public HttpResponse<?> index() {
return HttpResponse.ok(CollectionUtils.mapOf("loggedIn", true, "username", "sdelamo"))
}
}
Table of Contents
Micronaut Views
Provides integration between Micronaut and server-side views technologies
Version: 5.6.1-SNAPSHOT
1 Introduction
This project integrates the Micronaut framework and Server Side View Rendering.
2 Release History
For this project, you can find a list of releases (with release notes) here:
3 Server Side View Rendering
Although the Micronaut framework is primarily designed around message encoding / decoding there are occasions where it is convenient to render a view on the server side.
The views
module provides support for view rendering on the server side and does so by rendering views on the I/O thread pool in order to avoid blocking the Netty event loop.
To use the view rendering features described in this section, add a dependency based on the view rendering engine you prefer (see the following sections).
For file-based view schemes, views and templates can be placed in the src/main/resources/views
directory of your
project. If you use this feature and wish to use a different folder, set the property micronaut.views.folder
.
Your controller’s method can render the response with a template by using the View annotation.
The following is an example of a controller which renders a template by passing a model as a java.util.Map
via the returned response object.
1 | Use @View annotation to indicate the view name which should be used to render a view for the route. |
In addition, you can return any POJO object and the properties of the POJO will be exposed to the view for rendering:
@Controller("/views")
class ViewsController {
@View("home")
@Get("/pojo")
public HttpResponse<Person> pojo() {
return HttpResponse.ok(new Person("sdelamo", true))
}
}
1 | Use @View annotation to indicate the view name which should be used to render the POJO responded by the controller. |
Use the @Introspected annotation on your POJO object to generate BeanIntrospection
metadata at compilation time.
You can also return a ModelAndView and skip specifying the View annotation.
@Controller("/views")
class ViewsController {
@Get("/modelAndView")
ModelAndView modelAndView() {
return new ModelAndView("home",
new Person("sdelamo", true))
}
Moreover, the Controller’s method can return a POJO annotated with @Introspected
to the view:
@View("fruits")
@Get
public Fruit index() {
return new Fruit("apple", "red");
}
@View("fruits")
@Get
Fruit index() {
return new Fruit("apple", "red")
}
@View("fruits")
@Get
fun index() = Fruit("apple", "red")
The following sections show different template engines integrations.
To create your own implementation create a class which implements ViewsRenderer and annotate it with @Produces to the media types the view rendering supports producing.
If you want to render a template directly on your code (for example, to generate the body of an email) you can inject the ViewsRenderer bean and use its method "render". |
3.1 Template Engines
Micronaut Views provides out-of-the-box support for multiple template languages.
3.1.1 Thymeleaf
Micronaut Views Thymeleaf includes ThymeleafViewsRenderer which uses the Thymeleaf Java template engine.
Add the micronaut-views-thymeleaf
dependency to your classpath.
implementation("io.micronaut.views:micronaut-views-thymeleaf")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-thymeleaf</artifactId>
</dependency>
Thymeleaf integration instantiates a ClassLoaderTemplateResolver
.
The properties used can be customized by overriding the values of:
Property | Type | Description |
---|---|---|
|
java.lang.String |
The character encoding to use. Default value ("UTF-8"). |
|
org.thymeleaf.templatemode.TemplateMode |
The template mode to be used. |
|
java.lang.String |
The suffix to use. Default value (".html"). |
|
boolean |
Sets whether to force the suffix. Default value (false). |
|
boolean |
Whether to force template mode. Default value (false). |
|
long |
The cache TTL in millis. |
|
boolean |
Whether templates should be checked for existence. |
|
boolean |
Whether templates are cacheable. |
|
boolean |
Whether thymeleaf rendering is enabled. Default value (true). |
|
java.time.Duration |
Sets the cache TTL as a duration. |
The example shown in the Views section, could be rendered with the following Thymeleaf template:
<!DOCTYPE html>
<html lang="en" th:replace="~{layoutFile :: layout(~{::title}, ~{::section})}" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Home</title>
</head>
<body>
<section>
<h1 th:if="${loggedIn}">username: <span th:text="${username}"></span></h1>
<h1 th:unless="${loggedIn}">You are not logged in</h1>
</section>
</body>
</html>
and layout:
<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
<title th:replace="${title}">Layout Title</title>
</head>
<body>
<h1>Layout H1</h1>
<div th:replace="${content}">
<p>Layout content</p>
</div>
<footer>
Layout footer
</footer>
</body>
</html>
Use Thymeleaf templates with Micronaut i18n messages. Internationalization messages will be resolved from Micronaut’s MessageSource automatically. For more information on the MessageSource, see the core documentation.
See the guide for Server-side HTML with Thymeleaf to learn more. |
3.1.2 Handlebars.java
Micronaut Views Handlebars includes HandlebarsViewsRenderer which uses the Handlebars.java project.
Add the following dependency on your classpath. For example, in build.gradle
implementation("io.micronaut.views:micronaut-views-handlebars")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-handlebars</artifactId>
</dependency>
The example shown in the Views section, could be rendered with the following Handlebars template:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
{{#if loggedIn}}
<h1>username: <span>{{username}}</span></h1>
{{else}}
<h1>You are not logged in</h1>
{{/if}}
</body>
</html>
3.1.3 Apache Velocity
Micronaut Views Velocity includes VelocityViewsRenderer which uses the Apache Velocity Java-based template engine.
Add the micronaut-views-velocity
dependency to your classpath.
implementation("io.micronaut.views:micronaut-views-velocity")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-velocity</artifactId>
</dependency>
The example shown in the Views section, could be rendered with the following Velocity template:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
#if( $loggedIn )
<h1>username: <span>$username</span></h1>
#else
<h1>You are not logged in</h1>
#end
</body>
</html>
3.1.4 Apache Freemarker
Micronaut Views Freemarker includes FreemarkerViewsRenderer which uses the Apache Freemarker Java-based template engine.
Add the following dependency on your classpath.
implementation("io.micronaut.views:micronaut-views-freemarker")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-freemarker</artifactId>
</dependency>
The example shown in the Views section, could be rendered with the following Freemarker template:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
<#if loggedIn??>
<h1>username: <span>${username}</span></h1>
<#else>
<h1>You are not logged in</h1>
</#if>
</body>
</html>
Freemarker integration instantiates a freemarker Configuration
.
All configurable properties are extracted from Configuration
and
Configurable
, and properties names are reused in the Micronaut configuration.
If a value is not declared and is null, the default configuration from Freemarker is used. The expected format of each value is the same from Freemarker, and no conversion or validation is done by the Micronaut framework. You can find in Freemarker documentation how to configure each one.
3.1.5 Rocker
Micronaut Views Rocker includes RockerEngine which uses the Rocker Java-based template engine.
Add the micronaut-views-rocker
dependency to your classpath.
implementation("io.micronaut.views:micronaut-views-rocker")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-rocker</artifactId>
</dependency>
The example shown in the Views section, could be rendered with the following Rocker template:
@args (Boolean loggedIn, String username)
<html>
<head>
<title>Home</title>
</head>
<body>
@if (loggedIn != null && loggedIn) {
<h1>username: <span>@username</span></h1>
} else {
<h1>You are not logged in</h1>
}
</body>
</html>
Compiling Templates
Rocker templates must be precompiled at build time. This can be done either by a Gradle or Maven plugin.
plugins {
id "com.fizzed.rocker" version "1.2.3"
}
sourceSets {
main {
rocker {
srcDir('src/main/resources')
}
}
}
<build>
<plugins>
<plugin>
<groupId>com.fizzed</groupId>
<artifactId>rocker-maven-plugin</artifactId>
<version>1.2.3</version>
<executions>
<execution>
<id>generate-rocker-templates</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<templateDirectory>src/main/resources</templateDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Compile Time Type Checking
Rocker templates are compiled at build time giving the ability to statically type check template arguments.
Using the standard @View
annotation will invoke the template dynamically without these checks.
Instead the compiled template should be invoked within the controller and a RockerWritable
returned.
import io.micronaut.views.rocker.RockerWritable;
import views.home;
...
@Get("/")
@Produces(TEXT_HTML)
public HttpResponse<?> staticTemplate() {
return HttpResponse.ok(new RockerWritable(home.template(true, "sdelamo")));
}
...
Configuration
The properties used can be customized by overriding the values of:
Property | Type | Description |
---|---|---|
|
boolean |
Whether Rocker views are enabled. Default value (true). |
|
java.lang.String |
The default extension to use for Rocker templates. Default value ("rocker.html"). |
|
boolean |
Whether hot reloading is enabled. Default value (false). |
|
boolean |
Whether relaxed binding is enabled for dynamic templates. Default value (false). |
3.1.6 Soy/Closure Support
Micronaut Views Soy includes SoySauceViewsRenderer which uses Soy, also known as Closure Templates, a template compiler from Google. Soy is usable standalone, or together with the rest of the Closure Tools ecosystem, and targets both server-side and client-side, with the ability to compile templates into Java, Python, or JavaScript.
Add the micronaut-views-soy
dependency to your classpath.
implementation("io.micronaut.views:micronaut-views-soy")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-soy</artifactId>
</dependency>
3.1.6.1 Soy Configuration
The entire set of supported Soy configuration properties are documented below:
Property | Type | Description |
---|---|---|
|
boolean |
Whether Soy-backed views are enabled. Default value (true). |
|
boolean |
Specifies whether renaming is enabled. Defaults value (true). |
3.1.6.2 Soy Usage
To use Soy integration you have to provide a bean of type SoyFileSetProvider. Such a bean is responsible for loading the Soy templates, either in compiled or source form.
Given the following Soy template:
{namespace sample}
// Sample template.
{template home}
{@param loggedIn: bool} // Whether the user is logged in.
{@param? username: string} // The user's username, if they are logged in.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Home</title>
<script type="text/javascript">alert("Hello, CSP!");</script>
</head>
<body>
{if $loggedIn}
<h1>username: <span>{$username}</span></h1>
{else}
<h1>You are not logged in</h1>
{/if}
</body>
</html>
{/template}
{template tim}
{@param? username: string}
username: <span>{$username}</span>
{/template}
A naive implementation of SoyFileSetProvider could look like:
@Singleton
public class CustomSoyFileSetProvider implements SoyFileSetProvider {
private static final Logger LOG = LoggerFactory.getLogger(CustomSoyFileSetProvider.class);
private static final String[] VIEWS = {
"home.soy"
};
private final String folder;
private final ResourceLoader resourceLoader;
public CustomSoyFileSetProvider(ViewsConfiguration viewsConfiguration, ResourceLoader resourceLoader) {
this.folder = viewsConfiguration.getFolder();
this.resourceLoader = resourceLoader;
}
@Override
public SoyFileSet provideSoyFileSet() {
final SoyFileSet.Builder builder = SoyFileSet.builder();
for (final String template : VIEWS) {
resourceLoader.getResource(folder + "/" + template).ifPresent(url -> {
try {
builder.add(new File(url.toURI()));
} catch (URISyntaxException e) {
if (LOG.isWarnEnabled()) {
LOG.warn("URISyntaxException raised while generating the SoyFileSet for folder {}",
folder, e);
}
}
});
}
return builder.build();
}
}
@Singleton
class CustomSoyFileSetProvider implements SoyFileSetProvider {
private static final Logger LOG = LoggerFactory.getLogger(CustomSoyFileSetProvider.class)
private static final List<String> VIEWS = [
"home.soy"
]
private final String folder
private final ResourceLoader resourceLoader
CustomSoyFileSetProvider(ViewsConfiguration viewsConfiguration, ResourceLoader resourceLoader) {
this.folder = viewsConfiguration.getFolder()
this.resourceLoader = resourceLoader
}
@Override
SoyFileSet provideSoyFileSet() {
final SoyFileSet.Builder builder = SoyFileSet.builder()
for (String template : VIEWS) {
String path = "${folder}${template}"
resourceLoader.getResource(path).ifPresent(url -> {
try {
builder.add(new File(url.toURI()))
} catch (URISyntaxException e) {
if (LOG.isWarnEnabled()) {
LOG.warn("URISyntaxException raised while generating SoyFileSet for folder {}", folder, e)
}
}
})
}
builder.build()
}
}
@Singleton
class CustomSoyFileSetProvider(
private val resourceLoader: ResourceLoader,
viewsConfiguration: ViewsConfiguration
) : SoyFileSetProvider {
private val folder: String = viewsConfiguration.folder
override fun provideSoyFileSet(): SoyFileSet {
val builder = SoyFileSet.builder()
for (template in VIEWS) {
resourceLoader.getResource("$folder/$template").ifPresent { url: URL ->
try {
builder.add(File(url.toURI()))
} catch (e: URISyntaxException) {
if (LOG.isWarnEnabled) {
LOG.warn("URISyntaxException raised while generating the SoyFileSet for folder {}", folder, e)
}
}
}
}
return builder.build()
}
companion object {
private val LOG = LoggerFactory.getLogger(CustomSoyFileSetProvider::class.java)
private val VIEWS = arrayOf("home.soy")
}
}
Soy allows users to write custom functions that templates can call, as described by their docs for Creating a Plugin. If you have one or more implementations of SoySourceFunction, you can register them in the SoyFileSetProvider.provideSoyFileSet() method using SoyFileSet builder’s addSourceFunction(SoySourceFunction) or addSourceFunction(Iterable<? extends SoySourceFunction>) methods.
|
The return value of the following Controller is converted to Soy template context parameters, and passed to the @View
-annotation-bound template.
@Controller("/soy")
public class SoyController {
@View("sample.home")
@Get
public HttpResponse<?> home() {
return HttpResponse.ok(CollectionUtils.mapOf("loggedIn", true, "username", "sgammon"));
}
}
@Controller("/soy")
class SoyController {
@View("sample.home")
@Get
HttpResponse<?> home() {
HttpResponse.ok([loggedIn: true, username: "sgammon"])
}
}
@Controller("/soy")
class SoyController {
@View("sample.home")
@Get
fun home() = HttpResponse.ok(mutableMapOf(
"loggedIn" to true,
"username" to "sgammon"
))
}
The View annotation value is set to namespace and template name. |
Both server-side Soy rendering layers are supported in Micronaut Views Soy:
-
SoySauceViewsRenderer
: This renderer uses templates pre-compiled to Java bytecode, generally AOT, withSoyToJbcSrcCompiler
. If compiled templates can’t be located by theSoyFileSetProvider
, templates are pre-compiled into bytecode at server startup. This can be impactful on startup-time, so, if that’s an important metric for your app, pre-compile your templates using the AOT bytecode compiler.
3.1.6.3 Soy Features - Template Security
Soy/Closure Templates has seen significant work from Google where security is concerned, both in the browser and on the
backend. Soy is extremely strict and strongly typed, with validation being performed by Soy and then subsequently either
by javac
or Closure Compiler, and additionally in some cases at render-time. What follows is a brief guide of these
security features. These are outlined in more detail over in the
Closure Templates docs.
Front-end Security
-
Trusted URIs: Soy is smart enough to know that
scriptUrl
might be influenced by user input, and, therefore, introduces an XSS vulnerability when used as a scriptsrc
. To inject resources, useuri
:/* I won't compile because I introduce a vulnerability */ {template .foo} {@param scriptUrl: string} <script src={$scriptUrl}></script> {/template}
/* I will compile because I accept a URI type, which is trusted in this context */ {template .foo} {@param scriptUrl: uri} <script src={$scriptUrl}></script> {/template}
-
Markup: Passing in HTML for
content
won’t work if it’s astring
. Soy will comply and inject your content, but it will be escaped. To inject markup, use the strict markup types (js
,css
,html
)./* I will compile, but I may escape the injected content if it contains markup. */ {template .foo} {@param content: string} <b>{$content}</b> {/template}
/* I allow markup because the appropriate types are used */ {template .foo} {@param content: html} <b>{$content}</b> {/template}
Back-end Security
-
CSP Nonce Support: Soy has support for Content Security Policy (Level 3), specifically, embedding server-generated
nonce
attributes in<script>
tags. This is accomplished by providing an "injected value" with the key"csp_nonce"
. The nonce, which should change on each page load, is available in the template like so:/* Soy will inject the nonce for you, but if you need it anyway, this is how you access it. */ {template .foo} {@param injectedScript: uri} {@inject csp_nonce: string} <script src={$injectedScript} nonce={$csp_nonce} type="text/javascript"></script> {/template}
3.1.6.4 Soy Features - Renaming
Soy has powerful built-in renaming features, that both obfuscate and optimize your code as it is rendered. Renaming is opt-in and requires a few things:
-
A compatible CSS compiler (GSS / Closure Stylesheets is a good one)
-
Special calls in your templates that map CSS classes and IDs
-
JSON renaming map provided at compile time for JS templates, and runtime for Java rendering
Renaming is similar to other frameworks' corresponding uglify features, but it’s significantly more powerful, allowing you to rewrite CSS classes and IDs as you would JavaScript symbols. Here’s how you can use renaming server-side with Micronaut Views Soy:
-
Configure Micronaut Views Soy to enable renaming:
micronaut.views.soy.enabled=true
micronaut.views.soy.renaming=true
micronaut:
views:
soy:
enabled: true
renaming: true
[micronaut]
[micronaut.views]
[micronaut.views.soy]
enabled=true
renaming=true
micronaut {
views {
soy {
enabled = true
renaming = true
}
}
}
{
micronaut {
views {
soy {
enabled = true
renaming = true
}
}
}
}
{
"micronaut": {
"views": {
"soy": {
"enabled": true,
"renaming": true
}
}
}
}
-
When building your styles with GSS, or a similar tool, generate a rewrite map:
> java -jar closure-stylesheets.jar \ --output-renaming-map-format JSON \ --output-renaming-map src/main/resources/renaming-map.json \ --rename CLOSURE \ [...inputs...] > src/main/resources/styles/app-styles.min.css
-
In your template sources, annotate CSS classes with
{css('name')}
calls. Note that the value passed into each call must be a string literal (variables cannot be used):{template .foo} <div class="{css('my-cool-class')} {css('my-cool-class-active')}"> ... content, and stuff... </div> {/template}
-
In your CSS, use the names you mentioned in your template:
.my-cool-class { color: blue; } .my-cool-class-active { background: yellow; }
-
Compile your templates (see: Building Soy).
> java -jar SoyToJbcSrcCompiler.jar \ --output templates.jar \ --srcs [...templates...];
The last step is to provide the renaming map to Micronaut Views Soy:
@Singleton
public class RewriteMapProvider implements SoyNamingMapProvider {
/** Filename for the JSON class renaming map. */
private static final String RENAMING_MAP_NAME = "renaming-map.json";
/** Naming map to use for classes. */
private static SoyCssRenamingMap CSS_RENAMING_MAP = null;
/**
* Load JSON embedded in a JAR resource into a naming map for Soy rendering.
*
* @param mapPath URL to the JAR resource we should load.
*/
@Nullable
private static Map<String, String> loadMapFromJSON(URL mapPath) {
try {
return new ObjectMapper().readValue(mapPath, new TypeReference<Map<String, String>>() { });
} catch (Throwable thr) {
//handle `JsonMappingException` and `IOException`, if, for instance, you're using Jackson
throw new RuntimeException(thr);
}
}
static {
final URL mapPath =
SoyRenderConfigProvider.class.getClassLoader().getResource(RENAMING_MAP_NAME);
if (mapPath != null) {
// load the renaming map
final Map<String, String> cssRenamingMap = loadMapFromJSON(mapPath);
if (renamingMapRaw != null) {
CSS_RENAMING_MAP = new SoyCssRenamingMap() {
@Nullable @Override
public String get(@NotNull String className) {
// (or whatever logic you need to rewrite the class)
return cssRenamingMap.get(className);
}
};
}
}
}
/**
* Provide a CSS renaming map to Soy/Micronaut.
*
* @return Inflated Soy CSS renaming map.
*/
@Nullable @Override public SoyCssRenamingMap cssRenamingMap() {
return CSS_RENAMING_MAP;
}
}
Then, your output will be renamed. Referencing the Soy template sample above, output would look something like this:
<div class="a-b-c a-b-c-d">... content, and stuff...</div>
With your CSS rewritten and minified to match:
.a-b-c{color:blue;}.a-b-c-d{background:yellow;}
3.1.6.5 Building Soy
Soy has tooling support from Node (Gulp, Grunt), Maven, Gradle, and Bazel. You can also invoke each Soy compiler directly via the runner classes for each one:
-
SoyHeaderCompiler: Compile templates into light "headers," that can be used downstream as dependencies for other templates.
-
SoyMsgExtractor: Enables easy i18n by extracting
{msg desc=""}{/msg}
declarations for processing or translation. -
SoyParseInfoGenerator: Generates template parser metadata information as Java sources.
-
SoyPluginValidator: Validates
SoySourceFunction
definitions found in a set of JARs. -
SoyToIncrementalDomSrcCompiler: Generate client-side templates that render with IncrementalDOM (AKA
idom
). This compiler uses direct calls into the DOM to incrementally render content, as opposed to the traditional client-side approach, which renders strings intoelement.innerHTML
. This support is currently experimental and involves a number of external dependencies. -
SoyToJbcSrcCompiler: Compiles Soy templates directly to Java bytecode, packaged up in a JAR. These templates can be used together with Soy Java for highly performant server-side rendering.
-
SoyToJsSrcCompiler: Traditional JS client-side compiler, which assembles strings of rendered template content. Like
idom
, these compiled templates work well with Closure Compiler, and Closure Library viagoog.soy
. -
SoyToPySrcCompiler: Compiles templates into Python sources for use server-side.
3.1.7 Pebble
Micronaut Views Pebble includes PebbleViewsRenderer which uses the Pebble project.
Add the following dependency on your classpath. For example, in build.gradle
implementation("io.micronaut.views:micronaut-views-pebble")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-pebble</artifactId>
</dependency>
The example shown in the Views section, could be rendered with the following Pebble template:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
{% if loggedIn %}
<h1>username: <span>{{ username }}</span></h1>
{% else %}
<h1>You are not logged in</h1>
{% endif %}
</body>
</html>
The properties used can be customized by overriding the values of:
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether the component is enabled. Default value (true). |
|
java.lang.String |
The default extension. Default value ("html"). |
|
boolean |
Enable/disable all caches, i.e. cache used by the engine to store compiled PebbleTemplate instances and tags cache. Default value (true). |
|
boolean |
Changes the newLineTrimming setting of the PebbleEngine. By default, Pebble will trim a new line that immediately follows a Pebble tag. If set to false, then the first newline following a Pebble tag won’t be trimmed. All newlines will be preserved. Default value (true). |
|
boolean |
Sets whether or not escaping should be performed automatically. Default value (true). |
|
java.lang.String |
Sets the default escaping strategy of the built-in escaper extension. Default value (EscapeFilter.HTML_ESCAPE_STRATEGY). |
|
boolean |
Changes the strictVariables setting of the PebbleEngine. Default value (false). |
|
boolean |
Enable/disable greedy matching mode for finding java method. Default is disabled. If enabled, when can not find perfect method (method name, parameter length and parameter type are all satisfied), reduce the limit of the parameter type, try to find other method which has compatible parameter types. Default value (false). |
|
boolean |
Sets whether or not core operators overrides should be allowed. Default value (false). |
|
boolean |
Enable/disable treat literal decimal as Integer. Default value (false), treated as Long. |
|
boolean |
Enable/disable treat literal numbers as BigDecimals. Default value (false), treated as Long/Double. |
3.1.8 JTE
Micronaut Views Jte includes JteViewsRenderer which uses jte (Java Template Engine), a secure and lightweight template engine for Java and Kotlin.
Add the micronaut-views-jte
dependency to your classpath.
implementation("io.micronaut.views:micronaut-views-jte")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-jte</artifactId>
</dependency>
If you want to write your views in Kotlin, you can include an additional dependency:
implementation("gg.jte:jte-kotlin")
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte-kotlin</artifactId>
</dependency>
The example shown in the Views section, could be rendered with the following Jte template:
@param Boolean loggedIn
@param String username
<html>
<head>
<title>Home</title>
</head>
<body>
@if (loggedIn != null && loggedIn)
<h1>username: <span>${username}</span></h1>
@else
<h1>You are not logged in</h1>
@endif
</body>
</html>
Compiling Templates
Jte templates may be precompiled at build time. This can be done by a Gradle plugin or Maven plugin. If not precompiled, the application will need a JDK so it can compile templates at runtime.
Dynamic Reloading
When dynamic
is enabled (see below), jte will load templates from the project source directory, and will reload them after changes.
Configuration
The properties used can be customized by overriding the values of:
Property | Type | Description |
---|---|---|
|
boolean |
Whether to enable dynamic reloading of templates. Default value (false). |
|
java.lang.String |
Root directory under which to write generated source and class files. . Default value ("build/jte-classes"). |
|
java.lang.String |
When using dynamic templates, the root source directory to search. If not specified, jte will search src/<sourceset>/jte and src/<sourceset>/resources/<folder> where 'folder' is ViewsConfiguration.getFolder(). In cases where the source directory cannot be found, jte will use classpath loading instead, and will not dynamically reload templates. |
|
boolean |
Enable building binary content for templates. Default value (false). (Only has an effect when 'dynamic' is true. To use with precompiled templates, enable it in the build plugin) |
.kte (Kotlin) templates
To use .kte
(Kotlin) templates include the dependency:.
implementation("gg.jte:jte-kotlin")
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte-kotlin</artifactId>
</dependency>
3.1.9 JStachio
Micronaut Views JStachio includes JStachioMessageBodyWriter, which uses JStachio, a type-safe compiled Mustache-based template engine, to render templates.
3.1.9.1 JStachio Installation
To use Micronaut Jstachio, add the micronaut-views-jstachio
dependency to your classpath.
implementation("io.micronaut.views:micronaut-views-jstachio")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-jstachio</artifactId>
</dependency>
Additionally, you need to add JStachio’s Java annotation processor.
annotationProcessor("io.jstach:jstachio-apt")
<annotationProcessorPaths>
<path>
<groupId>io.jstach</groupId>
<artifactId>jstachio-apt</artifactId>
</path>
</annotationProcessorPaths>
For Kotlin, add the jstachio-apt dependency in kapt or ksp scope, and for Groovy add jstachio-apt in compileOnly scope.
|
Read the Jstachio’s user guide to learn more.
3.1.9.2 JStachio Example
With Micronaut JStachio, you cannot use the View annotation, respond in your controller a ModelAndView or use Micronaut Views Configuration to configure the views directory since it does not use the ViewsFilter. |
Instead, a controller returns a model annotated with @JStache
referencing the view
name.
@Controller("/views")
public class HomeController {
@Produces(MediaType.TEXT_HTML)
@Get
HomeModel index() {
return new HomeModel("sdelamo", true);
}
}
package io.micronaut.views.jstachio.pkginfo;
import io.jstach.jstache.JStache;
import io.micronaut.core.annotation.Nullable;
@JStache(path = "home")
public record HomeModel(@Nullable String username, boolean loggedIn) {
}
You can provide a suffix
and prefix
for every model with the @JStacheConfig
annotation.
Add a package-info.java
in the same package of your model classes.
@JStacheConfig(
pathing = @JStachePath(prefix = "views/", suffix = ".mustache")
)
package io.micronaut.views.jstachio.pkginfo;
import io.jstach.jstache.JStacheConfig;
import io.jstach.jstache.JStachePath;
The previous example renders the following template:
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
{{#loggedIn}}
<h1>username: <span>{{username}}</span></h1>
{{/loggedIn}}
{{^loggedIn}}
<h1>You are not logged in</h1>
{{/loggedIn}}
</body>
</html>
3.1.10 React SSR
React server-side rendering (SSR) allows you to pre-render React components to HTML before the page is sent to the user. This improves performance by ensuring the page appears before any Javascript has loaded (albeit in a non-responsive state) and makes it easier for search engines to index your pages.
This module is experimental and subject to change. |
Micronaut’s support for React SSR has the following useful features:
-
Javascript runs using GraalJS, a high performance Javascript engine native to the JVM. Make sure to run your app on GraalVM or by compiling it to a native image to get full Javascript performance.
-
Compatible out of the box with both React and Preact, an alternative lighter weight implementation of the React concept.
-
Customize the Javascript used to invoke SSR to add features like head managers, or use the prepackaged default scripts to get going straight away.
-
The Javascript can be sandboxed, ensuring that your server environment is protected from possible supply chain attacks.
-
You can pass any
@Introspectable
Java objects to use as props for your page components. This is convenient for passing in things like the user profile info. -
Logging from Javascript is sent to the Micronaut logs.
console.log
and related will go to theINFO
level of the logger namedjs
,console.error
and Javascript exceptions will go to theERROR
level of the same.
To use React SSR you need to add two dependencies.
-
Add the
micronaut-views-react
dependency. -
Add a dependency on
org.graalvm.polyglot:js
ororg.graalvm.polyglot:js-community
. The difference is to do with licensing and performance, with thejs
version being faster and free to use but not open source. Learn more about choosing an edition.
implementation("io.micronaut.views:micronaut-views-react")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-react</artifactId>
</dependency>
Scaffold a Micronaut frontend/backend project
You can easily create a new ReactJS-based project from scratch using Micronaut Launch, the built-in IntelliJ Micronaut wizard, or the mn
CLI tool:
$ mn create-app --features=views-react my-cool-project
This will create a complete project with webpack
based bundling for client and server side rendering, a pre-configured Micronaut Views React app, and a sample component/page handler. Javascript package installation and bundling is handled by Gradle or Maven, depending on the value of the --build
flag you pass to mn
(we recommend Gradle as it will be much faster). The build scripts will even download NodeJS for you, so you don’t need anything other than a JDK to get started.
Configuration properties
The properties used can be customized by overriding the values of:
Property | Type | Description |
---|---|---|
|
java.lang.String |
the URL (relative or absolute) where the client Javascript bundle can be found. It will be appended to the generated HTML in a <script> tag. Defaults to "/static/client.js" |
|
java.lang.String |
the path relative to micronaut.views.folder where the bundle used for server-side rendering can be found. Defaults to "classpath:views/ssr-components.mjs" |
|
java.lang.String |
Either a file path (starting with "file:" or a resource in the classpath (starting with "classpath:") to a render script. Please see the user guide for more information on what this Javascript file should contain. |
|
boolean |
If true, GraalJS sandboxing is enabled. This helps protect you against supply chain attacks that might inject code into your server via hijacked React components. It requires a sufficiently new version of GraalJS. Defaults to OFF. |
How it fits together
Props can be supplied in the form of an introspectable bean or a Map<String, Object>
. Both forms will be serialized to JSON and sent to the client for hydration, as well as used to render the root component. The URL of the current page will be taken from the request and added to the props under the url
key, which is useful when working with libraries like preact-router
. If you use Map<String, Object>
as your model type and use Micronaut Security, authenticated usernames and other security info will be added to your props automatically.
On the server side prop objects are exposed to Javascript directly, without being serialized to JSON first. Micronaut’s compile time reflection is used and this avoids some overhead as well as simplifying access to your props (see below). If your props bean returns a non-introspectable object from a property, it will be mapped in the normal way for GraalJS (meaning it will use runtime reflection and may require @HostAccess.Export
on methods you wish to call).
By default, you will need React components that return the entire page, including the <html>
tag. You’ll also need to prepare your Javascript (see below). Then just name your required page component in the @View
annotation on a controller, for example @View("App")
will render the <App/>
component with your page props.
If your page components don’t render the whole page or you need better control over how the framework is invoked you can use render scripts (see below).
Accessing Java from Javascript
The usual GraalJS rules for accessing Java apply with a few differences:
-
Your root prop object and any introspectable object reachable from it can be accessed using normal Javascript property syntax, for instance if you have an
@Introspectable
bean with aString getFoo()
method then you can just access that property by writingprops.foo
instead ofprops.getFoo()
, as would normally be required when accessing Java objects. -
Methods annotated with
@Executable
can be invoked from Javascript. Arguments and return values are mapped to/from Java in a natural manner. -
Your code can use
Java.type("com.foo.bar.BazClass")
style calls to get access to Java classes and then instantiate them or call static methods on them.
Note that props are read only. Attempting to set the value of a Java property on a props object will fail.
Sandbox
By default, Javascript executing server side runs with the same privilege level as the server itself. This is similar to the Node security model but exposes you to supply chain attacks. If a third party React component you depend on turns out to be malicious or simply buggy, it could allow an attacker to run code server side instead of only inside the browser sandbox.
Normally with React SSR you can’t do much about this, but with Micronaut Views React you can enable a server-side sandbox if you use GraalVM 24.1 or higher. This prevents Javascript from accessing any Java host objects that haven’t been specifically exposed into the sandbox. To use this set micronaut.views.react.sandbox
to true in your application.properties
.
In this mode:
-
The
Java
top level object that lets code access any class will be gone. -
Methods in
@Introspectable
objects reachable from your prop objects that are marked as@Executable
will be exposed into the sandbox regardless of sandbox settings. So be careful what methods you add to your props. -
Any objects exposed via your root props that are not marked
@Introspectable
will be exposed via runtime reflection instead. In that case what’s available inside the sandbox will depend on theHostAccess
policy, which can be customized by using the factory replacement mechanism (see the docs for Micronaut Core for details). By default anything not annotated with@HostAccess.Exposed
will be invisible and uninvokable. Normally this is sufficient, but customizing theHostAccess
can be useful if you want to expose third party code you don’t control into the sandbox.
3.1.10.1 Preparing your Javascript
An app that uses SSR needs the React components to be bundled twice, once for the client and once for the server. For the server you need to make a Javascript module bundle that imports and then re-exports the (page) components you will render, along with React
and ReactServerDOM
. The bundle must be compatible with GraalJS, which is not NodeJS and thus doesn’t support the same set of APIs. You will also need to create a client-side bundle as per usual, and change how you start up React.
This tutorial doesn’t take you through how to create a ReactJS project from scratch - please refer to the React documentation for that. |
To start we will need a server.js
file. It should be a part of your frontend project and can be named and placed wherever you like, as the server will only need the final compiled bundle. Your server.js
should look like this:
import React from 'react';
import ReactDOMServer from 'react-dom/server';
// Page components
import App from './components/App';
export { React, ReactDOMServer, App };
Add your page components as imports, and then also to the export line. We will now set up Webpack to turn this file into a bundle.
-
Run
npm i webpack node-polyfill-webpack-plugin text-encoding
to install some extra packages that are needed. -
Create a config file called e.g.
webpack.server.js
like the following:
const path = require('path');
const webpack = require('webpack');
// This targets the browser even though we run it server-side, because that's closer to GraalJS.
module.exports = {
entry: ['web-streams-polyfill/dist/polyfill', './server.js'],
output: {
path: path.resolve(__dirname, '../resources/views/'),
filename: 'ssr-components.mjs',
module: true,
library: {
type: 'module',
},
// GraalJS uses `globalThis` instead of `window` for the global object.
globalObject: 'globalThis'
},
devtool: false,
experiments: {
outputModule: true
},
plugins: [
new webpack.ProvidePlugin({
// GraalJS doesn't support TextEncoder yet. It's easy to add and here's a polyfill in the meantime.
TextEncoder: ['text-encoding', 'TextEncoder'],
TextDecoder: ['text-encoding', 'TextDecoder'],
}),
new webpack.DefinePlugin({
SERVER: true,
})
],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
}
]
}
};
This Webpack config does several things:
-
It polyfills APIs that lack a native implementation in the GraalJS engine.
-
It ensures the output is a native Javascript module.
-
It names the result
ssr-components.mjs
. You can use any name, but it’s looked for under this name in theviews
resource directory by default. All components must be in one server side bundle. -
It makes the
SERVER
variable be statically true when the Javascript is being bundled for server-side rendering. This allows you to include/exclude code blocks at bundle optimization time.
You can use such a config by running npx webpack --mode production --config webpack.server.js
. Add the --watch
flag if you want the bundle to be recreated whenever an input file changes. Micronaut React SSR will notice if the bundle file has changed on disk and reload it (see Development).
Now create client.js
. This will contain the Javascript that runs once the page is loaded, and which will "hydrate" the React app (reconnect the event handlers to the pre-existing DOM). It should look like this:
import React from 'react';
import {hydrateRoot} from 'react-dom/client';
const pageComponentName = Micronaut.rootComponent;
import(`./components/${pageComponentName}.js`).then(module => {
const PageComponent = module[pageComponentName]
hydrateRoot(document, <PageComponent {...Micronaut.rootProps}/>)
})
Depending on how you configure minification, you may also need to import your page components here. This small snippet of code reads the Micronaut
object which is generated by the Micronaut React SSR renderer just before your client.js
code is loaded. It contains the component named in your @View("MyPageComponent")
annotation, which is then loaded assuming it is in a Javascript module of the same name. The props that will be passed to that page component as generated from the object you return from your controller method. If you wish you can wrap <PageComponent/>
here with any contexts you need.
And now for the webpack.client.js
config:
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: ['./client.js'],
devtool: false,
output: {
path: path.resolve(__dirname, '../resources/views/static'),
filename: 'client.js',
},
plugins: [
new webpack.DefinePlugin({
SERVER: false,
})
],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
}
]
}
};
It tells Webpack to generate a series of JS files that are then placed in the src/main/resources/static
directory.
Run Webpack to generate the needed Javascript files for both client and server.
3.1.10.2 Setting serving properties
React SSR needs some Micronaut application properties to be set.
micronaut.views.react.server-bundle-path=classpath:views/ssr-components.mjs
micronaut.router.static-resources.js.mapping=/static/**
micronaut.router.static-resources.js.paths=classpath:static
micronaut.executors.blocking.virtual=false
micronaut:
# Point to the server-side JS. This value is the default.
views:
react:
server-bundle-path: "classpath:views/ssr-components.mjs"
router:
static-resources:
js:
mapping: "/static/**"
paths: "classpath:static"
# A temporary workaround for a GraalJS limitation.
executors:
blocking:
virtual: false
[micronaut]
[micronaut.views]
[micronaut.views.react]
server-bundle-path="classpath:views/ssr-components.mjs"
[micronaut.router]
[micronaut.router.static-resources]
[micronaut.router.static-resources.js]
mapping="/static/**"
paths="classpath:static"
[micronaut.executors]
[micronaut.executors.blocking]
virtual=false
micronaut {
views {
react {
serverBundlePath = "classpath:views/ssr-components.mjs"
}
}
router {
staticResources {
js {
mapping = "/static/**"
paths = "classpath:static"
}
}
}
executors {
blocking {
virtual = false
}
}
}
{
micronaut {
views {
react {
server-bundle-path = "classpath:views/ssr-components.mjs"
}
}
router {
static-resources {
js {
mapping = "/static/**"
paths = "classpath:static"
}
}
}
executors {
blocking {
virtual = false
}
}
}
}
{
"micronaut": {
"views": {
"react": {
"server-bundle-path": "classpath:views/ssr-components.mjs"
}
},
"router": {
"static-resources": {
"js": {
"mapping": "/static/**",
"paths": "classpath:static"
}
}
},
"executors": {
"blocking": {
"virtual": false
}
}
}
}
This sets up static file serving so your client JS will be served by your Micronaut app. This isn’t mandatory: you can serve your client JS from anywhere, but you would need to set micronaut.views.react.client-bundle-url
in that case to where the client root bundle can be found.
Watch out for the last property that disables virtual threads. If you skip this you will get an error the first time a view is rendered. Future releases of GraalJS will remove the need to disable virtual threads in Micronaut. |
Development
During development you want the fastest iteration speed possible. These property changes will help:
micronaut.server.netty.responses.file.cache-seconds=0
micronaut.io.watch.enabled=true
micronaut.io.watch.paths=build/resources/main/views
micronaut:
# For development purposes only.
server:
netty:
responses:
file:
cache-seconds: 0
io:
watch:
enabled: true
paths: build/resources/main/views
[micronaut]
[micronaut.server]
[micronaut.server.netty]
[micronaut.server.netty.responses]
[micronaut.server.netty.responses.file]
cache-seconds=0
[micronaut.io]
[micronaut.io.watch]
enabled=true
paths="build/resources/main/views"
micronaut {
server {
netty {
responses {
file {
cacheSeconds = 0
}
}
}
}
io {
watch {
enabled = true
paths = "build/resources/main/views"
}
}
}
{
micronaut {
server {
netty {
responses {
file {
cache-seconds = 0
}
}
}
}
io {
watch {
enabled = true
paths = "build/resources/main/views"
}
}
}
}
{
"micronaut": {
"server": {
"netty": {
"responses": {
"file": {
"cache-seconds": 0
}
}
}
},
"io": {
"watch": {
"enabled": true,
"paths": "build/resources/main/views"
}
}
}
}
The paths
property is correct if you’re using the default JS compilation setup created for you by Micronaut Starter. If your JS bundle is held in a different directory in your project, make sure to set the path appropriately.
Now you can tell your build system to only recompile the needed files. In a Micronaut Starter based project that uses Gradle, just run ./gradlew --continuous processResources
. This will cause new bundles to be created for both client and server whenever your input JS changes. If using Maven turn off Micronaut’s automatic restart features so that changes to the compiled bundle JS don’t cause the whole server to reboot, and then make sure to re-run the Maven build when necessary:
<plugin> <groupId>io.micronaut.maven</groupId> <artifactId>micronaut-maven-plugin</artifactId> <version>...</version> <configuration> <watches> <watch> <directory>src/main/resources</directory> <excludes> <exclude>**/*.js</exclude> <exclude>**/*.mjs</exclude> </excludes> </watch> </watches> </configuration> </plugin>
3.1.10.3 Integrating with Preact
The Preact library is a smaller and lighter weight implementation of React, with a few nice enhancements as well. Like React it also supports server side rendering and can be used with Micronaut React SSR. It requires some small changes to how you prepare your Javascript. Please read and understand how to prepare your JS for regular React first, as this section only covers the differences.
Your server.js
should look like this:
import {renderToString} from 'preact-render-to-string';
import * as preact from 'preact';
// Page components
import App from './components/App';
export { preact, renderToString, App };
Notice the differences: we’re re-exporting the h
symbol from Preact (which it uses instead of React.createComponent
) and renderToString
from the separate preact-render-to-string
module. Otherwise the script is the same: we have to export each page component.
Your client.js
should look like this:
import {h, render} from 'preact'
const pageComponentName = Micronaut.rootComponent;
import(`./components/${pageComponentName}.js`).then(module => {
const PageComponent = module[pageComponentName]
render(h(PageComponent, Micronaut.rootProps, null), document)
})
Finally, you need to tell Micronaut Views React to use a different render script (see below). Set the micronaut.views.react.render-script
application property to be classpath:/io/micronaut/views/react/preact.js
.
That’s it. If you want to use existing React components then you will also need to set up aliases in your webpack.{client,server}.js
files like this:
module.exports = {
// ... existing values
resolve: {
alias: {
"react": "preact/compat",
"react-dom/test-utils": "preact/test-utils",
"react-dom": "preact/compat", // Must be below test-utils
"react/jsx-runtime": "preact/jsx-runtime"
},
}
}
3.1.10.4 Render scripts
The code that kicks off the SSR process using your React libraries API is called a render script. Micronaut Views React ships with two pre-packaged render scripts, one for ReactJS and one for Preact, but you are also able to supply your own. This lets you take complete control over the server-side Javascript. To use a custom script, place it somewhere on your classpath or file system and then set the micronaut.views.react.render-script
property to its path, prefixed with either classpath:
or file:
depending on where it should be found.
A render script should be an ESM module that exports a single function called ssr
that takes four arguments:
-
A function object for the page component to render.
-
An object containing the root props.
-
A callback object that contains APIs used to communicate with Micronaut.
-
A string that receives the URL of the bundle that the browser should load. This is specified by the
micronaut.views.react.clientBundleURL
application property.
The default render script looks like this:
export async function ssr(component, props, callback, clientBundleURL) {
globalThis.Micronaut = {};
const url = callback.url();
if (url)
props = {...props, "url": url};
const element = React.createElement(component, props, null);
// Data to be passed to the browser after the main HTML has finished loading.
const boot = {
rootProps: props,
rootComponent: component.name,
};
// The Micronaut object defined here is not the same as the Micronaut object defined server side.
const bootstrapScriptContent = `var Micronaut = ${JSON.stringify(boot)};`;
const stream = await ReactDOMServer.renderToReadableStream(element, {
bootstrapScriptContent: bootstrapScriptContent,
bootstrapScripts: [clientBundleURL]
});
// This ugliness is because renderToPipeableStream (what we should really use) is only in the node build
// of react-dom/server, but we use the browser build. Trying to use the node build causes various errors
// and problems that I don't yet understand, something to do with module formats.
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
callback.write(value);
}
}
The default render script for Preact looks like this:
export function ssr(component, props, callback, clientBundleURL) {
globalThis.Micronaut = {};
const url = callback.url();
if (url)
props = {...props, "url": url};
const html = renderToString(preact.h(component, props, null))
callback.write(html)
const boot = {
rootProps: props,
rootComponent: component.name,
};
// The Micronaut object defined here is not the same as the Micronaut object defined server side.
callback.write(`<script type="text/javascript">var Micronaut = ${JSON.stringify(boot)};</script>`)
callback.write(`<script type="text/javascript" src="${clientBundleURL}" async="true">`)
}
A more sophisticated render script might support the use of head managers (see below), do multiple renders, expose other APIs and so on.
A render script is evaluated after your server side bundle, and has access to any symbols your server script exported. If you wish to access a JS module you should therefore include it in your server.js
that gets fed to Webpack or similar bundler, and then re-export it like this:
import * as mymod from 'mymod';
export { mymod };
The callback object has a few different APIs you can use:
-
write(string)
: Writes the given string to the network response. -
write(bytes)
: Writes the given array of bytes to the network response. -
url()
: Returns either null or a string containing the URL of the page being served. Useful for sending to page routers.
3.1.10.4.1 Using head managers
Head managers are libraries that let you build up the contents of your <head>
block as your <body>
renders. One use of custom render scripts is to integrate a head manager with your code. Here’s an example of a simple render script that usees the React Helmet library in this way. Remember to export Helmet
from your server-side bundle.
export async function ssr(component, props, callback, config) {
// Create the vdom.
const element = React.createElement(component, props, null);
// Render the given component, expecting it to fill a <div id="content"></div> in the <body> tag.
const body = ReactDOMServer.renderToString(element)
// Get the data that should populate the <head> from the Helmet library.
const helmet = Helmet.renderStatic();
// Data to be passed to the browser after the main HTML has finished loading.
const boot = {
rootProps: props,
rootComponent: component.name,
};
// Assemble the HTML.
const html = `
<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
</head>
<body ${helmet.bodyAttributes.toString()}>
<div id="content">
${body}
</div>
<script>var Micronaut = ${JSON.stringify(boot)};</script>
<script type="text/javascript" src="${config.getClientBundleURL()}" async="true"></script>
</body>
</html>`;
// Send it back.
callback.write(html);
}
3.1.10.5 Known limitations
Micronaut React SSR has the following known issues and limitations:
-
There is no built-in support for server side fetching.
-
The rendering isn’t streamed to the user.
-
<Suspense>
is not supported.
3.2 Working with Models
The Micronaut framework provides a simple way to modify and enhance a model prior to being sent to a template engine for rendering.
3.2.1 Dynamically Enriching Models
Provide custom processors by registering beans of type ViewModelProcessor.
The following example shows a bean of type ViewModelProcessor which includes a fictitious configuration object in the rendering context:
@Singleton (1)
public class ConfigViewModelProcessor implements ViewModelProcessor<Map<String, Object>> {
private final ApplicationConfiguration config;
ConfigViewModelProcessor(ApplicationConfiguration environment) {
this.config = environment;
}
@Override
public void process(@NonNull HttpRequest<?> request,
@NonNull ModelAndView<Map<String, Object>> modelAndView) {
modelAndView.getModel()
.ifPresent(model -> {
if (config.getName().isPresent()) {
model.put("applicationName", config.getName().get());
}
});
}
}
1 | Use javax.inject.Singleton to designate this class as a singleton in the Micronaut Bean Context. |
If you use Micronaut Security, Micronaut Views registers a ViewModelProcessor to enrich models with security related information.
3.3 Fieldset Generation
Fieldset API is experimental and subject to change. |
The FieldsetGenerator API simplifies the generation of an HTML Fieldset representation for a given type or instance. It leverages the introspection builder support.
The FormGenerator API wraps the previous API and simplifies the generation of an HTML form.
To use these APIs, you need the dependency the following dependency:
implementation("io.micronaut.views:micronaut-views-fieldset")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-fieldset</artifactId>
</dependency>
3.3.1 Form Generation Example
Imagine, you want to create an application which displays a form such as:
<form action="/books/save" method="post">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" name="title" value="" id="title" minlength="2" maxlength="255" class="form-control" required="required"/>
</div>
<div class="mb-3">
<label for="pages" class="form-label">Pages</label>
<input type="number" name="pages" value="" id="pages" min="1" max="21450" class="form-control" required="required"/>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
In Java, you will create a representation for the form submission. Something like:
package io.micronaut.views.fields.thymeleaf;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@Serdeable
public record BookSave(@Size(min = 2, max = 255) @NotBlank String title,
@Min(1) @Max(21450) @NotNull Integer pages) {
}
The field types and the validation annotations in the previous code sample influence the form generation. |
Then using the form generation API, create a controller such as:
@Controller("/books")
class BookController {
public static final String CONTROLLER_PATH = "/books";
public static final String SAVE_PATH = "/save";
public static final String FORM = "form";
private final FormGenerator formGenerator;
private final BookRepository bookRepository;
BookController(FormGenerator formGenerator,
BookRepository bookRepository) {
this.formGenerator = formGenerator;
this.bookRepository = bookRepository;
}
@Produces(MediaType.TEXT_HTML)
@View("/books/create.html")
@Get("/create")
Map<String, Object> create() {
return Collections.singletonMap(FORM,
formGenerator.generate(CONTROLLER_PATH + SAVE_PATH, BookSave.class));
}
@Produces(MediaType.TEXT_HTML)
@View("/books/list.html")
@Get("/list")
Map<String, Object> list() {
return Collections.singletonMap("books", bookRepository.findAll());
}
@Produces(MediaType.TEXT_HTML)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Post(SAVE_PATH)
HttpResponse<?> save(@Valid @Body BookSave bookSave) {
bookRepository.save(new Book(null, bookSave.title(), bookSave.pages()));
return HttpResponse.seeOther(UriBuilder.of("/books").path("list").build());
}
@Error(exception = ConstraintViolationException.class)
public HttpResponse<?> onConstraintViolationException(HttpRequest<?> request, ConstraintViolationException ex) {
if (request.getPath().equals(CONTROLLER_PATH + SAVE_PATH)) {
Optional<BookSave> bookSaveOptional = request.getBody(BookSave.class);
if (bookSaveOptional.isPresent()) {
Form form = formGenerator.generate(CONTROLLER_PATH + SAVE_PATH, bookSaveOptional.get(), ex);
ModelAndView<Map<String, Object>> body = new ModelAndView<>("/books/create.html",
Collections.singletonMap(FORM, form));
return HttpResponse.unprocessableEntity().body(body);
}
}
return HttpResponse.serverError();
}
}
Micronaut Launch or the Micronaut Command Line Interface (CLI) will generate Thymeleaf fragments to render a form when you select the views-thymeleaf
feature.
Thanks to those fragments, rendering the form for the /books/create
route in the previous example is really simple:
<!DOCTYPE html>
<html lang="en" th:replace="~{layout :: layout(~{::title},~{::script},~{::main})}" xmlns:th="http://www.thymeleaf.org">
<head>
<title></title>
<script></script>
</head>
<body>
<main>
<form th:replace="~{fieldset/form :: form(${form})}"></form>
</main>
</body>
</html>
3.3.2 Fieldset Annotations
Sometimes, more than the Java type is needed to define the input type. For example, you may want to render a login form such as:
<form action="/login" method="post">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" name="username" value="" id="username" class="form-control" required="required"/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" name="password" value="" id="password" class="form-control" required="required"/>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
In Java, you will create a representation for the form submission and annotate the password
field with @InputPassword. Something like:
package io.micronaut.views.fields.thymeleaf;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.views.fields.annotations.InputPassword;
import jakarta.validation.constraints.NotBlank;
@Introspected
public record Login(@NotBlank String username,
@InputPassword @NotBlank String password) {
}
The following annotations are available:
Annotation | Description |
---|---|
Annotation to specify a field is a checkbox input. |
|
Annotation to specify a field is an email input. |
|
Annotation to specify a field is a hidden input. |
|
Annotation to specify a field is a number input. |
|
Annotation to specify a field is a password input. |
|
Annotation to specify a field is a radio input. |
|
Annotation to specify a field is a telephone input. |
|
Annotation to specify a field is a text input. |
|
Annotation to specify a field is an url input. |
|
Annotation to specify a field is an HTML select element. |
|
Annotation to specify a field is a textarea. |
|
Annotation to mark a field as a Trix editor. |
3.3.3 Radio, Checkbox and Option Fetcher
The @InputCheckbox, @InputRadio, and @Select annotations allow you to specify a fetcher class to load data necessary for the form.
Imagine you want a form to associate an author with a book form, such as:
<form action="/books/authors/save" method="post">
<input type="hidden" name="bookId" value="1"/>
<div class="mb-3">
<label for="authorId" class="form-label">Author Id</label>
<select name="authorId" id="authorId" class="form-select" required="required">
<option value="1">Kishori Sharan</option>
<option value="2">Peter Späth</option>
<option value="3">Sam Newman</option>
</select>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>"
In Java, you will create a representation for the form submission. Something like:
package io.micronaut.views.fields.thymeleaf;
import io.micronaut.serde.annotation.Serdeable;
import io.micronaut.views.fields.annotations.InputHidden;
import io.micronaut.views.fields.annotations.Select;
import jakarta.validation.constraints.NotNull;
@Serdeable
public record BookAuthorSave(@NotNull @InputHidden Long bookId,
@NotNull @Select(fetcher = AuthorFetcher.class) Long authorId) {
}
The fetcher
member of the Select
annotation allows you to specify a class. AuthorFetcher
is a Singleton
of type OptionFetcher
, and you could write it, for example, like this:
package io.micronaut.views.fields.thymeleaf;
import io.micronaut.views.fields.messages.Message;
import io.micronaut.views.fields.elements.Option;
import io.micronaut.views.fields.fetchers.OptionFetcher;
import jakarta.inject.Singleton;
import java.util.List;
@Singleton
public class AuthorFetcher implements OptionFetcher<Long> {
private final AuthorRepository authorRepository;
public AuthorFetcher(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
@Override
public List<Option> generate(Class<Long> type) {
return authorRepository.findAll()
.stream()
.map(author -> Option.builder()
.value(String.valueOf(author.id()))
.label(Message.of(author.title()))
.build()
).toList();
}
@Override
public List<Option> generate(Long instance) {
return authorRepository.findAll()
.stream()
.map(author -> Option.builder()
.selected(author.id().equals(instance))
.value(String.valueOf(author.id()))
.label(Message.of(author.title()))
.build())
.toList();
}
}
3.3.4 CSRF Token Hidden Field
If you use the Micronaut Security CSRF module, and a CSRF token is resolved, the generated form automatically contains a hidden input with the CSRF token as the value.
3.3.5 Custom Form Elements
You many need a custom implementation of FormElement. If you do, create a bean of type FormElementResolver to resolve your custom FormElement
class for a certain bean property’s type.
Providing a bean of type FormElementResolver allows the custom form element to be part of the FieldsetGenerator API.
3.4 HTMX
To integrate with HTMX, add the following dependency:
implementation("io.micronaut.views:micronaut-views-htmx")
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-htmx</artifactId>
</dependency>
3.4.1 HTMX Request Headers
You can bind HtmxRequestHeaders in a controller method.
In the following example, the parameter is bound if the request is an HTMX request. If it is not an HTMX request, the parameter is bound as null.
@Get
ModelAndView<Map<String, Object>> index(@Nullable HtmxRequestHeaders htmxRequestHeaders) {
Map<String, Object> model = Collections.singletonMap("fruit", new Fruit("Apple", "Red"));
if (htmxRequestHeaders != null) {
return new ModelAndView<>("fruit", model);
}
return new ModelAndView<>("fruits", model);
}
@Get
ModelAndView<Map<String, Object>> index(@Nullable HtmxRequestHeaders htmxRequestHeaders) {
Map<String, Object> model = [fruit: new Fruit("Apple", "Red")]
if (htmxRequestHeaders != null) {
return new ModelAndView<>("fruit", model)
}
new ModelAndView<>("fruits", model)
}
@Get
fun index(htmxRequestHeaders: HtmxRequestHeaders?): ModelAndView<Map<String, Any>> {
val model = mapOf("fruit" to Fruit("Apple", "Red"))
if (htmxRequestHeaders != null) {
return ModelAndView("fruit", model)
}
return ModelAndView("fruits", model)
}
3.4.2 Out of Band Swaps
You can return an HtmxResponse in a controller method to render multiple views in a single HTMX response—for example, to do Out Of Band Swaps.
@Post
HtmxResponse<?> outOfBandSwaps(@NonNull HtmxRequestHeaders htmxRequestHeaders) {
return HtmxResponse.builder()
.modelAndView(new ModelAndView<>("fruit", Collections.singletonMap("fruit", new Fruit("Apple", "Red"))))
.modelAndView(new ModelAndView<>("swap", Collections.emptyMap()))
.build();
}
@Post
HtmxResponse<?> outOfBandSwaps(@NonNull HtmxRequestHeaders htmxRequestHeaders) {
HtmxResponse.builder()
.modelAndView(new ModelAndView<>("fruit", [fruit: new Fruit("Apple", "Red")]))
.modelAndView(new ModelAndView<>("swap", [:]))
.build()
}
@Post
fun outOfBandSwaps(htmxRequestHeaders: HtmxRequestHeaders): HtmxResponse<*> {
return HtmxResponse.builder<Any>()
.modelAndView(ModelAndView("fruit", mapOf("fruit" to Fruit("Apple", "Red"))))
.modelAndView(ModelAndView("swap", emptyMap<Any, Any>()))
.build()
}
3.4.3 HTMX Response Headers
The class HtmxResponseHeaders defines constants for the HTMX Response headers.
@Get("/responseHeaders")
HttpResponse<?> htmxResponseHeaders(@NonNull HtmxRequestHeaders htmxRequestHeaders) {
return HttpResponse.ok().header(HtmxResponseHeaders.HX_REFRESH, StringUtils.TRUE);
}
@Get("/responseHeaders")
HttpResponse<?> htmxResponseHeaders(@NonNull HtmxRequestHeaders htmxRequestHeaders) {
HttpResponse.ok().header(HtmxResponseHeaders.HX_REFRESH, StringUtils.TRUE);
}
@Get("/responseHeaders")
fun htmxResponseHeaders(htmxRequestHeaders: @NonNull HtmxRequestHeaders): HttpResponse<*> {
return HttpResponse.ok<Any>().header(HtmxResponseHeaders.HX_REFRESH, StringUtils.TRUE)
}
3.5 Turbo
A set of complementary techniques for speeding up page changes and form submissions, dividing complex pages into components, and stream partial page updates over WebSocket. All without writing any JavaScript at all. And designed from the start to integrate perfectly with native hybrid applications for iOS and Android.
3.5.1 TurboFrameView annotation
Turbo fetch request includes an HTTP Header Turbo-Frame
when you click a link within a Turbo Frame.
For example, for a frame id message_1
and a response such as:
<!DOCTYPE html>
<html>
<head>
<title>Edit</title>
</head>
<body>
<h1>Editing message</h1>
<turbo-frame id="message_1">
<form action="/messages/1">
<input name="name" type="text" value="My message title">
<textarea name="content">My message content</textarea>
<input type="submit">
</form>
</turbo-frame>
</body>
</html>
Turbo extracts <turbo-frame id="message_1">
and it replaces the frame content from where the click originated. When Turbo updates the frame, it uses only content inside a matching <turbo-frame>
.
You can render a Turbo Frame easily by annotating a controller route with TurboFrameView and returning only the HTML used by Turbo.
<turbo-frame id="message_1">
<form action="/messages/1">
<input name="name" type="text" value="My message title">
<textarea name="content">My message content</textarea>
<input type="submit">
</form>
</turbo-frame>
The following example illustrates this behaviour with two Velocity templates and a controller.
<!DOCTYPE html>
<html>
<head>
<title>Edit</title>
</head>
<body>
<h1>Editing message</h1>
<turbo-frame id="message_$message.getId()">
#parse("views/form.vm")
</turbo-frame>
</body>
</html>
<form action="/messages/$message.getId()">
<input name="name" type="text" value="$message.getName()">
<textarea name="content">$message.getContent()</textarea>
<input type="submit">
</form>
@Produces(MediaType.TEXT_HTML)
@TurboFrameView("form")
@View("edit")
@Get
Map<String, Object> index() {
return Map.of("message", new Message(1L, "My message title", "My message content"));
}
@Produces(MediaType.TEXT_HTML)
@TurboFrameView("form")
@View("edit")
@Get
Map<String, Object> index() {
["message": new Message(id: 1L, name: "My message title", content: "My message content")]
}
@Produces(MediaType.TEXT_HTML)
@TurboFrameView("form")
@View("edit")
@Get
fun index() =
mapOf("message" to Message(1L, "My message title", "My message content"))
3.5.2 TurboFrame Builder
You can render Turbo Frames also by returning TurboFrame.Builder
from a controller.
@Produces(MediaType.TEXT_HTML)
@Get("/builder")
HttpResponse<?> index(@Nullable @Header(TurboHttpHeaders.TURBO_FRAME) String turboFrame) {
Long messageId = 1L;
Map<String, Object> model = Map.of(
"message", new Message(messageId, "My message title", "My message content")
);
return HttpResponse.ok(turboFrame == null ?
new ModelAndView<>("edit", model) :
TurboFrame.builder()
.id("message_" + messageId)
.templateModel(model)
.templateView("form"))
.contentType(MediaType.TEXT_HTML);
}
@Produces(MediaType.TEXT_HTML)
@Get("/builder")
HttpResponse<?> index(@Nullable @Header(TurboHttpHeaders.TURBO_FRAME) String turboFrame) {
Long messageId = 1L
Map<String, Object> model = [
"message": new Message(id: messageId, name: "My message title", content: "My message content")
]
HttpResponse.ok(turboFrame == null ? new ModelAndView("edit", model) :
TurboFrame.builder()
.id("message_" + messageId)
.templateModel(model)
.templateView("form")
).contentType(MediaType.TEXT_HTML)
}
@Produces(MediaType.TEXT_HTML)
@Get("/builder")
fun index(@Header(TurboHttpHeaders.TURBO_FRAME) turboFrame: String?): HttpResponse<*> {
val messageId = 1L
val model = mapOf("message" to Message(messageId, "My message title", "My message content"))
return HttpResponse.ok(
if (turboFrame == null)
ModelAndView<Any?>("edit", model)
else
TurboFrame.builder()
.id("message_$messageId")
.templateModel(model)
.templateView("form")
).contentType(MediaType.TEXT_HTML)
}
3.5.3 Turbo Stream Fluid API
You can render Turbo Streams by returning a TurboStream.Builder
from a controller.
TurboStream
features a fluid API:
TurboStream turboStream = TurboStream.builder()
.action(TurboStreamAction.APPEND)
.targetDomId("dom_id")
.template("Content to append to container designated with the dom_id.")
.build();
Optional<Writable> writable = turboStream.render();
TurboStream turboStream = TurboStream.builder()
.action(TurboStreamAction.APPEND)
.targetDomId("dom_id")
.template("Content to append to container designated with the dom_id.")
.build()
Optional<Writable> writable = turboStream.render();
val turboStream = TurboStream.builder()
.action(TurboStreamAction.APPEND)
.targetDomId("dom_id")
.template("Content to append to container designated with the dom_id.")
.build()
val writable = turboStream.render()
The previous example generates:
"<turbo-stream action=\"append\" target=\"dom_id\">" +
"<template>" +
"Content to append to container designated with the dom_id." +
"</template>" +
"</turbo-stream>"
'<turbo-stream action="append" target="dom_id">' +
'<template>' +
'Content to append to container designated with the dom_id.' +
'</template>' +
'</turbo-stream>'
"<turbo-stream action=\"append\" target=\"dom_id\">" +
"<template>" +
"Content to append to container designated with the dom_id." +
"</template>" +
"</turbo-stream>"
3.5.4 TurboView Annotation
You can render Turbo Streams easily by annotating a controller route with TurboView
@Produces(value = {MediaType.TEXT_HTML, TurboMediaType.TURBO_STREAM})
@TurboView(value = "fruit", action = TurboStreamAction.APPEND)
@Get("/turbofruit")
Map<String, Object> show() {
return Collections.singletonMap("fruit", new Fruit("Banana", "Yellow"));
}
@Produces(value = [MediaType.TEXT_HTML, TurboMediaType.TURBO_STREAM])
@TurboView(value = "fruit", action = TurboStreamAction.APPEND)
@Get("/turbofruit")
Map<String, Object> show() {
return Collections.singletonMap("fruit", new Fruit("Banana", "Yellow"));
}
@Produces(value = [MediaType.TEXT_HTML, TurboMediaType.TURBO_STREAM])
@TurboView(value = "fruit", action = TurboStreamAction.APPEND)
@Get("/turbofruit")
fun show() = mapOf("fruit" to Fruit("Banana", "Yellow"))
Given the HTTP Request:
HttpRequest<?> request = HttpRequest.GET("/turbofruit")
.accept(TurboMediaType.TURBO_STREAM, MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML)
.header(TurboHttpHeaders.TURBO_FRAME, "dom_id");
HttpRequest<?> request = HttpRequest.GET("/turbofruit")
.accept(TurboMediaType.TURBO_STREAM, MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML)
.header(TurboHttpHeaders.TURBO_FRAME, "dom_id");
val request: HttpRequest<*> = HttpRequest.GET<Any>("/turbofruit")
.accept(TurboMediaType.TURBO_STREAM, MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML)
.header(TurboHttpHeaders.TURBO_FRAME, "dom_id")
The previous controller returns:
"<turbo-stream action=\"append\" target=\"dom_id\">"+
"<template>" +
"<h1>fruit: Banana</h1>\n" +
"<h2>color: Yellow</h2>\n" +
"</template>" +
"</turbo-stream>"
'<turbo-stream action="append" target="dom_id">' +
'<template>' +
'<h1>fruit: Banana</h1>\n' +
'<h2>color: Yellow</h2>\n' +
'</template>' +
'</turbo-stream>'
"<turbo-stream action=\"append\" target=\"dom_id\">" +
"<template>" +
"<h1>fruit: Banana</h1>\n" +
"<h2>color: Yellow</h2>\n" +
"</template>" +
"</turbo-stream>"
with content type text/vnd.turbo-stream.html
.
3.5.5 Turbo Stream Media Type
You can use TurboMediaType to evaluate whether a request accepts Turbo Streams.
When submitting a <form> element whose method attribute is set to POST, PUT, PATCH, or DELETE, Turbo injects text/vnd.turbo-stream.html into the set of response formats in the request’s Accept header.
An example of handling a multipart form POST with Turbo and the Micronaut Framework would be:
@Consumes(MediaType.MULTIPART_FORM_DATA)
@TurboView("view")
@Produces(value = {MediaType.TEXT_HTML, TurboMediaType.TURBO_STREAM})
@Post("/messages/{id}")
Map<String, Object> processEdit(@Part int id, @Part String title, @Part String body) {
// Process the posted data, and return the updated message
return Map.of("message", new Message((long) id, title, body));
}
@Consumes(MediaType.MULTIPART_FORM_DATA)
@TurboView("view")
@Produces(value = [MediaType.TEXT_HTML, TurboMediaType.TURBO_STREAM])
@Post("/messages/{id}")
Map<String, Object> processEdit(@Part int id, @Part String title, @Part String body) {
// Process the posted data, and return the updated message
[message: new Message((long) id, title, body)]
}
@Consumes(MediaType.MULTIPART_FORM_DATA)
@TurboView("view")
@Produces(value = [MediaType.TEXT_HTML, TurboMediaType.TURBO_STREAM])
@Post("/messages/{id}")
fun processEdit(@Part id: Int, @Part title: String, @Part body: String): Map<String, Any> {
// Process the posted data, and return the updated message
return mapOf("message" to Message(id.toLong(), title, body))
}
3.5.6 Turbo Streams with Templates
You can use TurboStreamRenderer to render Turbo Streams with server side templates.
String view = "fruit";
Map<String, Object> model = Collections.singletonMap("fruit", new Fruit("Banana", "Yellow"));
TurboStream.Builder builder = TurboStream.builder()
.action(TurboStreamAction.APPEND)
.targetDomId("dom_id")
.template(view, model);
Optional<Writable> writable = turboStreamRenderer.render(builder, null);
String view = "fruit"
Map<String, Object> model = Collections.singletonMap("fruit", new Fruit("Banana", "Yellow"))
TurboStream.Builder builder = TurboStream.builder()
.action(TurboStreamAction.APPEND)
.targetDomId("dom_id")
.template(view, model)
Optional<Writable> writable = turboStreamRenderer.render(builder, null)
val view = "fruit"
val model = mapOf("fruit" to Fruit("Banana", "Yellow"))
val builder = TurboStream.builder()
.action(TurboStreamAction.APPEND)
.targetDomId("dom_id")
.template(view, model)
val writable = turboStreamRenderer.render(builder, null)
3.5.7 Guides
See the following guides to learn more about working with Turbo views in Micronaut Framework.
3.6 Security
3.6.1 Content Security Policy
Micronaut Views supports CSP (Content Security Policy, Level 3) out of the box. By default, CSP is disabled. To enable CSP, modify your configuration. For example:
micronaut.views.csp.enabled=true
micronaut:
views:
csp:
enabled: true
[micronaut]
[micronaut.views]
[micronaut.views.csp]
enabled=true
micronaut {
views {
csp {
enabled = true
}
}
}
{
micronaut {
views {
csp {
enabled = true
}
}
}
}
{
"micronaut": {
"views": {
"csp": {
"enabled": true
}
}
}
}
See the following table for all configuration options:
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether CSP is enabled. Default value (false). |
|
java.lang.String |
Sets the policy directives. |
|
boolean |
If true, the Content-Security-Policy-Report-Only header will be sent instead of Content-Security-Policy. Default value (false). |
|
java.util.Random |
The |
|
boolean |
If true, the CSP header will contain a generated nonce that is made available to view renderers. The nonce should change for each request/response cycle and can be used by views to authorize inlined script blocks. |
|
boolean |
Sets whether |
|
java.lang.String |
The path the CSP filter should apply to. Default value ("/**"). |
Nonce Support
At the developer’s option, Micronaut Views can generate a nonce with each render cycle. This nonce value can be used in
script-src
and style-src
directives in a CSP response header (note that nonce
values generally do not have any
effect when Content Security Policy is set via a <meta http-equiv>
tag).
To opt-in to this behavior, configure Micronaut Views with generateNonce
set to true
. Additionally, provide a spot for the
nonce value in your CSP directives, with the token {#nonceValue}
. It must be preceded by nonce-
and wrapped in
single quotes, as per the CSP3 spec:
micronaut.views.csp.enabled=true
micronaut.views.csp.generateNonce=true
micronaut.views.csp.policyDirectives=default-src https: self:; script-src 'nonce-{#nonceValue}';
micronaut:
views:
csp:
enabled: true
generateNonce: true
policyDirectives: "default-src https: self:; script-src 'nonce-{#nonceValue}';"
[micronaut]
[micronaut.views]
[micronaut.views.csp]
enabled=true
generateNonce=true
policyDirectives="default-src https: self:; script-src \'nonce-{#nonceValue}\';"
micronaut {
views {
csp {
enabled = true
generateNonce = true
policyDirectives = "default-src https: self:; script-src 'nonce-{#nonceValue}';"
}
}
}
{
micronaut {
views {
csp {
enabled = true
generateNonce = true
policyDirectives = "default-src https: self:; script-src 'nonce-{#nonceValue}';"
}
}
}
}
{
"micronaut": {
"views": {
"csp": {
"enabled": true,
"generateNonce": true,
"policyDirectives": "default-src https: self:; script-src 'nonce-{#nonceValue}';"
}
}
}
}
That’s it! After applying the above configuration, HTTP responses might include a header that look like this:
Content-Security-Policy: default-src https: self:; script-src 'nonce-4ze2IRazk4Yu/j5K6SEzjA';
The nonce value can be accessed on the server as a request attribute named cspNonce
. This is the value to use
in the nonce
attribute on script
and related tags. For example (adapt as appropriate for your template language):
<script type="text/javascript" src="/path/to/script.js" nonce="${cspNonce}" />
Inline scripts which aren’t otherwise whitelisted will be declined for execution, unless CSP is operating in report-only mode. Inline scripts can be whitelisted with the syntax:
<script type="text/javascript" nonce="4ze2IRazk4Yu/j5K6SEzjA">
// some javascript code here
</script>
3.6.2 Integration with Micronaut Security
The views project has integration with the Micronaut security project.
SecurityViewModelProcessor is enabled by default and injects the current username in the view.
SecurityViewModelProcessor is typed to Map<String, Object> . If are models are POJOs you will need to implement your own security ViewModelProcessor.
|
The following properties allow you to customize the injection:
Property | Type | Description |
---|---|---|
|
boolean |
Enable {@link SecurityViewModelProcessor}. Default value (true). |
|
java.lang.String |
Model key name. Default value ("security"). |
|
java.lang.String |
Nested security map key for the user’s name property. Default value ("name"). |
|
java.lang.String |
Nested security map key for the user’s attributes property. Default value ("attributes"). |
In a controller, you can return a model without specifying in the model the authenticated user:
@Controller("/")
public class BooksController {
@Secured(SecurityRule.IS_AUTHENTICATED)
@View("securitydecorator")
@Get
public Map<String, Object> index() {
Map<String, Object> model = new HashMap<>();
model.put("books", Arrays.asList("Developing Microservices"));
return model;
}
}
and still access the authenticated user in the view (for example a velocity template):
<!DOCTYPE html>
<html>
<head>
<title>User Books</title>
</head>
<body>
#if( $security )
<h1>User: ${security['name']} email: ${security['attributes']['email']}</h1>
#end
#foreach($book in $books)
$book
#end
#if( $securitycustom )
<h1>Custom: ${securitycustom['name']}</span></h1>
#end
</body>
</html>
You can access information about the current user with the security
map.
CSRF Token View Model Processor
If you use the Micronaut Security CSRF module, there is also a view model processor for a model of type Map<String, Object>
.
If a CSRF Token can be resolved, CSRFViewModelProcessor
adds it to the model.
The following properties allow you to customize the injection:
Property | Type | Description |
---|---|---|
|
java.lang.String |
Model key for CSRF Token. Default value ("csrfToken"). |
|
boolean |
Enable {@link CsrfViewModelProcessor}. Default value (true). |
3.7 GraalVM Support
The Micronaut framework supports GraalVM and starting in 1.2.0
special configuration files have been added to make easier the native image creation.
For Micronaut framework 2.0 the support for GraalVM has been improved and now it’s not necessary to declare any view.
This also applies if you serve static resources like
html, css and javascript files. Everything in the public
directory will also be added to the GraalVM resource configuration
automatically.
Learn more about Micronaut framework’s GraalVM support. |
4 Guides
See the following list of guides to learn more about working with Views in the Micronaut Framework:
5 Breaking Changes
This section outlines the breaking changes done in major versions of Micronaut Views.
4.0.0
-
The Soy dependency has been upgraded to version 2022-10-26, which includes breaking changes. Among others,
.
is no longer allowed before template names. To know the full changes, we suggest looking at the commits since there exist no release notes. -
To reduce the size of applications where Http requests are not required, the core library has been detached from the micronaut http library and no longer declares it as an api dependency. This change has added another Generic type to ViewRenderer, and breaks binary compatibility.
3.0.0
-
ViewModelProcessor no longer assumes a
Map<String,Object>
model and must be typed to the exact type of the model you would like to process. -
ViewsRenderer are now typed. Moreover, provided
ViewsRenderer
don’t specify@Produces(MediaType.TEXT_HTML)
and responses content type respect the content type defined for the route. -
Method
ViewsRenderer::render(String, T)
was removed. UseViewsRenderer::render(String, T, HttpRequest)
instead. -
Decoration of view model is now handled by ViewsModelDecorator and its default implementation DefaultViewsModelDecorator
-
Resolution of view name is now handled by ViewsResolver and its default implementation DefaultViewsResolver
-
Resolution of ViewsRenderer used to render a view is now handled by ViewsRendererLocator and its default implementation DefaultViewsRendererLocator
-
ViewsRenderer::modelOf
method has been moved toViewUtils::modelOf
-
Constant
EXTENSION_SEPARATOR
has been moved fromViewsRenderer
toViewUtils
2.0.0
-
The
micronaut-views
dependency is no longer published. Replace the dependency with the one specific to the view implementation being used. -
Deprecated classes, constructors, etc have been removed.
-
ViewsFilterOrderProvider has been removed. The view filter now always runs on
ServerFilterPhase.RENDERING
. -
Maven Group ID changed to
io.micronaut.views
6 Repository
You can find the source code of this project in this repository: