Шаблоны 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.