Начало работы с Thymeleaf в Java и Spring

Введение При разработке веб-приложений важно выбрать, какой движок будет заботиться о слое представления. Страницы сервера Java (JSP) были очень популярны, хотя накладные расходы и затраты времени были одними из основных недостатков их использования. Они потребовали внесения значительных изменений в HTML-код страниц. В настоящее время Thymeleaf [https://www.thymeleaf.org/] широко используется и используется в качестве механизма создания шаблонов для приложений Spring / MVC. Его также можно использовать для создания шаблонов электронной почты в формате HTML.

Вступление

При разработке веб-приложений важно выбрать, какой движок будет заботиться о слое представления.

Страницы сервера Java (JSP) были очень популярны, хотя накладные расходы и затраты времени были одними из основных недостатков их использования. Они потребовали внесения значительных изменений в HTML-код страниц.

В настоящее время Thymeleaf широко применяется и используется в качестве механизма создания шаблонов для приложений Spring / MVC. Его также можно использовать для создания шаблонов электронной почты в формате HTML. В то время как JSP компилируются в классы сервлетов Java, Thymeleaf анализирует простые файлы шаблонов HTML. На основе выражений, присутствующих в файле, он генерирует статическое содержимое. Он способен обрабатывать HTML, XML, JS, CSS и т. Д.

Стандартные диалекты тимелеафа

Thymeleaf предоставляет широкий спектр процессоров атрибутов из коробки как часть своих стандартных диалектов . Этих процессоров достаточно для большинства типичных шаблонов. Тем не менее, вы также можете расширить их, чтобы в случае необходимости создавать собственные процессоры атрибутов.

Давайте посмотрим на самый важный сегмент диалекта - стандартные функции выражения . Вот некоторые из выражений, которые вы будете использовать довольно часто:

  • Выражения переменных: ${...}
  • Выражения переменных выбора: *{...}
  • Выражения сообщений: #{...}
  • Выражения URL ссылки: @{...}
  • Выражения фрагментов: ~{...}

Вот некоторые литералы, которые вы, вероятно, будете использовать:

  • Текстовые литералы: 'hello world' , 'Welcome to stackabuse' ,…
  • Числовые литералы: 0 , 123 , 67.90 ,…
  • Логические литералы: true , false
  • Нулевой литерал: null

Основные операции:

  • Конкатенация строк: +

  • Буквальные замены: |Welcome to ${city}|

  • Бинарные операторы: + , - , * , / , `%

  • Бинарные операторы: and , or

  • Логическое отрицание (унарный оператор) ! not

Сравнения:

  • Компараторы: > , < , >= , <= ( gt , lt , ge , le )
  • Операторы равенства: == != ( eq , ne )

Условные:

  • Если-то: (if) ? (then)
  • Если-то-еще: (if) ? (then) : (else)
  • По умолчанию: (value) ?: (defaultvalue)

Все эти выражения можно использовать в сочетании друг с другом для получения желаемых результатов.

Зависимость от тимелеафа

Самый простой способ начать работу с Thymleaf через Maven - это включить зависимость:

 <dependency> 
 <groupId>org.thymeleaf</groupId> 
 <artifactId>thymeleaf</artifactId> 
 <version>${version}</version> 
 </dependency> 

Или, если вы используете Gradle:

 compile group: 'org.thymeleaf', name: 'thymeleaf', version: '${version}' 

Механизм шаблонов и преобразователи шаблонов

Для Thymeleaf преобразователь шаблонов отвечает за загрузку шаблонов из заданного места, а механизм шаблонов отвечает за их обработку для заданного контекста. Нам нужно будет настроить оба в классе конфигурации:

 @Configuration 
 public class WebConfig implements WebMvcConfigurer { 
 
 @Bean 
 public ClassLoaderTemplateResolver templateResolver() { 
 ClassLoaderTemplateResolver templateResolver = 
 new ClassLoaderTemplateResolver(); 
 templateResolver.setPrefix("/templates/"); 
 templateResolver.setSuffix(".html"); 
 templateResolver.setCharacterEncoding("UTF-8"); 
 
 return templateResolver; 
 } 
 
 @Bean 
 public SpringTemplateEngine templateEngine() { 
 SpringTemplateEngine templateEngine = new SpringTemplateEngine(); 
 templateEngine.setTemplateResolver(templateResolver()); 
 return templateEngine; 
 } 
 } 

Здесь мы templateResolver и установили его префикс и суффикс. Представления будут располагаться в /templates и оканчиваться на .html .

После этого мы настроили templateEngine , просто установив преобразователь и вернув его.

Давайте проверим, работает ли он, попробовав обработать сообщение:

 StringWriter writer = new StringWriter(); 
 Context context = new Context(); 
 TemplateEngine templateEngine = templateEngine(); 
 
 context.setVariable("message", "Welcome to thymeleaf article"); 
 templateEngine.process("myTemplate", context, writer); 
 LOG.info(writer.toString()); 

Движок используется для обработки myTemplate.html , расположенного в src/main/resources/templates . /resources является каталогом по умолчанию. Переменная передается в context , что позволяет нам ссылаться на нее в самом шаблоне:

 <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-3.dtd"> 
 <html xmlns="http://www.w3.org/1999/xhtml" 
 xmlns:th="http://www.thymeleaf.org"> 
 <body> 
 <h1 th:text="${message}"></h1> 
 </body> 
 </html> 

th:text будет оценивать значение этого message и вставлять его в тело тега, в котором оно находится. В нашем случае тело <h1> :

 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
 <html xmlns="http://www.w3.org/1999/xhtml"> 
 <body> 
 <h1>Welcome to thymeleaf article</h1> 
 </body> 
 </html> 

Работает отлично! Давайте продолжим и настроим ViewResolver чтобы мы могли заполнять представления через контроллеры, а не жестко кодировать значения в контексте.

Просмотр Resolver

Прямо под другой конфигурацией давайте ViewResolver . Он сопоставляет имена представлений с фактическими представлениями. Это позволяет нам просто ссылаться на представления в контроллерах, а не на значения жесткого кодирования:

 @Bean 
 public ViewResolver viewResolver() { 
 ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); 
 viewResolver.setTemplateEngine(templateEngine()); 
 viewResolver.setCharacterEncoding("UTF-8"); 
 return viewResolver; 
 } 

Отображение атрибутов модели

Основное использование большинства движков, таких как Thymeleaf, - это отображение определенных свойств / атрибутов моделей. Давайте создадим обработчик запроса, который возвращает объект с парой установленных полей:

 @GetMapping("/article") 
 public ModelAndView getArticle(ModelAndView modelAndView) { 
 Article article = new Article(); 
 article.setAuthor(getName()); 
 article.setContent(getArticleContent()); 
 article.setTitle(getTitle()); 
 modelAndView.addObject("article", article); 
 modelAndView.setViewName("articleView"); 
 return modelAndView; 
 } 

Обработчик отправляет обратно представление с именем articleView и объект с именем article . Эти двое теперь взаимосвязаны. Мы можем получить доступ к article на странице articleView Это похоже на то, как мы в прошлый раз message Context

Давайте посмотрим, как мы можем получить доступ к объекту и отобразить его значения на странице:

 <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-3.dtd"> 
 <html xmlns="http://www.w3.org/1999/xhtml" 
 xmlns:th="http://www.thymeleaf.org"> 
 <link th:href="@{/css/app.css}" rel="stylesheet"/> 
 <body class='typora-export os-windows'> 
 <div id='write' class='is-node'> 
 <h1 th:text="${article.title}">Article title</h1> 
 <h4 th:text="${article.author}">Author name</h4> 
 <p th:text="${article.content}">contetnt</p></div> 
 </body> 
 </html> 

Используя переменное выражение, ${...} , мы ссылаемся на article и соответственно вставляем поля в атрибуты th:text Вот как будет выглядеть отрисованная страница:

отображение атрибутовмодели{.ezlazyload}

Примечание. Если у тега есть тело, th:text заменит его. Если значение отсутствует или есть проблемы с его отображением, вместо него будет использоваться тело.

Локальные переменные

Локальные переменные в Thymeleaf очень удобны. Локальные переменные определяются в определенном фрагменте шаблона. Они доступны только в рамках определяющего фрагмента.

С локальными переменными мы избавляемся от необходимости делать все в контроллере и выполнять операции на самой странице. Давайте взглянем:

 <tr th:each="article : ${articles}"> 
 <td th:text="${article.name}">name</td> 
 <td th:text="${article.author}">author</td> 
 <td th:text="${article.description">description</td> 
 </tr> 

Здесь article - это локальная переменная. Он представляет собой article объект из articles списка. Мы не можем ссылаться на article за пределами таблицы HTML.

article не передавалась контроллером - она была определена на самой странице. th:each будет назначать новые значения article при каждом проходе списка.

Это выглядело бы примерно так:

списокстатей{.ezlazyload}

Другой способ определить локальные переменные - через атрибут th:with

 <div th:with="article=${articles[0]}"> 
 <p> 
 This article is writen by <span th:text="${article.author}">John Doe</span>. 
 </p> 
 </div> 

Здесь мы определили переменную через th:with как первый элемент списка, переданного контроллером. Мы можем ссылаться на эту переменную из <div> она определена.

Точно так же мы можем определить несколько переменных с помощью одного атрибута th:with

 <div th:with="article=${articles[0]}, category=${categories[1]}"> 
 <p> 
 This article is writen by <span th:text="${article.author}">John Doe</span>. 
 </p> 
 <p> 
 Category <span th:text="${category.name}">John Doe</span>. 
 </p> 
 </div> 

Мы также можем использовать эти локальные переменные для выполнения манипуляций с данными или их извлечения, чтобы уменьшить количество обращений к контроллерам:

 <div th:with="article=${articles[0]}, author=${authors[article.author]}"> 
 </div> 

Обратите внимание , что мы используем article переменные , чтобы получить author детали из карты автора. Это позволяет нам повторно использовать переменную в том же атрибуте.

Кроме того, теперь нам больше не нужно зависеть от контроллера, чтобы делиться данными об авторе для каждой статьи, мы можем просто передать список авторов помимо списка статей:

 @GetMapping("/articles") 
 public ModelAndView getArticles(ModelAndView modelAndView) { 
 modelAndView.addObject("articles", getArticles()); 
 modelAndView.addObject("authors", getAuthors()); 
 modelAndView.setViewName("articles"); 
 return modelAndView; 
 } 

Вам не нужно устанавливать привязанные к объектам локальные переменные. Вы можете так же легко использовать строковые литералы или числа:

 <div th:with="name = 'John', age = 25}"> 
 <p> Hello, <span th:text="${name}"></span>!</p> 
 </div> 

Выражения переменных выбора

Здесь стоит отметить выражения переменных выбора . Давайте посмотрим, как они работают:

 <div th:object="${article}"> 
 <td th:text="*{name}">name</td> 
 <td th:text="*{author}">author</td> 
 <td th:text="*{description">description</td> 
 </tr> 

Вместо написания ${article.name} , ${article.author} и т. Д. Мы можем просто указать выражение *{...} th:object определяет, к какому объекту принадлежат указанные поля.

Создание форм и входов

Работа с формами происходит часто, и это один из основных способов отправки информации на наш сервер. Thymeleaf предоставляет различные атрибуты для создания и обработки отправленных форм.

- th:action атрибут заменяет HTML action атрибут <form> . th:object используется для привязки полей формы к объекту. Это похоже на modelAttribute или commandName вы обычно используете с JSP.

Давайте посмотрим на определение формы:

 <form th:action="@{/article}" th:object="${article}" method="post"> 
 </form> 

Здесь через выражение ссылки форма отправляет запрос POST на URL-адрес /article Связанный объект - это article . Теперь нам нужно ввести несколько полей для ввода, чтобы фактически заполнить информацию о article :

 <form th:action="@{/article}" th:object="${article}" method="post"> 
 <div class='is-node custom-form'> 
 <label>Title:</label> 
 <input type="text" th:field="*{title}"/> 
 </div> 
 <div class='is-node custom-form'> 
 <label>Content:</label> 
 <textarea th:field="*{content}"/> 
 </div> 
 </form> 

Мы связали article с этой формой, поэтому упомянутый title и content принадлежат ей.

Теперь, когда пользователь вводит контент в эти поля, мы захотим обработать его и сохранить в базе данных. Давайте /form который сначала отобразит форму на странице:

 @GetMapping("/form") 
 public ModelAndView getArticleForm(ModelAndView modelAndView) { 
 Article article = new Article(); 
 modelAndView.addObject("article", article); 
 modelAndView.setViewName("articleForm"); 
 return modelAndView; 
 } 

форма отображениясообщения{.ezlazyload}

Мы должны добавить в форму пустой article , иначе th:object будет недопустимым. Теперь давайте создадим обработчик запроса POST, который попадает в форму:

 @PostMapping("/article") 
 public String saveArticle(@ModelAttribute Article article) { 
 articleService.saveArticle(article); 
 return "articles"; 
 } 

Здесь @ModelAttribute связывает полученную модель с обрабатывающим ее объектом. Все это упаковано в article который затем сохраняется с помощью классической службы, расширяющей CrudRepository .

Хотя такой рудиментарной формы часто бывает недостаточно. Давайте посмотрим, как мы можем добавить переключатели, флажки, раскрывающиеся меню и т. Д.

Радио-кнопки

Чтобы добавить переключатель, мы должны создать классический <input> и определить его тип через HTML. Задача Thymeleaf - привязать поле и значение этого переключателя к th:object формы:

 <form th:action="@{/article}" th:object="${article}" method="post"> 
 <div> 
 <label>Select a Category:</label> 
 <div th:each="category : ${categories}"> 
 <input type="radio" th:field="*{category}" th:value="${category}" /> 
 <label th:for="${#ids.prev('category')}" th:text="${category}"></label> 
 </div> 
 </div> 
 </form> 

После рендеринга это будет выглядеть примерно так:

Радио-кнопки{.ezlazyload}

Флажки

Флажки работают точно так же:

 <form th:action="@{/article}" th:object="${article}" method="post"> 
 <div class='is-node custom-form'> 
 <label>Select Areas:</label> 
 <div th:each="area : ${areas}"> 
 <input type="checkbox" th:field="*{area}" th:value="${area}"/> 
 <label th:for="${#ids.prev('area')}" th:text="${area}"></label> 
 </div> 
 </div> 
 </form> 

Это выглядело бы так:

флажки{.ezlazyload}

Меню опций

И, наконец, давайте посмотрим, как мы можем добавить несколько опций:

 <form th:action="@{/article}" th:object="${article}" method="post"> 
 <div class='is-node custom-form'> 
 <label>Select a Technology:</label> 
 <select th:field="*{technology}"> 
 <option th:each="technology : ${technologies}" th:value="${technology}" 
 th:text="${technology}"> 
 </option> 
 </select> 
 </div> 
 </form> 

Обычно варианты представлены списком. В этом случае мы создали <option> для каждой technology в списке и присвоили technology может видеть пользователь.

Это выглядело бы примерно так:

менюопций{.ezlazyload}

Условные утверждения

Веб-сайты не статичны. В зависимости от определенных оценок элементы отображаются, скрываются, заменяются или настраиваются. Например, мы можем выбрать отображение сообщения вместо таблицы, если в базе данных нет строк.

Давайте посмотрим на некоторые основные условные операторы в Thymeleaf:

 <body> 
 <table th:if="${not #list.isEmpty(articles)}"> 
 <tr> 
 <th>Name</th> 
 <th>Author</th> 
 <th>Description</th> 
 <th>Category</th> 
 <th>Date</th> 
 </tr> 
 <tr th:each="article : ${articles}"> 
 <td th:text="${article.name}">name</td> 
 <td th:text="${article.author}">author</td> 
 <td th:text="${article.description">description</td> 
 <td th:text="${article.category}">category</td> 
 <td th:text="${article.date}">date</td> 
 </tr> 
 </table> 
 
 <div th:if="${#lists.isEmpty(kv)}"> 
 <h2>No data found</h2> 
 </div> 
 </body> 

th:if используется как обычный оператор if Если articles не пустой, мы заполняем таблицу - если она пуста, мы выводим сообщение. Здесь #list - служебный объект, используемый для выполнения вспомогательных методов в коллекциях.

Кроме того, у нас также могут быть операторы th:switch и th:case . Они довольно просты:

 <div> 
 <td th:switch="${article.category}"> 
 <span th:case="'TECHNOLOGY'" th:text="Technical Articles"/> 
 <span th:case="'FASHION'" th:text="About latest fashion trends"/> 
 <span th:case="'FOOD'" th:text="Are you hungry..."/> 
 </td> 
 </div> 

Отображается только соответствующий регистр.

Внешний текст для интернационализации

По умолчанию Thymeleaf поддерживает интернационализацию. Создайте myTemplate.properties в том же каталоге, что и ваши шаблоны.

Создадим сообщение и присвоим ему значение:

 welcome.message=Welcome to Stack Abuse 

Теперь в любом шаблоне мы можем ссылаться на значение, вызывая welcome.message с выражением сообщения :

 <body> 
 <h1 th:text="#{welcome.message}"></h1> 
 </body> 

myTemplate_de.properties создайте больше файлов, например myTemplate_de.properties. При создании контекста для шаблона в исходной настройке просто передайте ему локаль:

 Context context = new Context(Locale.GERMAN); 

Фрагменты и макеты

Некоторые вещи на странице не сильно меняются во всем интерфейсе. А именно, верхний и нижний колонтитулы обычно идентичны. Кроме того, как только они будут изменены / обновлены, вам нужно будет перейти на каждую страницу и обновить код там.

Этот шаблонный код можно использовать повторно и просто ссылаться на него на каждой странице. Thymeleaf предлагает нам фрагменты , которые представляют собой отдельные файлы, которые вы можете вставить в другой файл. Создадим фрагмент заголовка и включим его в другой шаблон:

 <html xmlns="http://www.w3.org/1999/xhtml" 
 xmlns:th="http://www.thymeleaf.org"> 
 <body> 
 <div th:fragment="header_fragment"> 
 <h1>Welcome to Stack Abuse</h1> 
 </div> 
 </body> 
 </html> 

Мы сохраним этот файл с именем header.html в том же каталоге, что и другие шаблоны. Хотя многие сохраняют их в подкаталоге, называемом fragments .

Теперь мы захотим включить этот заголовок на другую страницу. Обратите внимание, что это не будет включать весь файл . Просто <div> мы пометили как th:fragment . Поместим этот заголовок над нашим приветственным сообщением:

 <body> 
 <div id="holder" th:insert="header :: header_fragment"></div> 
 <h1 th:text="#{welcome.message}"></h1> 
 </body> 

Когда мы визуализируем этот файл, HTML-страница будет выглядеть так:

 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
 <html xmlns="http://www.w3.org/1999/xhtml"> 
 <body> 
 <div id="holder"> 
 <div> 
 <h1>Welcome to Stack Abuse Article</h1> 
 </div> 
 </div 
 <h1>Welcome to world</h1> 
 </body> 
 </html> 

Теперь есть три способа включить фрагменты: th:insert , th:replace и th:include .

th:insert добавляет фрагмент как дочерний узел внутри включающего тега. Как мы видим в приведенном выше примере, фрагмент заголовка вставляется в <div> с идентификатором holder

th:replace заменит текущий тег на фрагмент:

 <body> 
 <div id="holder" th:replace="header :: header_fragment"></div> 
 <h1 th:text="#{welcome.message}"></h1> 
 </body> 

Это будет выглядеть как:

 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
 <html xmlns="http://www.w3.org/1999/xhtml"> 
 <body> 
 <div> 
 <h1>Welcome to Stack Abuse Article</h1> 
 </div> 
 <h1>Welcome to world</h1> 
 </body> 
 </html> 

<div> с идентификатором holder теперь заменяется фрагментом.

th:include является предшественником th:replace и работает точно так же. Теперь это устарело.

Обработка ошибок и сообщений об ошибках

Обработка ошибок - очень важный аспект веб-приложений. Когда что-то не так, мы хотим помочь пользователю исправить пользовательские проблемы, например неправильную отправку формы.

Для простоты мы будем использовать javax.validations для проверки полей отправки формы:

 @PostMapping("/article") 
 public String saveArticle(@ModelAttribute @Valid Article article, BindingResult bindingResult) { 
 if (bindingResult.hasErrors()) { 
 return "articleForm"; 
 } 
 articleService.saveArticle(article); 
 return "redirect:articles"; 
 } 

Это классический обработчик отправки формы. Мы упаковали информацию в article и сохранили ее в базе данных. Однако на этот раз мы пометили article как @Valid и добавили проверку для экземпляра BindingResult

@Valid гарантирует, что полученная и упакованная информация об объекте соответствует проверкам, которые мы установили в модели Article

 public class Article { 
 @NotNull 
 @Size(min = 2, max = 30) 
 private String title; 
 private String author; 
 @NotNull 
 @Size(min = 2, max = 1000) 
 private String content; 
 private String category; 
 private String technology; 
 private String area; 
 } 

Если есть какие-либо нарушения этих правил, bindingResults.hasErrors() вернет true . И таким образом возвращаем форму обратно. вместо перенаправления пользователя на страницу /articles

Ошибки будут отображаться в форме в обозначенных местах, которые мы установили с помощью th:errors :

 <form th:action="@{/article}" th:object="${article}" method="post"> 
 <div class='is-node custom-form'> 
 <label>Title:</label> 
 <input type="text" th:field="*{title}"/> 
 <span class="field-error" th:if="${#fields.hasErrors('title')}" th:errors="*{title}">Name Error</span> 
 </div> 
 <div class='is-node custom-form'> 
 <label>Content:</label> 
 <textarea th:field="*{content}"/> 
 <span class="field-error" th:if="${#fields.hasErrors('content')}" th:errors="*{content}">Name Error</span> 
 </div> 
 </form> 

Используя пару условных #fields.hasErrors() , мы можем сообщить пользователю, что не так с проверками, и вежливо попросить пересмотреть предоставленную информацию.

Вот как будет выглядеть отрисованная страница:

обработка ошибок с помощьютимелеафа{.ezlazyload}

В качестве альтернативы мы также можем сгруппировать все ошибки вместе, используя подстановочный знак или all :

 <li class="field-error" th:each="error : ${#fields.errors('*')}" th:text="${error}" /> 
 <li class="field-error" th:each="error : ${#fields.errors('all')}" th:text="${error}" /> 

группаошибок{.ezlazyload}

Заключение

Эта статья предназначена для ознакомления с Thymeleaf, очень популярным современным механизмом создания шаблонов для приложений Java / Spring.

Хотя мы не сделали глубокого погружения в движок, который является довольно обширным, описанного материала должно быть более чем достаточно, чтобы вы начали с хорошей основы для более продвинутых функций.

comments powered by Disqus