Spring Boot
View Resolver
ViewResolver is the Spring MVC component that maps a logical view name — a string returned by a controller method — to a concrete View implementation that renders the HTTP response. In server-side rendering applications built with Thymeleaf, FreeMarker, or JSP, the ViewResolver is what translates 'users/list' into the template file that produces the final HTML.
What ViewResolver Does
When a @Controller method returns a String, Spring MVC treats it as a logical view name and passes it to the ViewResolver chain. Each resolver in the chain attempts to resolve the name to a View object. The first successful resolution wins. The View then renders the response — writing HTML, XML, or another format to the HttpServletResponse output stream.
In a REST API built with @RestController, ViewResolver is never invoked — @ResponseBody causes the return value to be written directly to the response body via an HttpMessageConverter, bypassing view resolution entirely. ViewResolver is relevant only for server-side rendering (SSR) applications.
Java
// ── @Controller + view name → ViewResolver is invoked ─────────────────
@Controller
public class UserPageController {
@GetMapping("/users")
public String usersPage(Model model) {
model.addAttribute("users", userService.findAll());
return "users/list"; // logical view name → resolved to a template
}
}
// ── @RestController + @ResponseBody → ViewResolver is NOT invoked ──────
@RestController
public class UserApiController {
@GetMapping("/api/users")
public List<UserResponse> findAll() {
return userService.findAll(); // serialised to JSON — no view resolution
}
}
// ── Explicit redirect and forward — special view name prefixes ─────────
@Controller
public class RedirectController {
@PostMapping("/users")
public String create(@ModelAttribute CreateUserRequest request) {
userService.create(request);
return "redirect:/users"; // 302 redirect — no template rendered
}
@GetMapping("/legacy")
public String forward() {
return "forward:/users"; // server-side forward — no redirect
}
}Thymeleaf ViewResolver
Thymeleaf is the recommended template engine for Spring Boot server-side rendering. Adding spring-boot-starter-thymeleaf auto-configures ThymeleafViewResolver with sensible defaults — templates live in src/main/resources/templates/ and have a .html extension.
XML
<!-- pom.xml — add Thymeleaf starter: -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
# ── Auto-configured defaults (application.yml): ───────────────────────
spring:
thymeleaf:
prefix: classpath:/templates/ # template root directory
suffix: .html # file extension appended to view name
mode: HTML # template mode: HTML, XML, TEXT, JAVASCRIPT
encoding: UTF-8
cache: false # disable cache in dev (true in prod)
check-template-location: true # fail fast if template directory missing
# ── View name resolution: ─────────────────────────────────────────────
# Controller returns "users/list"
# → ThymeleafViewResolver resolves to:
# → classpath:/templates/users/list.html
# File layout:
# src/main/resources/
# └── templates/
# ├── layout.html ← shared layout fragment
# ├── users/
# │ ├── list.html ← resolved by "users/list"
# │ ├── detail.html ← resolved by "users/detail"
# │ └── form.html ← resolved by "users/form"
# └── error/
# ├── 404.html
# └── 500.htmlThymeleaf Template Basics
Thymeleaf templates are valid HTML files with th:* attributes that bind model data. The template engine processes th:* attributes server-side and removes them from the rendered output, producing clean HTML.
html
<!-- src/main/resources/templates/users/list.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Users</title>
</head>
<body>
<!-- th:text — set element text content from model attribute: -->
<h1 th:text="${pageTitle}">Users</h1>
<!-- th:each — iterate over a collection: -->
<table>
<tr th:each="user : ${users}">
<td th:text="${user.name}">Name</td>
<td th:text="${user.email}">Email</td>
<!-- th:href — build a URL: -->
<td><a th:href="@{/users/{id}(id=${user.id})}">View</a></td>
</tr>
</table>
<!-- th:if / th:unless — conditional rendering: -->
<p th:if="${users.empty}">No users found.</p>
<!-- th:href with query param: -->
<a th:href="@{/users(page=${currentPage + 1})}">Next</a>
<!-- th:fragment — define a reusable fragment: -->
<footer th:fragment="footer">
<p>© 2024 MyApp</p>
</footer>
</body>
</html>
<!-- Including a fragment from another template: -->
<div th:insert="~{layout :: footer}"></div>
<div th:replace="~{layout :: footer}"></div>InternalResourceViewResolver (JSP)
InternalResourceViewResolver resolves view names to JSP files. It is the legacy default for Spring MVC applications without a modern template engine. Spring Boot supports JSP but with limitations — it does not work with the embedded Tomcat JAR packaging without additional configuration and is generally not recommended for new projects.
XML
<!-- pom.xml — JSP support (not recommended for new projects): -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
</dependency>
# ── application.yml: ──────────────────────────────────────────────────
spring:
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
# ── View name resolution: ─────────────────────────────────────────────
# Controller returns "users/list"
# → InternalResourceViewResolver resolves to:
# → /WEB-INF/views/users/list.jsp
# ── Manual configuration (if not using application.yml): ──────────────
@Configuration
public class ViewConfig {
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver resolver =
new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setOrder(Ordered.LOWEST_PRECEDENCE);
return resolver;
}
}ContentNegotiatingViewResolver
ContentNegotiatingViewResolver delegates to other ViewResolvers based on the requested media type (Accept header or file extension). It selects the best matching View from all registered resolvers. Spring Boot auto-configures it when multiple view technologies are on the classpath.
Java
// ── ContentNegotiatingViewResolver — auto-configured by Spring Boot ────
// It wraps all other ViewResolvers and selects based on Accept header.
// Resolution order for GET /users/42:
// Accept: text/html → ThymeleafViewResolver → users/detail.html
// Accept: application/json → MappingJackson2JsonView → JSON body
// Accept: application/xml → MappingJackson2XmlView → XML body
// ── Manual configuration (if customisation is needed): ────────────────
@Configuration
public class ViewConfig {
@Bean
public ContentNegotiatingViewResolver contentNegotiatingViewResolver(
List<ViewResolver> resolvers) {
ContentNegotiatingViewResolver cnvr =
new ContentNegotiatingViewResolver();
cnvr.setViewResolvers(resolvers);
// Default views for non-HTML requests:
List<View> defaultViews = new ArrayList<>();
defaultViews.add(new MappingJackson2JsonView());
cnvr.setDefaultViews(defaultViews);
cnvr.setOrder(Ordered.HIGHEST_PRECEDENCE);
return cnvr;
}
}
// ── Controlling content negotiation in application.yml: ───────────────
spring:
mvc:
contentnegotiation:
favor-parameter: false # ?format=json — disabled by default
favor-path-extension: false # /users.json — deprecated, disabled
default-content-type: application/jsonViewResolver Priority and Chaining
Multiple ViewResolvers are evaluated in priority order — the first to return a non-null View wins. Setting the order correctly prevents lower-priority resolvers from intercepting view names meant for a higher-priority one.
Java
@Configuration
public class ViewResolverConfig {
// ── Thymeleaf — highest priority (resolves .html templates): ──────
// Auto-configured by spring-boot-starter-thymeleaf
// Default order: Ordered.LOWEST_PRECEDENCE - 5 (effectively first)
// ── BeanNameViewResolver — resolves view name to a View bean: ─────
@Bean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); // before Thymeleaf
return resolver;
}
// ── Named view bean — resolved by BeanNameViewResolver: ───────────
@Bean("usersExcelView")
public View usersExcelView() {
return new AbstractXlsView() {
@Override
protected void buildExcelDocument(Map<String, Object> model,
Workbook workbook, HttpServletRequest request,
HttpServletResponse response) {
Sheet sheet = workbook.createSheet("Users");
List<UserResponse> users = (List<UserResponse>) model.get("users");
// ... populate spreadsheet
}
};
}
// Controller returns "usersExcelView" → BeanNameViewResolver matches it
// before Thymeleaf attempts to find a .html template.
// ── InternalResourceViewResolver — always last: ────────────────────
// Must have lowest priority because it never returns null —
// it always resolves to a resource path even if the file doesn't exist.
@Bean
public InternalResourceViewResolver jspResolver() {
InternalResourceViewResolver resolver =
new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setOrder(Ordered.LOWEST_PRECEDENCE); // always last
return resolver;
}
}