В этом руководстве объясняется, как Thymeleaf может быть интегрирован с Spring Framework, особенно (но не только) Spring MVC.
Обратите внимание, что Thymeleaf имеет интеграции для версий 3.x и 4.x Spring Framework и выше, предоставляемые двумя отдельными библиотеками, которые называются thymeleaf-spring3 и thymeleaf-spring4. Эти библиотеки упакованы в отдельные файлы .jar (thymeleaf-spring3-{version}.jar и thymeleaf-spring4-{version}.jar) и должны быть добавлены в ваш путь к классам для использования интеграций Thymeleaf Spring в вашем приложении.
Примеры кода и пример приложения в этом руководстве используют Spring 4.x и соответствующие ему интеграции Thymeleaf, но содержимое этого текста также применимо для Spring 3.x. Если ваше приложение использует Spring 3.x, все, что вам нужно сделать, это заменить пакет org.thymeleaf.spring4 на org.thymeleaf.spring3 в примерах кода.
1. Интеграция Thymeleaf с Spring
Thymeleaf предлагает набор интеграций Spring, которые позволяют использовать его как полнофункциональную замену JSP в приложениях Spring MVC.
Эти интеграции позволят вам:
- Сделать сопоставление методам в ваших объектах Spring MVC Controller шаблонов, управляемых Thymeleaf, точно так же, как вы делаете это с JSP.
- Использовать Spring Expression Language (Spring EL) вместо OGNL в ваших шаблонах.
- Создавать формы в своих шаблонах, которые полностью интегрированы с вашими компонентами (bean) поддержки форм и привязками результатов, включая использование редакторов свойств, служб преобразования и обработки ошибок проверки.
- Отображать сообщения интернационализации из файлов сообщений, управляемых Spring (через обычные объекты MessageSource).
- Находить ваши шаблоны, используя собственные механизмы разрешения ресурсов Spring.
Обратите внимание, что для того, чтобы полностью понять этот учебник, вы должны сначала пройти учебник «Использование Thymeleaf», который подробно объясняет стандартный диалект.
2. SpringStandard Диалект
Чтобы добиться более легкой и лучшей интеграции, Thymeleaf предоставляет диалект, который специально реализует все необходимые функции для правильной работы с Spring.
Этот конкретный диалект основан на стандартном диалекте Thymeleaf и реализован в классе org.thymeleaf.spring4.dialect.SpringStandardDialect, который фактически происходит от org.thymeleaf.standard.StandardDialect.
Помимо всех функций, уже присутствующих в стандартном диалекте и, следовательно, унаследованных, SpringStandard Dialect предлагает следующие специфические функции:
- Использование язык выражений Spring (Spring EL или SpEL) в качестве языка выражений переменных, а не OGNL. Следовательно, все выражения ${...} и *{...} будут оцениваться движком языка выражений Spring. Также обратите внимание, что доступна поддержка компилятора Spring EL (Spring 4.2.4+).
- Доступ к любым компонентам в контексте вашего приложения с использованием синтаксиса SpringEL: ${@myBean.doSomething()}
- Новые атрибуты для обработки формы: th:field, th:errors и th:errorclass, кроме новой реализации th:object, которая позволяет использовать ее для выбора команды формы.
- Объект и метод выражения #themes.code(...), который эквивалентен пользовательскому тегу JSP spring:theme.
- Объект и метод выражения, #mvc.uri(...), который эквивалентен пользовательской функции JSP spring:mvcUrl(...) (только в Spring 4.1+).
Обратите внимание, что в большинстве случаев вам не следует использовать этот диалект непосредственно в обычном объекте TemplateEngine как части его конфигурации. Если у вас нет особых потребностей в интеграции с Spring, вам следует вместо этого создавать экземпляр нового класса шаблонизатора, который автоматически выполняет все необходимые шаги конфигурации: org.thymeleaf.spring4.SpringTemplateEngine.
Пример конфигурации бина:
@Bean
public SpringResourceTemplateResolver templateResolver(){
// SpringResourceTemplateResolver automatically integrates with Spring's own
// resource resolution infrastructure, which is highly recommended.
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(this.applicationContext);
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
// HTML is the default value, added here for the sake of clarity.
templateResolver.setTemplateMode(TemplateMode.HTML);
// Template cache is true by default. Set to false if you want
// templates to be automatically updated when modified.
templateResolver.setCacheable(true);
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine(){
// SpringTemplateEngine automatically applies SpringStandardDialect and
// enables Spring's own MessageSource message resolution mechanisms.
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
// Enabling the SpringEL compiler with Spring 4.2.4 or newer can
// speed up execution in most scenarios, but might be incompatible
// with specific cases when expressions in one template are reused
// across different data types, so this flag is "false" by default
// for safer backwards compatibility.
templateEngine.setEnableSpringELCompiler(true);
return templateEngine;
}
Или, используя конфигурацию Spring на основе XML:
<!-- SpringResourceTemplateResolver automatically integrates with Spring's own -->
<!-- resource resolution infrastructure, which is highly recommended. -->
<bean id="templateResolver"
class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
<property name="prefix" value="/WEB-INF/templates/" />
<property name="suffix" value=".html" />
<!-- HTML is the default value, added here for the sake of clarity. -->
<property name="templateMode" value="HTML" />
<!-- Template cache is true by default. Set to false if you want -->
<!-- templates to be automatically updated when modified. -->
<property name="cacheable" value="true" />
</bean>
<!-- SpringTemplateEngine automatically applies SpringStandardDialect and -->
<!-- enables Spring's own MessageSource message resolution mechanisms. -->
<bean id="templateEngine"
class="org.thymeleaf.spring4.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
<!-- Enabling the SpringEL compiler with Spring 4.2.4 or newer can speed up -->
<!-- execution in most scenarios, but might be incompatible with specific -->
<!-- cases when expressions in one template are reused across different data -->
<!-- ypes, so this flag is "false" by default for safer backwards -->
<!-- compatibility. -->
<property name="enableSpringELCompiler" value="true" />
</bean>
3. Views и View Resolvers
3.1 Views и View Resolvers в Spring MVC
В Spring MVC есть два интерфейса, которые соответствуют ядру его системы шаблонов:
- org.springframework.web.servlet.View
- org.springframework.web.servlet.ViewResolver
Views моделируют страницы в наших приложениях и позволяют изменять и предопределять их поведение, определяя их как bean-компоненты. Представления (Views) отвечают за рендеринг реального HTML-интерфейса, как правило, за выполнение какого-либо механизма шаблонов, например Thymeleaf.
ViewResolvers — это объекты, отвечающие за получение объектов View для конкретной операции и локали. Обычно контроллеры просят ViewResolvers переслать представление с определенным именем (строка, возвращаемая методом контроллера), а затем все средства разрешения представления в приложении выполняются в упорядоченной цепочке, пока один из них не сможет разрешить это представление, в каком случае объект View возвращается и ему передается управление для рендеризации HTML.
Обратите внимание, что не все страницы в наших приложениях должны быть определены как представления, а только те, поведение которых мы хотим, чтобы мы были нестандартными или настраивались особым образом (например, путем подключения к нему некоторых специальных компонентов). Если ViewResolver запрашивается представление, у которого нет соответствующего bean-компонента (что является распространенным случаем), новый объект View создается ad hoc и возвращается.
Типичная конфигурация JSP + JSTL ViewResolver в приложении Spring MVC из прошлого выглядела так:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsps/" />
<property name="suffix" value=".jsp" />
<property name="order" value="2" />
<property name="viewNames" value="*jsp" />
</bean>
Беглого взгляда на его свойства достаточно, чтобы узнать, как он был настроен:
- viewClass устанавливает класс экземпляров View. Это необходимо для распознавателя JSP, но совсем не нужно, когда мы работаем с Thymeleaf.
- Префикс и суффикс работают аналогично атрибутам с одинаковыми именами в объектах Thymeleaf TemplateResolver.
- order устанавливает порядок, в котором ViewResolver будет запрашиваться в цепочке.
- viewNames позволяет определять (с подстановочными знаками) имена представлений, которые будут разрешаться этим ViewResolver.
3.2 Views и View Resolvers в Thymeleaf
Thymeleaf предлагает реализации для двух интерфейсов, упомянутых выше:
- org.thymeleaf.spring4.view.ThymeleafView
- org.thymeleaf.spring4.view.ThymeleafViewResolver
Эти два класса будут отвечать за обработку шаблонов Thymeleaf в результате выполнения контроллеров.
Конфигурация Resolver Thymeleaf View очень похожа на JSP:
@Bean
public ThymeleafViewResolver viewResolver(){
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
// NOTE 'order' and 'viewNames' are optional
viewResolver.setOrder(1);
viewResolver.setViewNames(new String[] {".html", ".xhtml"});
return viewResolver;
}
…или в XML:
<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine" />
<!-- NOTE 'order' and 'viewNames' are optional -->
<property name="order" value="1" />
<property name="viewNames" value="*.html,*.xhtml" />
</bean>
Параметр templateEngine, конечно же, является объектом SpringTemplateEngine, который мы определили в предыдущей главе. Два других (order и viewNames) являются необязательными и имеют то же значение, что и в JSP ViewResolver, который мы видели ранее.
Обратите внимание, что нам не нужны префиксные или суффиксные параметры, потому что они уже указаны в Template Resolver (который, в свою очередь, передается в Template Engine).
А что, если мы хотим определить bean-компонент View и добавить к нему несколько статических переменных? Легко, просто определите для него прототип:
@Bean
@Scope("prototype")
public ThymeleafView mainView() {
ThymeleafView view = new ThymeleafView("main"); // templateName = 'main'
view.setStaticVariables(
Collections.singletonMap("footer", "The ACME Fruit Company"));
return view;
}
Сделав это, вы сможете запросить этот компонент, выбрав его по имени (в данном случае mainView).
4. Spring Thyme Seed Starter Manager
Исходный код для примеров, показанных в этой и последующих главах этого руководства, можно найти в репозитории GitHub Spring Seyme Seed Starter Manager.
4.1 Концепция
Касательно Thymeleaf — мы большие поклонники thyme, и каждый spring мы готовим наши стартовые наборы с хорошей почвой и нашими любимыми семенами, сажаем их под испанское солнце и терпеливо ждем, пока наши новые растения вырастут.
Но в этом году нам надоело наклеивать ярлыки на стартовые контейнеры для семян, чтобы узнать, какие семена были в каждой ячейке контейнера, поэтому мы решили подготовить приложение, используя Spring MVC и Thymeleaf, чтобы помочь нам каталогизировать наши стартеры: Spring Thyme SeedStarter Менеджер.
Аналогично приложению Good Thymes Virtual Grocery, которое мы разработали в учебном пособии «Использование Thymeleaf», STSM позволит нам продемонстрировать наиболее важные аспекты интеграции Thymeleaf как механизма шаблонов для Spring MVC.
4.2 Бизнес слой
Нам понадобится очень простой бизнес-уровень для нашего приложения. Прежде всего, давайте посмотрим на наши модельные объекты:
Пара очень простых классов обслуживания обеспечит необходимые бизнес-методы. Подобно:
@Service
public class SeedStarterService {
@Autowired
private SeedStarterRepository seedstarterRepository;
public List<SeedStarter> findAll() {
return this.seedstarterRepository.findAll();
}
public void add(final SeedStarter seedStarter) {
this.seedstarterRepository.add(seedStarter);
}
}
И:
@Service
public class VarietyService {
@Autowired
private VarietyRepository varietyRepository;
public List<Variety> findAll() {
return this.varietyRepository.findAll();
}
public Variety findById(final Integer id) {
return this.varietyRepository.findById(id);
}
}
4.3 Spring MVC configuration
Затем нам нужно настроить конфигурацию Spring MVC для приложения, которая будет включать не только стандартные артефакты Spring MVC, такие как обработка ресурсов или сканирование аннотаций, но также создание экземпляров Template Engine и View Resolver.
@Configuration
@EnableWebMvc
@ComponentScan
public class SpringWebConfig
extends WebMvcConfigurerAdapter implements ApplicationContextAware {
private ApplicationContext applicationContext;
public SpringWebConfig() {
super();
}
public void setApplicationContext(final ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
/* ******************************************************************* */
/* GENERAL CONFIGURATION ARTIFACTS */
/* Static Resources, i18n Messages, Formatters (Conversion Service) */
/* ******************************************************************* */
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
registry.addResourceHandler("/images/**").addResourceLocations("/images/");
registry.addResourceHandler("/css/**").addResourceLocations("/css/");
registry.addResourceHandler("/js/**").addResourceLocations("/js/");
}
@Bean
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("Messages");
return messageSource;
}
@Override
public void addFormatters(final FormatterRegistry registry) {
super.addFormatters(registry);
registry.addFormatter(varietyFormatter());
registry.addFormatter(dateFormatter());
}
@Bean
public VarietyFormatter varietyFormatter() {
return new VarietyFormatter();
}
@Bean
public DateFormatter dateFormatter() {
return new DateFormatter();
}
/* **************************************************************** */
/* THYMELEAF-SPECIFIC ARTIFACTS */
/* TemplateResolver <- TemplateEngine <- ViewResolver */
/* **************************************************************** */
@Bean
public SpringResourceTemplateResolver templateResolver(){
// SpringResourceTemplateResolver automatically integrates with Spring's own
// resource resolution infrastructure, which is highly recommended.
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(this.applicationContext);
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
// HTML is the default value, added here for the sake of clarity.
templateResolver.setTemplateMode(TemplateMode.HTML);
// Template cache is true by default. Set to false if you want
// templates to be automatically updated when modified.
templateResolver.setCacheable(true);
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine(){
// SpringTemplateEngine automatically applies SpringStandardDialect and
// enables Spring's own MessageSource message resolution mechanisms.
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
// Enabling the SpringEL compiler with Spring 4.2.4 or newer can
// speed up execution in most scenarios, but might be incompatible
// with specific cases when expressions in one template are reused
// across different data types, so this flag is "false" by default
// for safer backwards compatibility.
templateEngine.setEnableSpringELCompiler(true);
return templateEngine;
}
@Bean
public ThymeleafViewResolver viewResolver(){
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
return viewResolver;
}
}
4.4 The Controller
Конечно, нам также понадобится контроллер для нашего приложения. Поскольку STSM будет содержать только одну веб-страницу со списком начальных значений и форму для добавления новых, мы напишем только один класс контроллера для всех взаимодействий сервера:
@Controller
public class SeedStarterMngController {
@Autowired
private VarietyService varietyService;
@Autowired
private SeedStarterService seedStarterService;
...
}
Теперь давайте посмотрим, что мы можем добавить к этому классу контроллера.
Атрибуты модели
Сначала мы добавим некоторые атрибуты модели, которые нам понадобятся на странице:
@ModelAttribute("allTypes")
public List<Type> populateTypes() {
return Arrays.asList(Type.ALL);
}
@ModelAttribute("allFeatures")
public List<Feature> populateFeatures() {
return Arrays.asList(Feature.ALL);
}
@ModelAttribute("allVarieties")
public List<Variety> populateVarieties() {
return this.varietyService.findAll();
}
@ModelAttribute("allSeedStarters")
public List<SeedStarter> populateSeedStarters() {
return this.seedStarterService.findAll();
}
Mapped методы
А теперь самая важная часть контроллера, отображаемые (mapped ) методы: один для отображения страницы формы, а другой для обработки добавления новых объектов SeedStarter.
@RequestMapping({"/","/seedstartermng"})
public String showSeedstarters(final SeedStarter seedStarter) {
seedStarter.setDatePlanted(Calendar.getInstance().getTime());
return "seedstartermng";
}
@RequestMapping(value="/seedstartermng", params={"save"})
public String saveSeedstarter(
final SeedStarter seedStarter, final BindingResult bindingResult, final ModelMap model) {
if (bindingResult.hasErrors()) {
return "seedstartermng";
}
this.seedStarterService.add(seedStarter);
model.clear();
return "redirect:/seedstartermng";
}
4.5 Configuring a Conversion Service
Чтобы обеспечить простое форматирование объектов Date, а также объектов Variety в нашем слое представления, мы настроили наше приложение таким образом, чтобы объект Spring ConversionService был создан и инициализирован (расширяемым WebMvcConfigurerAdapter) с помощью пары необходимых нам объектов форматирования.
Посмотри еще раз:
@Override
public void addFormatters(final FormatterRegistry registry) {
super.addFormatters(registry);
registry.addFormatter(varietyFormatter());
registry.addFormatter(dateFormatter());
}
@Bean
public VarietyFormatter varietyFormatter() {
return new VarietyFormatter();
}
@Bean
public DateFormatter dateFormatter() {
return new DateFormatter();
}
Spring formatters are implementations of the org.springframework.format.Formatter interface. For more information on how the Spring conversion infrastructure works, see the docs at spring.io.
Давайте посмотрим на DateFormatter, который форматирует даты в соответствии со строкой форматирования, присутствующей в ключе сообщения date.format наших Messages.properties:
public class DateFormatter implements Formatter<Date> {
@Autowired
private MessageSource messageSource;
public DateFormatter() {
super();
}
public Date parse(final String text, final Locale locale) throws ParseException {
final SimpleDateFormat dateFormat = createDateFormat(locale);
return dateFormat.parse(text);
}
public String print(final Date object, final Locale locale) {
final SimpleDateFormat dateFormat = createDateFormat(locale);
return dateFormat.format(object);
}
private SimpleDateFormat createDateFormat(final Locale locale) {
final String format = this.messageSource.getMessage("date.format", null, locale);
final SimpleDateFormat dateFormat = new SimpleDateFormat(format);
dateFormat.setLenient(false);
return dateFormat;
}
}
VarietyFormatter автоматически преобразует между нашими объектами Variety и способом, которым мы хотим использовать их в наших формах (в основном, по значениям их полей id):
public class VarietyFormatter implements Formatter<Variety> {
@Autowired
private VarietyService varietyService;
public VarietyFormatter() {
super();
}
public Variety parse(final String text, final Locale locale) throws ParseException {
final Integer varietyId = Integer.valueOf(text);
return this.varietyService.findById(varietyId);
}
public String print(final Variety object, final Locale locale) {
return (object != null ? object.getId().toString() : "");
}
}
Мы узнаем больше о том, как эти средства форматирования влияют на способ отображения наших данных в дальнейшем.
Автор: Константин