Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Шаблоны Thymeleaf

Кроме API блогу нужны обычные страницы для людей: список статей и страница одной статьи. Их собирает на сервере шаблонизатор — он берёт HTML-файл с особыми атрибутами и подставляет данные. В Spring Boot для этого чаще всего используют Thymeleaf. Его шаблоны — правильный HTML: файл можно открыть в браузере даже без сервера.

Подключение

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Шаблоны Spring Boot ищет в папке src/main/resources/templates/ — создайте её.

Контроллер страниц

// src/main/java/com/example/blog/PageController.java
package com.example.blog;
 
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
 
@Controller
public class PageController {
 
    private final PostService postService;
    private final CommentService commentService;
 
    public PageController(PostService postService, CommentService commentService) {
        this.postService = postService;
        this.commentService = commentService;
    }
 
    @GetMapping("/")
    public String index(Model model) {
        var page = PageRequest.of(0, 10, Sort.by("createdAt").descending());
        model.addAttribute("posts", postService.findAll(page).getContent());
        return "posts/list";
    }
 
    @GetMapping("/posts/{id}")
    public String show(@PathVariable Long id, Model model) {
        model.addAttribute("post", postService.findById(id));
        model.addAttribute("comments", commentService.findByPost(id));
        return "posts/show";
    }
}

Здесь @Controller, а не @RestController: метод возвращает имя шаблона. Строка "posts/list" означает файл templates/posts/list.html. Model — набор данных для шаблона: что положили через addAttribute, то шаблон увидит по имени. Сервисы те же, что у API.

Шаблон списка

<!-- src/main/resources/templates/posts/list.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Блог</title>
</head>
<body>
<h1>Статьи</h1>
 
<p th:if="${posts.isEmpty()}">Пока нет статей.</p>
 
<ul>
    <li th:each="post : ${posts}">
        <a th:href="@{/posts/{id}(id=${post.id})}" th:text="${post.title}">Заголовок</a>
        <small th:text="|автор: ${post.author}|">автор: anna</small>
    </li>
</ul>
</body>
</html>
АтрибутЧто делает
th:text="${post.title}"Заменяет текст тега значением; спецсимволы HTML экранируются
th:each="post : ${posts}"Повторяет тег для каждого элемента списка
th:if="${...}"Выводит тег, только если условие истинно
th:href="@{/posts/{id}(id=${post.id})}"Строит ссылку и подставляет параметр

Запись |автор: ${post.author}| склеивает текст и значение. Текст внутри тегов («Заголовок») — заглушка для просмотра файла без сервера; Thymeleaf её заменит.

Результат

Откройте http://localhost:8080/ или выполните curl http://localhost:8080/:

<h1>Статьи</h1>
 
<ul>
    <li>
        <a href="/posts/1">Привет, Spring</a>
        <small>автор: anna</small>
    </li>
</ul>

Атрибуты th:* из ответа исчезли — браузер получает чистый HTML. Шаблон posts/show.html устроен так же: заголовок, текст и th:each по комментариям. Дату там удобно выводить через ${#temporals.format(post.createdAt, 'dd.MM.yyyy HH:mm')}.

Частая ошибка

Если имя шаблона не совпадает с файлом, например вернули "posts/lst", страница ответит 500, а в журнале будет:

Error resolving template [posts/lst], template might not exist or might not be accessible by any of the configured Template Resolvers

Проверьте путь: имя считается от папки templates, без .html в конце. И не перепутайте аннотации: @RestController вернул бы браузеру просто текст posts/list.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.