25 ноября 2010 г.

Смешиваем, но не взбалтываем: Apache Tiles & Spring MVC

По совершенно обычному стечению обстоятельств наткнулся на весьма легкий проект - Apache Tiles, оказавшийся весьма популярным. Это простой и со своей второй версии весьма гибкий шаблонизатор пришедший из Struts. Собственно, мне не приходилось с ним сталкивался, тем более не слышал о том что в Spring уже есть поддержка этого легковесного шаблонизатора с совершенно незамысловатым устройством.

Что же это за шаблонизатор и с чем его готовить?
Так вот ничего революционного этот проект собой не представляет, но как оказалось он весьма удобен. Концепция проста - зачем работать с различного рода "обвесами" на странице (headers, footers, sidebars), если в действительности в контексте логики приложения нас интересует только конкретный динамический блок (при этом, совершенно необязательно, что "обвесы" являются статическими блоками). Таким образом, основная выгода - это разделение логики представления по нескольким уровням (да, это тривиально).

Что дает такое простое разбиение:
  • больше нет необходимости постоянно передавать в представление множество повторяющихся значений
  • больше нет необходимости работать со всем шаблоном, теперь достаточно вернуть из контроллера идентификатор конкретного динамического блока
Очень простая вещь, которая в совокупности с некоторыми дополнительными удобствами становиться настолько естественной, что лично у меня возник вопрос: "Почему я раньше не слышал о таком простом фреймворке?"


По полочкам...

Каким же образом использовать этот инструмент? Все достаточно просто, необходимо сделать следующее:
  1. Сконфигурировать Tiles в контексте Spring контейнера. Spring уже включает поддержку Tiles, поэтому для этого достаточно лишь создать Configurer и соответствующий ViewResolver:
    <!-- Configure Apache Tiles for the view -->
    <bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
        <property name="definitions" value="/WEB-INF/tiles/tiles-templates.xml" />
        <property name="preparerFactoryClass" value="org.springframework.web.servlet.view.tiles2.SpringBeanPreparerFactory"/>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.tiles2.TilesViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.tiles2.TilesView"/>
    </bean>
  2. Создать tiles-конфигурацию ("/WEB-INF/tiles/tiles-templates.xml" из spring-конфига), в которой описываются шаблоный, отдельные элементы шаблона.
  3. Реализовать представление для описанных шаблонов
Как правило после этого (с учетом, что будет готов уровень контроллера) уже можно будет посмотреть на результат.


Начинаем разработку

Прежде чем перейти к конфигурации Spring и Tiles в качестве примера определим структуру шаблона. В качестве примера возьмем следующую структуру:
Исходя из такой структуры можно заметить, что фактическая работа приложения сводиться к работе блоком CONTENT, все остальное либо не связанно с контентом, либо является статическими блоками (конечно, бывают исключения ^_^).


Конфигурируем Spring

В конфигурации Spring MVC ничего особенного нет, кроме выше изложенного ничего специфического не добавляем.
/WEB-INF/web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
                  <param-name>contextConfigLocation</param-name>
                  <param-value>
                     /WEB-INF/spring/*.xml
                  </param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>*.page</url-pattern>
    </servlet-mapping>

    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.page</welcome-file>
    </welcome-file-list>

</web-app>
И /WEB-INF/spring/mvc-config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context-3.0.xsd">


    <!-- Scan for annotation based controllers -->
    <context:component-scan base-package="ru.sultry.controllers">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    <!-- Configure Apache Tiles for the view -->
    <bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
        <property name="definitions" value="/WEB-INF/tiles/tiles-templates.xml" />
        <property name="preparerFactoryClass" value="org.springframework.web.servlet.view.tiles2.SpringBeanPreparerFactory"/>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.tiles2.TilesViewResolver">
        <property name="requestContextAttribute" value="requestContext"/>
        <property name="viewClass" value="org.springframework.web.servlet.view.tiles2.TilesView"/>
    </bean>

</beans>
Более тут добавить нечего, кроме того, что необходимо реализовать контроллер для нашего примера. Нужно заметить, что ничего принципиально не меняется и здесь, кроме того, что нас больше не интересуют данные для любых других блоков шаблона, кроме контентного:
package ru.sultry.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

/**
 * @author tsarev.oi@mail.ru
 *         User: Oleg Tsarev
 *         Date: 10.11.2010
 *         Time: 14:17:42
 *
 */

@Controller
public class MainController {

    public static final String INDEX_PAGE = "/index.page";
    public static final String INDEX_VIEW = "main";

    public static final String INFO_PAGE = "/info.page";
    public static final String INFO_VIEW = "info";

    @RequestMapping(value=INDEX_PAGE)
     public ModelAndView index(Model model) {

        model.addAttribute("message""Message from main controller to main page!");

        return new ModelAndView(INDEX_VIEW);
    }

    @RequestMapping(value=INFO_PAGE)
     public ModelAndView info(Model model) {

        model.addAttribute("message""Message from main controller to info page!");

        return new ModelAndView(INFO_VIEW);
    }

}


Конфигурируем Tiles

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

Все элементы конфигурации - definitions являются дочерними к корневому элементу tiles-definitions:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 2.1//EN"
       "http://tiles.apache.org/dtds/tiles-config_2_1.dtd">

<tiles-definitions>


</tiles-definitions>
Приступим к объявлению нового шаблона под выбранную выше структуру. На данном этапе предположим, что все блоки шаблона кроме контентного будут статическими:
<!-- Templates -->
<definition name="base-template" template="/WEB-INF/templates/base-template.jsp">
    <put-attribute name="title" value="Default title" />
    <put-attribute name="header" value="/WEB-INF/templates/header.jsp" />
    <put-attribute name="navigation" value="/WEB-INF/templates/navigation.jsp" />
    <put-attribute name="content" value="" />
    <put-attribute name="footer" value="/WEB-INF/templates/footer.jsp" />
</definition>
В данном объявлении шаблон под именем base-template связывается с представлением, расположенным в "/WEB-INF/templates/base-template.jsp". Кроме этого, в качестве атрибутов предаются ссылки на статические элементы - header, navigation, content, footer, а также строковый атрибут со значением заголовка по умолчанию. К реализации представления и применению в шаблоне внедренных атрибутов мы перейдем позже, а сейчас необходимо отметить следующее - при попытке обращения к шаблону из контроллера (при обращении к представлению base-template) получим (должны получить, так как представление еще не реализовано) страницу с заголовком по умолчанию, заполненными блоками header, footer, navigation, но с отсутствующим контентным блоком.

Приступим к наследованию этого шаблона для конкретных страниц, обрабатываемых нашим контроллером:
<definition name="main" extends="base-template">
    <put-attribute name="title" value="View: main" />
    <put-attribute name="content" value="/WEB-INF/templates/layouts/main.jsp" />
</definition>

<definition name="info" extends="base-template">
    <put-attribute name="title" value="View: info" />
    <put-attribute name="content" value="/WEB-INF/templates/layouts/info.jsp" />
</definition>

Объявлять представление для каждой новой страницы? Необязательно, в определениях tiles можно использовать регулярные выражения (причем допустимы два синтаксиса), поэтому предыдущее объявление можно заменить на следующее:
<definition name="*" extends="base-template">
    <put-attribute name="title" value="View: {1}" />
    <put-attribute name="content" value="/WEB-INF/templates/layouts/{1}.jsp" />
</definition>
У подобного объявления есть недостатки, при обращении к несуществующей странице в браузер будет выкинуто сообщение об исключении при поиске соответствующего файла для контентного блока. Но эта ситуация обходится несколькими способами, кроме того никто не мешает переопределить поведение Tiles в этом случае, так как доступны исходники. ^_^


Реализуем представление

В представлении нет ничего особенного, кроме наличия специфичных для Tiles включения и использования атрибутов, но их использование достаточно тривиально, так как достаточно знать только имена внедренных атрибутов из конфигурации Tiles.

/WEB-INF/templates/base-template.jsp:
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles" %>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>
            <tiles:getAsString name="title" />
        </title>
        <link rel="stylesheet" href="/css/style.css" type="text/css"/>
    </head>
    <body>

        <tiles:insertAttribute name="header" />
        <tiles:insertAttribute name="navigation" />
        <tiles:insertAttribute name="content" />
        <tiles:insertAttribute name="footer" />

    </body>
</html>
/WEB-INF/templates/header.jsp:
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<div id="header">Header</div>
/WEB-INF/templates/navigation.jsp:
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<div id="navigation">
    <ul>
        <li>
            <a href="/index.page" title="Main page">Main page</a>
        </li>
        <li>
            <a href="/info.page" title="Information page">Information page</a>
        </li>
    </ul>
</div>
/WEB-INF/templates/footer.jsp:
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<div id="footer">Footer</div>
Представления для самого шаблона реализованы, в base-template.jsp происходит вызов вложенных представлений по ссылкам, которые уже были переданы через атрибуты.

Теперь необходимо реализовать представление конкретных страниц, обрабатываемых контроллером.
/WEB-INF/templates/layouts/main.jsp и /WEB-INF/templates/layouts/info.jsp:
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<div id="content">
    <c:if test="${message != null}">
        <c:out value="${message}"/>
    </c:if>
</div>

Все представления описаны, поэтому уже можно увидеть результаты работы. Для такого простого примера все достаточно просто и в целом не вызывает вопросов, но, что если некоторые блоки шаблона также являются динамическими. Передача данных для такого блока через контроллер фактически вернет нас к объединению логики приложения и представления.


Независимые динамические блоки

Было бы удобно, чтобы некоторые блоки шаблона можно было бы связать с некотором контроллером именно для этого блока, который будет вызываться независимо от других контроллеров. В Tiles до второй версии это реализовывалось расширением специального класса контроллера (который предоставлял Spring), но так как теперь разница между контроллерами в Spring нивелировалось, то нам достаточно использовать обычный контроллер, который необходимо будет привязать к конкретному блоку в конфигурации Tiles. Просто так взять контроллер и связать его с шаблоном право тоже не выйдет, но Tiles предоставляет интересную стратегию для предобработчиков, которые уже связываются с конкретным блоком.

Предобработчики должны расширять класс ViewPreparerSupport, предоставляемый Tiles. Так как других требований нет, то этот предобработчик может диспетчезироваться из Spring как обычный контроллер, что позволяет использовать автоприсвоение (autowiring) и т.д. и т.п.

В качестве примера динамического блока возьмем статический на данный момент блок заголовка. НО начнем с написания контроллера:
package ru.sultry.controllers;

import org.apache.tiles.AttributeContext;
import org.apache.tiles.context.TilesRequestContext;
import org.apache.tiles.preparer.ViewPreparerSupport;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;

/**
 * @author tsarev.oi@mail.ru
 *         User: Oleg Tsarev
 *         Date: 10.11.2010
 *         Time: 15:06:25
 *
 */

@Controller("headerController")
@Scope("session")
public class HeaderController extends ViewPreparerSupport{

    @Override
    public void execute(TilesRequestContext tilesContext,
                        AttributeContext attributeContext) {

        // Get access to model parameters from MainController as example
        String message = (String) tilesContext.getRequestScope().get("message");

        tilesContext.getRequestScope().put("headerMessage""Message from header!");

    }

}
Как видно из примера, в данном контроллере имеется доступ к данным передаваемым из основного контроллера, а также к атрибутам Tiles. При этом данный предобработчик находиться под управлением Spring, что позволяет связать его со всем приложением.

Теперь необходимо изменить представление блока заголовка и связать definition с конкретным предобработчиком. Представление измениться следующим образом:
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<div id="header">
    Header<br/>
    <c:if test="${headerMessage != null}">
        <c:out value="${headerMessage}"/>
    </c:if>
</div>

А для того, чтобы связать заголовок с предобработчиком придется изменить и описание шаблона:
<!-- Templates -->
<definition name="base-template" template="/WEB-INF/templates/base-template.jsp">
    <put-attribute name="title" value="Default title" />
    <put-attribute name="header" value="header" />
    <put-attribute name="navigation" value="/WEB-INF/templates/navigation.jsp" />
    <put-attribute name="content" value="" />
    <put-attribute name="footer" value="/WEB-INF/templates/footer.jsp" />
</definition>

<definition name="header"
                 preparer="headerController"
                 template="/WEB-INF/templates/header.jsp">
</definition>
Вот и все, теперь контроллер будет передавать собственное сообщение в представление.

Важно обратить внимание, что предобработчики отрабатывают после основного конроллера, поэтому при неосторожности можно переопределить те данные которые вернул главный контроллер.

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

Progg it

10 комментариев:

  1. Отличная статейка! Спасибо, будем пользовать :)

    ОтветитьУдалить
  2. не работает

    SEVERE: Exception sending context initialized event to listener instance of class org.springframework.web.context.ContextLoaderListener
    java.lang.NoClassDefFoundError: org/slf4j/impl/StaticLoggerBinder

    ОтветитьУдалить
    Ответы
    1. Данное исключение связанно с тем, что в вашем classpath нет данного класса. В данном случае необходимо добавить библиотеку slf4j.

      Удалить
  3. в pom.xml нужно добавить:

    org.slf4j
    slf4j-simple
    1.6.1





    org.apache.tiles
    tiles-api
    2.2.2


    org.apache.tiles
    tiles-core
    2.2.2


    org.apache.tiles
    tiles-jsp
    2.2.2


    org.apache.tiles
    tiles-servlet
    2.2.2


    org.apache.tiles
    tiles-template
    2.2.2

    ОтветитьУдалить
    Ответы
    1. Все верно, я опустил полное описание зависимостей и оставил это на усмотрение разработчиков. Все же существует множество инструментов сборок и диспетчеризации зависимостей.

      Удалить
  4. "Конфигурируем Tiles" - в каких файлах это писать?

    ОтветитьУдалить
    Ответы
    1. /WEB-INF/tiles/tiles-templates.xml - это именно конфигурация шаблонов Tiles.

      Удалить
  5. Спасибо, получилось. Теперь пытаюсь прикрутить поверх tiles, webflow уже 2 недели ничего не выходит.

    ОтветитьУдалить
  6. делаю по этой статье http://www.springbyexample.org/examples/simple-spring-web-flow-webapp.html не получается смапить не могу понять где задаётся вебфлов'у какие url он должен обрабатывать...

    ОтветитьУдалить
  7. Hi,
    Thanks for this tutorial, can you please post the source code ?
    Thanks

    ОтветитьУдалить