2
votes

I have a Spring Boot 2.1.6 application (Spring 5), and I'd like to use Thymeleaf as my templating engine. I followed online tutorials to setup my project, the views and the controllers, and when I wanted to start it up I noticed that Thymeleaf complains that it is unable to find any templates:

2019-07-12T17:14:25,269 WARN  [main] o.s.b.a.t.ThymeleafAutoConfiguration$DefaultTemplateResolverConfiguration: Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)

I think I setup the project as it should be (at least according to the tutorials and forums I could find):

src/main/
    java/
        a.b.c.MyController
        rest of the classes and packages
    resources/
        static/
            css/
                bootstrap.min.css
                main.css
            js/
                bootstrap.min.js
                jquery-3.4.1.min.js
                login.js
                main.js
        templates/
            login.html
            main.html

My controller looks like this:

@ApiOperation(value = "Get login page", nickname = "login", notes = "", tags = { "My App", })
@ApiResponses(value = { @ApiResponse(code = 200, message = "Success") })
@GetMapping(value = { "/", "/login" })
@ResponseStatus(code = HttpStatus.OK)
public String login(Model model, String error, String logout) {
    if (error != null) {
        model.addAttribute("error", "Your username and/or password is invalid.");
    }

    if (logout != null) {
        model.addAttribute("message", "You have been logged out successfully.");
    }

    return "login";
}

The login.html looks like this:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>My App :: Login</title>
        <link rel="stylesheet" type="text/css" href="@{/css/bootstrap.min.css}">
        <link rel="stylesheet" type="text/css" href="@{/css/main.css}">
    </head>
    <body>
        <h1>My App</h1>

        <div class="container">
            <form id="userform" method="post"  action="#" th:action="@{/authenticate}" th:object="${userForm}" class="form-signin">
                <h2 class="form-heading">Log In</h2>

                <span>${message}</span>

                <div class="form-group ${status.error ? 'has-error' : ''}">
                    <input type="text" class="form-control" placeholder="Username" autofocus th:field="*{username}"></input>
                </div>

                <input name="password" id="password" type="password" class="form-control" placeholder="Password" th:field="*{password}"/>

                <input type="hidden" th:name="${ _csrf.parameterName }" th:value="${ _csrf.token }"/>
                <button class="btn btn-lg btn-primary btn-block" type="submit">Log In</button>
            </form>
        </div>

        <script src="@{/js/jquery-3.4.1.min.js}"></script>
        <script src="@{/js/bootstrap.min.js}"></script>
        <script src="@{/js/login.js}"></script>
    </body>
</html>

When I open the login page, I get a simple HTML page, with the word "login" written in the body.

I find this strange, Thymeleaf is looking for the templates in 'classpath:/templates/', which should be correct, because I have log4j2 XML configured in application.properties as 'logging.config=classpath:log4j2-${spring.profiles.active}.xml', and this XML is found in the same src/main/resources folder. So what can be the reason why the templates folder is not found there?

Update:

I forgot to mention: I tried to run it from Eclipse as a Spring Boot app, and also tried to run it with Maven as mvn spring-boot:run, with same results.

Also, I'm using Java 12. My pom.xml looks like this:

<?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>my.groupid</groupId>
    <artifactId>my.artifactid</artifactId>
    <packaging>war</packaging>
    <name>MyApp</name>
    <version>${baseversion}.${gitcommitcount}.${buildnumber}</version>
    <description>My App</description>

    <properties>
        <baseversion>1.0.0</baseversion>
        <java.version>12</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <springfox-version>2.9.2</springfox-version>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>

        <buildnumber>0</buildnumber>
        <gitcommitcount>0</gitcommitcount>
    </properties>

    <distributionManagement>
        <repository>
            <id>id</id>
            <name>Internal Local Releases</name>
            <url>http://x.x.x.x:xxxx/repository/local_release/</url>
        </repository>
        <snapshotRepository>
            <id>id</id>
            <name>Internal Local Snapshots</name>
            <url>http://x.x.x.x:xxxx/repository/local_snapshot/</url>
        </snapshotRepository>
    </distributionManagement>

    <repositories>
        <repository>
            <id>id</id>
            <url>x.x.x.x:xxxx/repository/local_group/</url>
        </repository>
    </repositories>

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

    <build>
        <finalName>${project.name}</finalName>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <excludes>
                    <!--
                    Will need to be excluded from final WAR
                    <exclude>*.properties</exclude>
                    <exclude>*.xml</exclude>
                    -->
                </excludes>
                <includes>
                    <!-- Include is only for running locally -->
                    <include>*.properties</include>
                    <include>*.xml</include>
                </includes>
            </resource>
        </resources>
        <sourceDirectory>src/main/java</sourceDirectory>

        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <release>12</release>
                    <compilerArgs>
                        <arg>--enable-preview</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <argLine>--enable-preview</argLine>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-failsafe-plugin</artifactId>
                <configuration>
                    <argLine>--enable-preview</argLine>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                            <goal>build-info</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <jvmArguments>--enable-preview</jvmArguments>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <configuration>
                    <archiveClasses>false</archiveClasses>
                    <warSourceDirectory>WebContent</warSourceDirectory>
                    <archive>
                        <manifestEntries>
                            <Built-On>${maven.build.timestamp} UTC</Built-On>
                            <ModuleName>${project.name}</ModuleName>
                            <ModuleVersion>${project.version}</ModuleVersion>
                        </manifestEntries>
                        <manifestSections>
                            <manifestSection>
                                <name>Release section</name>
                                <manifestEntries>
                                    <BaseVersion>${baseversion}</BaseVersion>
                                    <BuildNumber>${buildnumber}</BuildNumber>
                                    <GITRevision>${gitrevision}</GITRevision>
                                </manifestEntries>
                            </manifestSection>
                        </manifestSections>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>rename-wars</id>
                        <phase>install</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <executable>scripts/rename-wars.bat</executable>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- Logging -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <!--SpringFox dependencies -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${springfox-version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${springfox-version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.joschi.jackson</groupId>
            <artifactId>jackson-datatype-threetenbp</artifactId>
            <version>2.6.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.6</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.10.6</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.10.6</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.tomcat</groupId>
                    <artifactId>tomcat-jdbc</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- Commons HttpClient -->
        <dependency>
            <groupId>commons-httpclient</groupId>
            <artifactId>commons-httpclient</artifactId>
            <version>3.1</version>
        </dependency>
        <!-- Commons IO -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
        <!-- Oracle JDBC -->
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc8</artifactId>
            <version>12.2.0.1.0</version>
        </dependency>
        <!-- CSV parsing -->
        <dependency>
            <groupId>com.opencsv</groupId>
            <artifactId>opencsv</artifactId>
            <version>4.6</version>
        </dependency>
        <!-- Javax Mail for email validation -->
        <dependency>
            <groupId>javax.mail</groupId>
            <artifactId>mail</artifactId>
            <version>1.4.7</version>
        </dependency>
        <!-- Configuration -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Actuator to gather metrics and health -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- JSON -->
        <dependency>
            <groupId>org.json</groupId>
            <artifactId>json</artifactId>
            <version>20180813</version>
        </dependency>
        <!-- Testing dependencies -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>com.vaadin.external.google</groupId>
                    <artifactId>android-json</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.restdocs</groupId>
            <artifactId>spring-restdocs-mockmvc</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

Update 2: application.properties

server.port=9080
spring.profiles.active=dev
spring.jackson.date-format=a.b.c.RFC3339DateFormat
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false
logging.config=classpath:log4j2-${spring.profiles.active}.xml

# Setting session timeout
server.servlet.session.timeout=10m

# ThymeLeaf settings
spring.thymeleaf.cache=false
spring.thymeleaf.check-template=true
spring.thymeleaf.check-template-location=true
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

# dbcp2 settings
spring.datasource.dbcp2.test-while-idle=true
spring.datasource.dbcp2.test-on-borrow=true
spring.datasource.dbcp2.test-on-return=false
spring.datasource.dbcp2.validation-query=select 1 from dual
spring.datasource.dbcp2.validation-query-timeout=30000
spring.datasource.dbcp2.time-between-eviction-runs-millis=30000
spring.datasource.dbcp2.min-evictable-idle-time-millis=30000
spring.datasource.dbcp2.initial-size=10
spring.datasource.dbcp2.max-total=20
spring.datasource.dbcp2.pool-prepared-statements=true
spring.datasource.dbcp2.log-abandoned=true
spring.datasource.dbcp2.log-expired-connections=true
spring.datasource.dbcp2.max-wait-millis=1000
spring.datasource.dbcp2.remove-abandoned-on-borrow=true
spring.datasource.dbcp2.remove-abandoned-on-maintenance=true
spring.datasource.dbcp2.remove-abandoned-timeout=60
spring.datasource.dbcp2.num-tests-per-eviction-run=3
spring.datasource.dbcp2.default-auto-commit=true

# File upload settings
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=-1
spring.servlet.multipart.max-request-size=-1

# Actuator settings

# Actuator endpoint settings
management.endpoint.shutdown.enabled=true
management.endpoint.health.enabled=true
management.endpoint.health.show-details=always
management.endpoint.metrics.enabled=true
management.endpoint.loggers.enabled=true
management.endpoint.info.enabled=true
management.endpoints.web.exposure.include=health,metrics,loggers,info
management.health.cassandra.enabled=false
management.health.couchbase.enabled=false
management.health.db.enabled=true
management.health.diskspace.enabled=true
management.health.diskspace.path=/
management.health.elasticsearch.enabled=false
management.health.influxdb.enabled=false
management.health.ldap.enabled=false
management.health.mail.enabled=false
management.health.mongo.enabled=false
management.health.neo4j.enabled=false
management.health.rabbit.enabled=false
management.health.redis.enabled=false
management.health.solr.enabled=false

# App info for actuator
info.app.name=My App
info.app.description=My App
info.app.version=1.0.0
info.customer=My App

Update 3: added Template and View resolvers as follows:

@Configuration
public class TemplateBeans implements WebMvcConfigurer {

    @Autowired
    private ServletContext servletContext;

    @Bean
    @Description("Thymeleaf template resolver serving HTML5")
    public ServletContextTemplateResolver templateResolver() {
        ServletContextTemplateResolver servletContextTemplateResolver = new ServletContextTemplateResolver(
                servletContext);

        servletContextTemplateResolver.setPrefix("classpath:/templates/");
        servletContextTemplateResolver.setCacheable(false);
        servletContextTemplateResolver.setSuffix(".html");
        servletContextTemplateResolver.setTemplateMode("HTML5");
        servletContextTemplateResolver.setCharacterEncoding("UTF-8");

        return servletContextTemplateResolver;
    }

    @Bean
    @Description("Thymeleaf template engine with Spring integration")
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine();

        springTemplateEngine.setTemplateResolver(templateResolver());

        return springTemplateEngine;
    }

    @Bean
    @Description("Thymeleaf view resolver")
    public ViewResolver viewResolver() {
        ThymeleafViewResolver thymeleafViewResolver = new ThymeleafViewResolver();

        thymeleafViewResolver.setTemplateEngine(templateEngine());
        thymeleafViewResolver.setCharacterEncoding("UTF-8");

        return thymeleafViewResolver;
    }
}

With this, I get exception:

2019-07-15T14:43:21,382 DEBUG [http-nio-9080-exec-3] o.s.w.s.FrameworkServlet: Failed to complete request: org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "/classpath:/templates/login.html")
2019-07-15T14:43:21,389 ERROR [http-nio-9080-exec-3] o.a.j.l.DirectJDKLog: Servlet.service() for servlet [dispatcherServlet] in context with path [/AICGDPR] threw exception [Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "/classpath:/templates/login.html")] with root cause
java.io.FileNotFoundException: ServletContext resource "/classpath:/templates/login.html" does not exist

I also tried with ClassLoaderTemplateResolver instead of ServletContextTemplateResolver, I got a somewhat different exception:

2019-07-15T14:48:54,208 DEBUG [http-nio-9080-exec-1] o.s.w.s.FrameworkServlet: Failed to complete request: org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "classpath:/templates/login.html")
2019-07-15T14:48:54,217 ERROR [http-nio-9080-exec-1] o.a.j.l.DirectJDKLog: Servlet.service() for servlet [dispatcherServlet] in context with path [/AICGDPR] threw exception [Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "classpath:/templates/login.html")] with root cause
java.io.FileNotFoundException: ClassLoader resource "classpath:/templates/login.html" could not be resolved
3
Can you add your applications properties/yaml files?Mike
Are you saying everything works fine but your are seeing Warning message ?want2learn
@want2learn No, the template is not opened, instead I only see the string "login" in the HTML body, not the "login.html" template. But I figured it's for the same reason that Thymeleaf logs that warning that the templates are not found.Gábor Major
Just wondering if you are using restcontroller annotation. Make sure you are using controller annotation.want2learn
@want2learn I used RestController annotation, good call, after changing to Controller, I am getting an error saying that the template cannot be resolved. So after switching the annotation, it is at least trying to resolve the template.Gábor Major

3 Answers

0
votes

Change the location of your login page to static instead of template. It should be as follows,

resources/static/login.html instead of resources/templates/login.html.

And don't forget to specify the extension in your controller, return "login.html";

(Hope you have configured view resolver, because if not your endpoint will be returning the string "login.html")

-1
votes

hey i was suffering with same type of problem. After a long time i discovered that i was missing the class level mapping. For example i was writing this code for my case:

@Controller
public class ProviderController {
    @GetMapping(value = "providers")
    public String getAllProviders(Model model){
        model.addAttribute("roomReservations", null);
        return "provider";
    }

and thyleaf wasn't detecting my view. I solved this issue by using following code:

@RequestMapping(value = "providers")
@Controller
public class ProviderController {
@GetMapping
    public String getAllProviders(Model model){
        model.addAttribute("roomReservations", null);
        return "provider";
    }

i hope it will resolve your issue too.

-2
votes

Ended up using JSP instead. Works flawlessly on first try.