0
# View Resolution and Templating
1
2
Spring WebFlux provides a comprehensive view resolution system supporting multiple template engines including FreeMarker and script templates for server-side rendering. The system integrates seamlessly with reactive streams and supports both traditional template rendering and modern JavaScript-based templating.
3
4
## Capabilities
5
6
### Core View Interfaces
7
8
Basic interfaces for view rendering and resolution in reactive web applications.
9
10
```java { .api }
11
interface View {
12
/**
13
* Return the supported media types for this view.
14
* @return list of supported media types
15
*/
16
List<MediaType> getSupportedMediaTypes();
17
18
/**
19
* Render the view given the specified model.
20
* @param model the model containing attributes for rendering
21
* @param contentType the content type selected for rendering
22
* @param exchange the current server exchange
23
* @return completion signal
24
*/
25
Mono<Void> render(Map<String, ?> model, MediaType contentType, ServerWebExchange exchange);
26
}
27
28
interface ViewResolver {
29
/**
30
* Resolve the given view name to a View instance.
31
* @param viewName the name of the view to resolve
32
* @param locale the locale for which to resolve the view
33
* @return the resolved view, or empty if not found
34
*/
35
Mono<View> resolveViewName(String viewName, Locale locale);
36
}
37
```
38
39
**Usage Examples:**
40
41
```java
42
// Custom view implementation
43
public class CustomJsonView implements View {
44
45
private final ObjectMapper objectMapper;
46
47
public CustomJsonView(ObjectMapper objectMapper) {
48
this.objectMapper = objectMapper;
49
}
50
51
@Override
52
public List<MediaType> getSupportedMediaTypes() {
53
return List.of(MediaType.APPLICATION_JSON);
54
}
55
56
@Override
57
public Mono<Void> render(Map<String, ?> model, MediaType contentType, ServerWebExchange exchange) {
58
try {
59
String json = objectMapper.writeValueAsString(model);
60
ServerHttpResponse response = exchange.getResponse();
61
62
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
63
DataBuffer buffer = response.bufferFactory().wrap(json.getBytes());
64
65
return response.writeWith(Mono.just(buffer));
66
} catch (Exception e) {
67
return Mono.error(e);
68
}
69
}
70
}
71
72
// Custom view resolver
73
@Component
74
public class CustomViewResolver implements ViewResolver {
75
76
private final Map<String, View> views = new HashMap<>();
77
78
public CustomViewResolver(ObjectMapper objectMapper) {
79
views.put("json", new CustomJsonView(objectMapper));
80
views.put("xml", new CustomXmlView());
81
}
82
83
@Override
84
public Mono<View> resolveViewName(String viewName, Locale locale) {
85
View view = views.get(viewName);
86
return view != null ? Mono.just(view) : Mono.empty();
87
}
88
}
89
```
90
91
### Rendering Interface
92
93
Public API for view rendering with model and status configuration.
94
95
```java { .api }
96
interface Rendering {
97
/**
98
* Return the view name to be resolved.
99
*/
100
String name();
101
102
/**
103
* Return the model attributes for rendering.
104
*/
105
Map<String, Object> modelAttributes();
106
107
/**
108
* Return the HTTP status code for the response.
109
*/
110
HttpStatusCode status();
111
112
/**
113
* Return the HTTP headers for the response.
114
*/
115
HttpHeaders headers();
116
117
/**
118
* Create a new rendering builder with the specified view name.
119
* @param name the view name
120
* @return the rendering builder
121
*/
122
static Builder with(String name);
123
}
124
```
125
126
**Rendering Builder Interface:**
127
128
```java { .api }
129
interface Builder {
130
/**
131
* Add a model attribute.
132
* @param name the attribute name
133
* @param value the attribute value
134
* @return this builder for chaining
135
*/
136
Builder modelAttribute(String name, Object value);
137
138
/**
139
* Add multiple model attributes.
140
* @param attributes the attributes to add
141
* @return this builder for chaining
142
*/
143
Builder modelAttributes(Object... attributes);
144
145
/**
146
* Set the HTTP status code.
147
* @param status the status code
148
* @return this builder for chaining
149
*/
150
Builder status(HttpStatus status);
151
152
/**
153
* Add an HTTP header.
154
* @param headerName the header name
155
* @param headerValues the header values
156
* @return this builder for chaining
157
*/
158
Builder header(String headerName, String... headerValues);
159
160
/**
161
* Configure HTTP headers.
162
* @param headersConsumer the headers consumer
163
* @return this builder for chaining
164
*/
165
Builder headers(Consumer<HttpHeaders> headersConsumer);
166
167
/**
168
* Build the rendering instance.
169
* @return the rendering instance
170
*/
171
Rendering build();
172
}
173
```
174
175
**Usage Examples:**
176
177
```java
178
@RestController
179
public class ViewController {
180
181
@GetMapping("/users/{id}")
182
public Mono<Rendering> showUser(@PathVariable String id) {
183
return userService.findById(id)
184
.map(user -> Rendering.with("user-profile")
185
.modelAttribute("user", user)
186
.modelAttribute("title", "User Profile")
187
.status(HttpStatus.OK)
188
.header("Cache-Control", "max-age=3600")
189
.build())
190
.switchIfEmpty(Mono.just(
191
Rendering.with("error/404")
192
.modelAttribute("message", "User not found")
193
.status(HttpStatus.NOT_FOUND)
194
.build()));
195
}
196
197
@GetMapping("/dashboard")
198
public Mono<Rendering> dashboard(Authentication auth) {
199
return Mono.zip(
200
userService.getCurrentUser(auth),
201
statisticsService.getUserStats(auth.getName()),
202
recentActivityService.getRecentActivity(auth.getName())
203
).map(tuple -> Rendering.with("dashboard")
204
.modelAttribute("user", tuple.getT1())
205
.modelAttribute("stats", tuple.getT2())
206
.modelAttribute("activities", tuple.getT3())
207
.build());
208
}
209
}
210
```
211
212
### Request Context
213
214
Context holder for request-specific information accessible in views and templates.
215
216
```java { .api }
217
class RequestContext {
218
/**
219
* Return the current ServerWebExchange.
220
*/
221
ServerWebExchange getExchange();
222
223
/**
224
* Return the model for the current request.
225
*/
226
Map<String, Object> getModel();
227
228
/**
229
* Return the locale for the current request.
230
*/
231
Locale getLocale();
232
233
/**
234
* Return the path variables for the current request.
235
*/
236
Map<String, String> getPathVariables();
237
}
238
```
239
240
**Usage Examples:**
241
242
```java
243
// Access request context in custom view
244
public class TemplateView implements View {
245
246
@Override
247
public Mono<Void> render(Map<String, ?> model, MediaType contentType, ServerWebExchange exchange) {
248
RequestContext context = new RequestContext(exchange, model);
249
250
// Use context in template processing
251
String locale = context.getLocale().toString();
252
String path = exchange.getRequest().getPath().value();
253
254
// Add context information to model
255
Map<String, Object> enhancedModel = new HashMap<>(model);
256
enhancedModel.put("requestPath", path);
257
enhancedModel.put("locale", locale);
258
enhancedModel.put("contextPath", exchange.getRequest().getPath().contextPath().value());
259
260
return renderTemplate(enhancedModel, exchange);
261
}
262
}
263
```
264
265
### FreeMarker Template Engine
266
267
Configuration and view classes for integrating FreeMarker templates with Spring WebFlux.
268
269
```java { .api }
270
class FreeMarkerConfigurer {
271
/**
272
* Set the FreeMarker Configuration to be used by this configurer.
273
* @param configuration the FreeMarker configuration
274
*/
275
void setConfiguration(Configuration configuration);
276
277
/**
278
* Set the location of the FreeMarker template files.
279
* @param templateLoaderPath the template loader path
280
*/
281
void setTemplateLoaderPath(String templateLoaderPath);
282
283
/**
284
* Set multiple locations for FreeMarker template files.
285
* @param templateLoaderPaths the template loader paths
286
*/
287
void setTemplateLoaderPaths(String... templateLoaderPaths);
288
289
/**
290
* Set whether to prefer file system access for template loading.
291
* @param preferFileSystemAccess true to prefer file system access
292
*/
293
void setPreferFileSystemAccess(boolean preferFileSystemAccess);
294
}
295
296
class FreeMarkerView implements View {
297
/**
298
* Set the FreeMarker Configuration to use.
299
* @param configuration the FreeMarker configuration
300
*/
301
void setConfiguration(Configuration configuration);
302
303
/**
304
* Set the encoding for template files.
305
* @param encoding the encoding to use
306
*/
307
void setEncoding(String encoding);
308
309
@Override
310
List<MediaType> getSupportedMediaTypes();
311
312
@Override
313
Mono<Void> render(Map<String, ?> model, MediaType contentType, ServerWebExchange exchange);
314
}
315
316
class FreeMarkerViewResolver implements ViewResolver {
317
/**
318
* Set the suffix for template files.
319
* @param suffix the file suffix (e.g., ".ftl")
320
*/
321
void setSuffix(String suffix);
322
323
/**
324
* Set the prefix for template files.
325
* @param prefix the file prefix
326
*/
327
void setPrefix(String prefix);
328
329
@Override
330
Mono<View> resolveViewName(String viewName, Locale locale);
331
}
332
```
333
334
**Usage Examples:**
335
336
```java
337
@Configuration
338
public class FreeMarkerConfig {
339
340
@Bean
341
public FreeMarkerConfigurer freeMarkerConfigurer() {
342
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
343
configurer.setTemplateLoaderPaths("classpath:/templates/", "file:/var/templates/");
344
configurer.setPreferFileSystemAccess(false);
345
346
// Custom FreeMarker configuration
347
Configuration config = new Configuration(Configuration.VERSION_2_3_31);
348
config.setDefaultEncoding("UTF-8");
349
config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
350
config.setLogTemplateExceptions(false);
351
352
configurer.setConfiguration(config);
353
return configurer;
354
}
355
356
@Bean
357
public FreeMarkerViewResolver freeMarkerViewResolver() {
358
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
359
resolver.setPrefix("");
360
resolver.setSuffix(".ftl");
361
resolver.setContentType(MediaType.TEXT_HTML);
362
resolver.setExposeRequestAttributes(true);
363
resolver.setExposeSessionAttributes(true);
364
resolver.setExposeSpringMacroHelpers(true);
365
return resolver;
366
}
367
}
368
369
// Controller using FreeMarker views
370
@Controller
371
public class HomeController {
372
373
@GetMapping("/")
374
public Mono<String> home(Model model) {
375
return dataService.getHomePageData()
376
.doOnNext(data -> {
377
model.addAttribute("data", data);
378
model.addAttribute("timestamp", Instant.now());
379
})
380
.then(Mono.just("home")); // Resolves to home.ftl
381
}
382
}
383
```
384
385
### Script Template Engine
386
387
Support for JavaScript-based template engines like Handlebars, Mustache, and React server-side rendering.
388
389
```java { .api }
390
class ScriptTemplateConfigurer {
391
/**
392
* Set the ScriptEngine to use.
393
* @param engine the script engine
394
*/
395
void setEngine(ScriptEngine engine);
396
397
/**
398
* Set the engine name for ScriptEngine lookup.
399
* @param engineName the name of the script engine
400
*/
401
void setEngineName(String engineName);
402
403
/**
404
* Set the scripts to load on engine initialization.
405
* @param scriptNames the script file names
406
*/
407
void setScripts(String... scriptNames);
408
409
/**
410
* Set the render object name in the script context.
411
* @param renderObject the render object name
412
*/
413
void setRenderObject(String renderObject);
414
415
/**
416
* Set the render function name to call.
417
* @param renderFunction the render function name
418
*/
419
void setRenderFunction(String renderFunction);
420
421
/**
422
* Set the character encoding for template files.
423
* @param charset the character encoding
424
*/
425
void setCharset(Charset charset);
426
}
427
428
class ScriptTemplateView implements View {
429
/**
430
* Set the ScriptEngine to use.
431
* @param engine the script engine
432
*/
433
void setEngine(ScriptEngine engine);
434
435
/**
436
* Set the engine name for lookup.
437
* @param engineName the engine name
438
*/
439
void setEngineName(String engineName);
440
441
/**
442
* Set the script resources to load.
443
* @param scripts the script resources
444
*/
445
void setScripts(String... scripts);
446
447
/**
448
* Set the render object name.
449
* @param renderObject the render object name
450
*/
451
void setRenderObject(String renderObject);
452
453
/**
454
* Set the render function name.
455
* @param renderFunction the render function name
456
*/
457
void setRenderFunction(String renderFunction);
458
459
@Override
460
List<MediaType> getSupportedMediaTypes();
461
462
@Override
463
Mono<Void> render(Map<String, ?> model, MediaType contentType, ServerWebExchange exchange);
464
}
465
466
class ScriptTemplateViewResolver implements ViewResolver {
467
/**
468
* Set the view name prefix.
469
* @param prefix the view name prefix
470
*/
471
void setPrefix(String prefix);
472
473
/**
474
* Set the view name suffix.
475
* @param suffix the view name suffix
476
*/
477
void setSuffix(String suffix);
478
479
@Override
480
Mono<View> resolveViewName(String viewName, Locale locale);
481
}
482
```
483
484
**Usage Examples:**
485
486
```java
487
@Configuration
488
public class ScriptTemplateConfig {
489
490
@Bean
491
public ScriptTemplateConfigurer mustacheConfigurer() {
492
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
493
configurer.setEngineName("nashorn");
494
configurer.setScripts("classpath:mustache.js");
495
configurer.setRenderObject("Mustache");
496
configurer.setRenderFunction("render");
497
return configurer;
498
}
499
500
@Bean
501
public ScriptTemplateViewResolver mustacheViewResolver() {
502
ScriptTemplateViewResolver resolver = new ScriptTemplateViewResolver();
503
resolver.setPrefix("classpath:/templates/");
504
resolver.setSuffix(".mustache");
505
return resolver;
506
}
507
}
508
509
// React SSR configuration
510
@Configuration
511
public class ReactSsrConfig {
512
513
@Bean
514
public ScriptTemplateConfigurer reactConfigurer() {
515
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
516
configurer.setEngineName("nashorn");
517
configurer.setScripts(
518
"classpath:polyfill.js",
519
"classpath:react.js",
520
"classpath:react-dom-server.js",
521
"classpath:app-bundle.js"
522
);
523
configurer.setRenderFunction("renderReactComponent");
524
configurer.setCharset(StandardCharsets.UTF_8);
525
return configurer;
526
}
527
}
528
529
// Controller using script templates
530
@Controller
531
public class PageController {
532
533
@GetMapping("/product/{id}")
534
public Mono<String> productPage(@PathVariable String id, Model model) {
535
return productService.findById(id)
536
.doOnNext(product -> {
537
model.addAttribute("product", product);
538
model.addAttribute("pageTitle", "Product: " + product.getName());
539
})
540
.then(Mono.just("product-page")); // Resolves to product-page.mustache
541
}
542
}
543
```
544
545
### View Resolution Result Handler
546
547
Handler for processing view rendering results and writing template responses.
548
549
```java { .api }
550
class ViewResolutionResultHandler implements HandlerResultHandler {
551
/**
552
* Create a new ViewResolutionResultHandler.
553
* @param viewResolvers the view resolvers to use
554
* @param contentTypeResolver the content type resolver
555
*/
556
ViewResolutionResultHandler(List<ViewResolver> viewResolvers,
557
RequestedContentTypeResolver contentTypeResolver);
558
559
@Override
560
boolean supports(HandlerResult result);
561
562
@Override
563
Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result);
564
}
565
```
566
567
**Usage Examples:**
568
569
```java
570
@Configuration
571
public class ViewConfig {
572
573
@Bean
574
public ViewResolutionResultHandler viewResolutionResultHandler(
575
List<ViewResolver> viewResolvers,
576
RequestedContentTypeResolver contentTypeResolver) {
577
578
ViewResolutionResultHandler handler = new ViewResolutionResultHandler(
579
viewResolvers, contentTypeResolver);
580
581
// Configure default views for content negotiation
582
handler.setDefaultViews(List.of(
583
new MappingJackson2JsonView(),
584
new MappingJackson2XmlView()
585
));
586
587
return handler;
588
}
589
590
@Bean
591
@Order(1)
592
public ViewResolver primaryViewResolver() {
593
return new FreeMarkerViewResolver();
594
}
595
596
@Bean
597
@Order(2)
598
public ViewResolver fallbackViewResolver() {
599
return new ScriptTemplateViewResolver();
600
}
601
}
602
```
603
604
### Advanced View Features
605
606
Additional view resolution features and customization options.
607
608
```java { .api }
609
// URL-based view resolver registration
610
class UrlBasedViewResolverRegistration {
611
UrlBasedViewResolverRegistration prefix(String prefix);
612
UrlBasedViewResolverRegistration suffix(String suffix);
613
UrlBasedViewResolverRegistration viewClass(Class<?> viewClass);
614
UrlBasedViewResolverRegistration viewNames(String... viewNames);
615
UrlBasedViewResolverRegistration attributes(Map<String, ?> attributes);
616
}
617
618
// Content negotiating view resolver
619
class ContentNegotiatingViewResolver implements ViewResolver {
620
void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager);
621
void setViewResolvers(List<ViewResolver> viewResolvers);
622
void setDefaultViews(List<View> defaultViews);
623
624
@Override
625
Mono<View> resolveViewName(String viewName, Locale locale);
626
}
627
```
628
629
**Usage Examples:**
630
631
```java
632
// Advanced view resolver configuration
633
@Configuration
634
public class AdvancedViewConfig {
635
636
@Bean
637
public ContentNegotiatingViewResolver contentNegotiatingViewResolver(
638
ContentNegotiationManager contentNegotiationManager,
639
List<ViewResolver> viewResolvers) {
640
641
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
642
resolver.setContentNegotiationManager(contentNegotiationManager);
643
resolver.setViewResolvers(viewResolvers);
644
645
// Default views for different content types
646
resolver.setDefaultViews(List.of(
647
new MappingJackson2JsonView(), // application/json
648
new MappingJackson2XmlView(), // application/xml
649
new AtomFeedView(), // application/atom+xml
650
new RssChannelView() // application/rss+xml
651
));
652
653
return resolver;
654
}
655
656
@Bean
657
public ContentNegotiationManager contentNegotiationManager() {
658
ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();
659
factory.addMediaType("json", MediaType.APPLICATION_JSON);
660
factory.addMediaType("xml", MediaType.APPLICATION_XML);
661
factory.addMediaType("atom", MediaType.APPLICATION_ATOM_XML);
662
factory.addMediaType("rss", MediaType.APPLICATION_RSS_XML);
663
factory.setDefaultContentType(MediaType.TEXT_HTML);
664
factory.afterPropertiesSet();
665
return factory.getObject();
666
}
667
}
668
669
// Multi-format controller
670
@Controller
671
public class ApiController {
672
673
@GetMapping("/api/users")
674
public Mono<String> users(Model model) {
675
return userService.findAll()
676
.collectList()
677
.doOnNext(users -> model.addAttribute("users", users))
678
.then(Mono.just("users")); // Content negotiation will select format
679
}
680
}
681
```