Spring BootView Resolver
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.html

Thymeleaf 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/json

ViewResolver 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;
    }
}