После нескольких лет работы с JSP / JSTL и Apache Tiles я начал открывать Thymeleaf для своих приложений Spring MVC. Thymeleaf — это действительно отличный движок для просмотра, который упрощает и ускоряет разработку, несмотря на недостаток хорошей поддержки IntelliJ (голосуйте здесь: http://youtrack.jetbrains.com/issue/IDEABKL-6713 ) (есть Eclipse). хотя плагин ). Изучая, как использовать Thymeleaf, я исследовал различные возможности работы с макетами.
Помимо встроенного механизма включения фрагментов, есть как минимум два варианта работы с макетами: интеграция Thymeleaf с Apache Tile и Thymeleaf Layout Dialect . Оба, кажется, работают нормально, но, вдохновленный этим комментарием о простой и настраиваемой опции, я попробовал. В этом посте я покажу, что создал решение.
Создайте проект Spring MVC с поддержкой Thymeleaf
Для начала я использовал Spring MVC Archetype с поддержкой Thymeleaf 2.1. Я создал проект, просто вызвав архетип, а затем импортировал его в IntellJ.
Создание файла макета
В каталоге WEB-INF / views я создал папку раскладок, куда я поместил мой первый файл раскладки с именем default.html: переменная $ {view} будет содержать имя представления, возвращаемое @Controller, и фрагмент содержимого из $ {view} файл будет размещен здесь.
Создание файла вида
Я отредактировал WEB-INF / views / homeNotSignedIn.html и определил фрагмент содержимого следующим образом: единственное изменение — определение фрагмента с именем content и удаление дублированных включений фрагментов. Никаких дополнительных изменений не требуется. @Controller возвращает исходное имя представления, как это было раньше:
1
2
3
4
5
6
7
8
|
@Controller class HomeController { @RequestMapping (value = "/" , method = RequestMethod.GET) String index(Principal principal) { return principal != null ? "home/homeSignedIn" : "home/homeNotSignedIn" ; } } |
Я изменил другие взгляды соответственно.
Создание перехватчика и интеграция с Spring MVC
Чтобы закончить «новую структуру макета», я создал обработчик-перехватчик, который сделает всю работу:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
public class ThymeleafLayoutInterceptor extends HandlerInterceptorAdapter { private static final String DEFAULT_LAYOUT = "layouts/default" ; private static final String DEFAULT_VIEW_ATTRIBUTE_NAME = "view" ; @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { if (!modelAndView.hasView()) { return ; } String originalViewName = modelAndView.getViewName(); modelAndView.setViewName(DEFAULT_LAYOUT); modelAndView.addObject(DEFAULT_VIEW_ATTRIBUTE_NAME, originalViewName); } } |
ThymeleafLayoutInterceptor получает исходное имя представления, возвращенное из метода обработчика, и заменяет его именем макета (которое определено в WEB-INF / views / layouts / default.html). Исходный вид помещается в модель как переменная вида, поэтому его можно использовать в файле макета. Я переопределил метод postHandle, так как он выполняется непосредственно перед отображением представления.
Добавить перехватчик легко:
1
2
3
4
5
6
7
|
@Configuration public class WebMvcConfig extends WebMvcConfigurationSupport { @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor( new ThymeleafLayoutInterceptor()); } } |
И это все для базовой конфигурации. С тех пор не ракета. Результат после перехода на localhost: 8080. Это то, что я ожидал. Работает как шарм. Поэтому я пытаюсь зарегистрироваться для учетной записи и что я вижу после отправки формы:
1
|
500 returned for /signup with message Error resolving template "redirect:/" , template might not exist or might not be accessible by any of the configured Template Resolvers |
Конечно,
редирект: / после отправки формы. Мне нужно было изменить перехватчик следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
public class ThymeleafLayoutInterceptor extends HandlerInterceptorAdapter { private static final String DEFAULT_LAYOUT = "layouts/default" ; private static final String DEFAULT_VIEW_ATTRIBUTE_NAME = "view" ; @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { if (!modelAndView.hasView()) { return ; } String originalViewName = modelAndView.getViewName(); if (isRedirectOrForward(originalViewName)) { return ; } modelAndView.setViewName(DEFAULT_LAYOUT); modelAndView.addObject(DEFAULT_VIEW_ATTRIBUTE_NAME, originalViewName); } private boolean isRedirectOrForward(String viewName) { return viewName.startsWith( "redirect:" ) || viewName.startsWith( "forward:" ); } } |
И это сработало, как и ожидалось. Но я понял, что мне нужно определить и дополнительный макет, потому что регистрация и вход в систему использовали это раньше, а не после применения вышеуказанных изменений.
Создание дополнительных макетов
Я создал новый макет с именем blank.html и поместил его в папку WEB-INF / views / layouts. Но как использовать выбрать макет? Вероятно, есть много способов сделать это. Один из самых простых способов — вернуть имя макета из @Controller, просто добавив атрибут модели с именем layout. Если макет не указан, используется по умолчанию, в противном случае — заданный. Просто. Но я хотел более надежное решение. Поэтому я подумал, может быть, аннотацию, которую я мог бы использовать так
1
2
3
4
5
6
7
8
9
|
@Controller class SigninController { @Layout (value = "layouts/blank" ) @RequestMapping (value = "signin" ) String signin() { return "signin/signin" ; } } |
Для меня это звучало как хорошее решение. Так я это и реализовал.
Выбор макета
Я создал аннотацию уровня метода @Layout, которую я поместил в пакет org.thymeleaf.spring.support (вместе с ThymeleafLayoutInterceptor):
01
02
03
04
05
06
07
08
09
10
|
package org.thymeleaf.spring.support; import java.lang.annotation.*; @Target (ElementType.METHOD) @Retention (RetentionPolicy.RUNTIME) @Documented public @interface Layout { String value() default "" ; } |
Я изменил перехватчик следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public class ThymeleafLayoutInterceptor extends HandlerInterceptorAdapter { private static final String DEFAULT_LAYOUT = "layouts/default" ; private static final String DEFAULT_VIEW_ATTRIBUTE_NAME = "view" ; @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { if (!modelAndView.hasView()) { return ; } String originalViewName = modelAndView.getViewName(); if (isRedirectOrForward(originalViewName)) { return ; } String layoutName = getLayoutName(handler); modelAndView.setViewName(layoutName); modelAndView.addObject(DEFAULT_VIEW_ATTRIBUTE_NAME, originalViewName); } private boolean isRedirectOrForward(String viewName) { return viewName.startsWith( "redirect:" ) || viewName.startsWith( "forward:" ); } private String getLayoutName(Object handler) { HandlerMethod handlerMethod = (HandlerMethod) handler; Layout layout = handlerMethod.getMethodAnnotation(Layout. class ); if (layout == null ) { return DEFAULT_LAYOUT; } else { return layout.value(); } } } |
Теперь, когда метод-обработчик помечен аннотацией @Layout, он получает атрибут value. Прекрасно работает. Но когда я начал менять RegistrationController, я понял, что мне нужно аннотировать оба метода. Было бы лучше, если бы мою аннотацию можно было использовать для всех методов одновременно, аннотируя класс @Controller:
1
2
3
4
5
|
@Controller @Layout (value = "layouts/blank" ) class SignupController { } |
Так я и сделал.
Последние штрихи
Во-первых, я изменил аннотацию, чтобы она могла быть нацелена на уровень типа:
1
2
3
4
5
6
|
@Target ({ElementType.METHOD, ElementType.TYPE}) @Retention (RetentionPolicy.RUNTIME) @Documented public @interface Layout { String value() default "" ; } |
И перехватчик:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
public class ThymeleafLayoutInterceptor extends HandlerInterceptorAdapter { private static final String DEFAULT_LAYOUT = "layouts/default" ; private static final String DEFAULT_VIEW_ATTRIBUTE_NAME = "view" ; private String defaultLayout = DEFAULT_LAYOUT; private String viewAttributeName = DEFAULT_VIEW_ATTRIBUTE_NAME; public void setDefaultLayout(String defaultLayout) { Assert.hasLength(defaultLayout); this .defaultLayout = defaultLayout; } public void setViewAttributeName(String viewAttributeName) { Assert.hasLength(defaultLayout); this .viewAttributeName = viewAttributeName; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { if (!modelAndView.hasView()) { return ; } String originalViewName = modelAndView.getViewName(); if (isRedirectOrForward(originalViewName)) { return ; } String layoutName = getLayoutName(handler); modelAndView.setViewName(layoutName); modelAndView.addObject( this .viewAttributeName, originalViewName); } private boolean isRedirectOrForward(String viewName) { return viewName.startsWith( "redirect:" ) || viewName.startsWith( "forward:" ); } private String getLayoutName(Object handler) { HandlerMethod handlerMethod = (HandlerMethod) handler; Layout layout = getMethodOrTypeAnnotation(handlerMethod); if (layout == null ) { return this .defaultLayout; } else { return layout.value(); } } private Layout getMethodOrTypeAnnotation(HandlerMethod handlerMethod) { Layout layout = handlerMethod.getMethodAnnotation(Layout. class ); if (layout == null ) { return handlerMethod.getBeanType().getAnnotation(Layout. class ); } return layout; } } |
Как вы можете видеть, аннотация на уровне метода важнее аннотации на уровне типа, что дает некоторую гибкость. Кроме того, я добавил возможность настроить перехватчик. Я думал, что установка имени макета по умолчанию и имени атрибута представления может быть полезной.
Резюме
Представленное решение может потребовать некоторой доработки, чтобы использовать его в работе, но оно показывает, насколько просто мы можем создавать макеты шаблонов, не добавляя дополнительные библиотеки в наш проект и используя только основные функции Thymeleaf. Пожалуйста, поделитесь своими комментариями и мнениями о решении.
- Пожалуйста, найдите исходный код на GitHub: https://github.com/kolorobot/thymeleaf-custom-layout