Sử dụng nhiều ViewResolver trong Spring Boot

Xem thêm các chuyên mục:

1- Mục tiêu của bài viết

Thông thường trong ứng dụng Spring bạn thường chỉ cần sử dụng duy nhất một công nghệ cho tầng View, nó có thể là Thymeleaf, JSP hoặc FreeMarker,... Tuy nhiên bạn cũng có thể sử dụng cùng một lúc nhiều công nghệ cho tầng View. Trong bài viết này tôi hướng dẫn bạn tạo một ứng dụng như vậy.
OK, chúng ta sẽ tạo một ứng dụng sử dụng đồng thời cả 3 công nghệ Thymeleaf, JSP & FreeMarker cho tầng View.

2- Tao dự án Spring Boot

3- Cấu hình pom.xml

Để có thể sử dụng JSP, Thymeleaf, FreeMarker bạn cần có các thư viện sau:
<!-- Thymeleaf -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- FreeMarker -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- Web -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JSP -->
<dependency>
   <groupId>org.apache.tomcat.embed</groupId>
   <artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!-- JSTL -->
<dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>jstl</artifactId>
</dependency>
Nội dung đầy đủ của tập tin pom.xml:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
      http://maven.apache.org/xsd/maven-4.0.0.xsd">
      
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.o7planning</groupId>
    <artifactId>SpringBootMultiViewResolver</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringBootMultiViewResolver</name>
    <description>Spring Boot + Multi ViewResolver</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        
        <!-- Thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
        <!-- FreeMarker -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>        
        
        <!-- Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- JSP -->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>

        <!-- JSTL -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>
        

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

4- Cấu hình các ViewResolver

Khi phát triển một dự án Spring Boot, bạn thường chỉ sử dụng duy nhất một công nghệ cho tầng View ( JSP, Thymeleaf, ..), Spring Boot sẽ tự động cấu hình cho bạn một ViewResolver để bạn làm việc với công nghệ đó. Tuy nhiên trong trường hợp bạn sử dụng nhiều công nghệ cho tầng View, bạn phải tự cấu hình tất cả các ViewResolver cần thiết.
Đây là hình ảnh minh họa luồng đi (Flow) của một ứng dụng Spring trong trường hợp bạn sử dụng 1 ViewResolver.
Trong trường hợp bạn sử dụng nhiều công nghệ cho tầng View, sẽ có nhiều ViewResolver tham gia vào luồng đi (flow) của ứng dụng. Các ViewResolver được sắp xếp theo thứ tự ưu tiên (0, 1, 2, ..). Nếu ViewResolver (0) không tìm thấy "View Name" cần thiết, ViewResolver (1) sẽ được sử dụng, ...

Cấu hình Thymeleaf ViewResolver.

ThymeleafViewResolverConfig.java
package org.o7planning.sbmultiviewresolver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;

@Configuration
public class ThymeleafViewResolverConfig {

    @Bean
    public ViewResolver thymeleafViewResolver() {

        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();

        viewResolver.setTemplateEngine(thymeleafTemplateEngine());
        viewResolver.setCharacterEncoding("UTF-8");
        viewResolver.setOrder(0);

        // Important!!
        // th_page1.html, th_page2.html, ...
        viewResolver.setViewNames(new String[] { "th_*" });

        return viewResolver;
    }

    // Thymeleaf template engine with Spring integration
    @Bean
    public SpringTemplateEngine thymeleafTemplateEngine() {

        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(thymeleafTemplateResolver());
        templateEngine.setEnableSpringELCompiler(true);

        return templateEngine;
    }

    @Bean
    public SpringResourceTemplateResolver springResourceTemplateResolver() {
        return new SpringResourceTemplateResolver();
    }

    // Thymeleaf template resolver serving HTML 5
    @Bean
    public ITemplateResolver thymeleafTemplateResolver() {

        ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();

        templateResolver.setPrefix("templates/");
        templateResolver.setCacheable(false);
        templateResolver.setSuffix(".html");
        templateResolver.setTemplateMode(TemplateMode.HTML);
        templateResolver.setCharacterEncoding("UTF-8");

        return templateResolver;
    }
 
}
Trong ứng dụng này chúng ta cấu hình Thymeleaf ViewResolver có độ ưu tiên cao nhất (order = 0).
Chú ý: Thymeleaf ViewResolver sẽ ném ra ngoại lệ khi không tìm thấy "View Name" cần thiết (Một tập tin html cần thiết). Nó trái ngược với mong muốn của bạn là ViewResolver có độ ưu tiên kế tiếp sẽ được sử dụng. Vì vậy bạn cần thiết lập quy tắc cho các "View Name" sẽ được phục vụ bởi Thymeleaf ViewResolver.
// Important!!
// th_page1.html, th_page2.html, ...
viewResolver.setViewNames(new String[] { "th_*" });

Cấu hình FreeMarker ViewResolver.

FreeMarkerViewResolverConfig.java
package org.o7planning.sbmultiviewresolver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;

@Configuration
public class FreeMarkerViewResolverConfig {

    @Bean(name = "viewResolver")
    public ViewResolver getViewResolver() {
        FreeMarkerViewResolver viewResolver = new FreeMarkerViewResolver();

        viewResolver.setCache(true);
        viewResolver.setPrefix("/freemarker/");
        viewResolver.setSuffix(".ftl");
        viewResolver.setOrder(1);
        return viewResolver;
    }

    @Bean(name = "freemarkerConfig")
    public FreeMarkerConfigurer getFreemarkerConfig() {
        FreeMarkerConfigurer config = new FreeMarkerConfigurer();

        // Folder containing FreeMarker templates.
        // 1 - "/WEB-INF/views/"
        // 2 - "classpath:/templates"
        config.setTemplateLoaderPath("classpath:/templates");
        return config;
    }
    
}
Nguyên tắc hoạt động của FreeMarker ViewResolver:

Cấu hình JSP ViewResolver.

Chú ý: JSP ViewResolver phải được thiết lập với độ ưu tiên thấp nhất. Điều này được ghi chú trong các tài liệu của Spring.
JspViewResolverConfig.java
package org.o7planning.sbmultiviewresolver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

@Configuration
public class JspViewResolverConfig {

    @Bean
    public ViewResolver jspViewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        
        viewResolver.setViewClass(JstlView.class);
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");
        viewResolver.setContentType("text/html");
        
        // Make sure > Thymeleaf order & FreeMarker order.
        viewResolver.setOrder(1000);
        
        return viewResolver;
    }
    
}
Nguyên tắc hoạt động của JSP ViewResolver:

5- Controller

MainController.java
package org.o7planning.sbmultiviewresolver.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class MainController {

    @RequestMapping(value = { "/testJsp" }, method = RequestMethod.GET)
    public String testJspView() {

        return "testJsp";
    }

    @RequestMapping(value = { "/testThymeleaf" }, method = RequestMethod.GET)
    public String testThymeleafView() {

        return "th_page1";
    }
    
    @RequestMapping(value = { "/testFreeMarker" }, method = RequestMethod.GET)
    public String testFreeMarkerView() {

        return "testFreeMarker";
    }
    
}

6- Views

th_page1.html (Thymeleaf)
<!DOCTYPE html>

<html lang="en">
   <head>
       <title>Thymeleaf</title>
   </head>
   <body>
   
      <h2>Thymeleaf Page</h2>
      <p>templates/th_page1.html</p>
      
   </body>
</html>
freemaker/testFreeMarker.ftl (FreeMarker)
<!DOCTYPE html>

<html lang="en">
   <head>
       <title>FreeMarker</title>
   </head>
   <body>
   
      <h2>FreeMarker Page</h2>
      <p>templates/freemarker/testFreeMarker.ftl</p>
      
   </body>
</html>
testJsp.jsp
<!DOCTYPE html>

<html lang="en">
   <head>
       <title>JSP</title>
   </head>
   <body>
   
      <h2>JSP Page</h2>
      <p>WEB-INF/jsp/testJsp.jsp</p>
      
   </body>
</html>

Xem thêm các chuyên mục: