9 Май 2008 г.

Развивая тему Guice: прикручиваем Velocity с помощью Guice.


Данный пост написан не с целью развития холивара на тему, что лучше Velocity или JSP, но здесь я хочу продемонстрировать как можно подключать такой удобный шаблонный движок, как Velocity к приложению, построенному на базе Guice.

О том, что такое Velocity и многих аспектах его использования можно почитать на форуме программистов и конечно же официальной странице проекта.

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

package ru.sposition.web.view;



import java.io.Writer;

import java.util.Map;



import ru.sposition.web.view.exception.TemplateEngineException;



public interface ITemplateEngine {

   

    public void mergeTemplate(String templateLocation, Map<String, Object> model,

            Writer writer) throws TemplateEngineException;

   

    public void mergeTemplate(String templateLocation, String encoding,

            Map<String, Object> model, Writer writer) throws TemplateEngineException;

   

    public String mergeTemplateIntoString(String templateLocation,

            Map<String, Object> model) throws TemplateEngineException;

   

    public String mergeTemplateIntoString(String templateLocation, String encoding,

            Map<String, Object> model) throws TemplateEngineException;

}

 


Все взаимодействие приложения с механизмом шаблонов теперь достаточно строить через данный интерфейс, он предоставляет все необходимые для этого методы. Для того, чтобы использовать в качестве шаблонного движка Velocity необходимо реализовать данный интерфейс с использованием VelocityEngine. Собственно данный механизм реализован в проекте Spring Framework (класс VelocityEngineUtils), код данной реализации и возьмем за основу, слегка его модифицировав:

package ru.sposition.web.view.velocity;



import java.io.StringWriter;

import java.io.Writer;

import java.util.Map;



import org.apache.velocity.VelocityContext;

import org.apache.velocity.app.VelocityEngine;



import ru.sposition.web.view.ITemplateEngine;

import ru.sposition.web.view.exception.TemplateEngineException;

import ru.sposition.web.view.velocity.annotations.Velocity;



import com.google.inject.Inject;



/**

 * Utility class for working with a VelocityEngine. Provides convenience methods

 * to merge a Velocity template with a model.

 *

 * @author Juergen Hoeller

 * @author Samolisov Pavel <samolisov@gmail.com>

 */


public class VelocityTemplateEngine implements ITemplateEngine {

   

    private VelocityEngine velocityEngine;

       

    @Inject

    public void injectVelocitiEngine(@Velocity VelocityEngine engine) {

        this.velocityEngine = engine;

    }

   

    /**

     * Merge the specified Velocity template with the given model and write the

     * result to the given Writer.

     *

     * @param templateLocation the location of template, relative to Velocity's resource loader path

     * @param model the Map that contains model names as keys and model objects as values

     * @param writer the Writer to write the result to

     * @throws TemplateEngineException if the template wasn't found or rendering failed

     */


    public void mergeTemplate(String templateLocation, Map<String, Object> model,

            Writer writer) throws TemplateEngineException {

        try {

            VelocityContext velocityContext = new VelocityContext(model);

            velocityEngine.mergeTemplate(templateLocation, velocityContext, writer);       

        } catch (RuntimeException ex) {

            throw ex;

        } catch (Exception ex) {

            throw new TemplateEngineException(ex.getMessage());

        }

    }



    /**

     * Merge the specified Velocity template with the given model and write the

     * result to the given Writer.

     *

     * @param templateLocation the location of template, relative to Velocity's resource loader path

     * @param encoding the encoding of the template file

     * @param model the Map that contains model names as keys and model objects as values

     * @param writer the Writer to write the result to

     * @throws TemplateEngineException if the template wasn't found or rendering failed

     */


    public void mergeTemplate(String templateLocation, String encoding,

            Map<String, Object> model, Writer writer) throws TemplateEngineException {

        try {

            VelocityContext velocityContext = new VelocityContext(model);

            velocityEngine.mergeTemplate(templateLocation, encoding, velocityContext, writer);     

        } catch (RuntimeException ex) {

            throw ex;

        } catch (Exception ex) {

            throw new TemplateEngineException(ex.getMessage());

        }

    }



    /**

     * Merge the specified Velocity template with the given model into a String.

     *

     * @param templateLocation the location of template, relative to Velocity's resource loader path

     * @param model the Map that contains model names as keys and model objects as values

     * @return the result as String

     * @throws TemplateEngineException if the template wasn't found or rendering failed

     */


    public String mergeTemplateIntoString(String templateLocation,

            Map<String, Object> model) throws TemplateEngineException {

        StringWriter result = new StringWriter();

        mergeTemplate(templateLocation, model, result);

        return result.toString();

    }



    /**

     * Merge the specified Velocity template with the given model into a String.

     *

     * @param templateLocation the location of template, relative to Velocity's resource loader path

     * @param encoding the encoding of the template file

     * @param model the Map that contains model names as keys and model objects as values

     * @return the result as String

     * @throws TemplateEngineException if the template wasn't found or rendering failed     

     */


    public String mergeTemplateIntoString(String templateLocation, String encoding,

            Map<String, Object> model) throws TemplateEngineException {

        StringWriter result = new StringWriter();

        mergeTemplate(templateLocation, encoding, model, result);

        return result.toString();

    }

}


Вот теперь и начинается самое интересное - нам нужно создать экземпляр класса VelocityEngine и инъекцировать его в VelocityTemplateEngine. Само по себе создание объекта данного класса - задача не сложная, сложности возникают тогда, когда этот объект надо сконфигурировать. Дело в том, что для корректной работы Velocity ему необходимо указать как минимум - путь к каталогу с шаблонами, кодировку этих шаблонов и кодировку результата. Есть и ряд дополнительных параметров, но я не стал с ними заморачиватся, кому интересно сможет разобраться, посмотрев на конфигурацию кодировок. Замечу, что в проекте Spring Framework существует фабрика VelocityEngineFactory, которую мы и возьмем за основу нашего Guice-конфигуратора.

Сразу разберемся с терминологией. В терминологии Guice фабрика объектов называется провайдером. Про механизм расширения Guice-провайдеров я уже писал ранее. Сейчас мы попробуем написать провайдер, который создает объект класса VelocityEngine, используя конфигурации заданные с помощью инъекции констант (в Spring Framework используется xml). Повторюсь, что в данной реализации настраивается только путь к каталогу с шаблонами, кодировка входных файлов, кодировка результата, остальное впринципе не сложно добавить. Начнем с кода провайдера:

package ru.sposition.web.view.velocity.provider;



import java.io.IOException;

import java.util.HashMap;



import org.apache.velocity.app.VelocityEngine;

import org.apache.velocity.exception.VelocityException;

import org.apache.velocity.runtime.RuntimeConstants;



import ru.sposition.web.view.velocity.provider.annotations.InputEncoding;

import ru.sposition.web.view.velocity.provider.annotations.OutputEncoding;

import ru.sposition.web.view.velocity.provider.annotations.ResourceLoaderPath;



import com.google.inject.Inject;

import com.google.inject.Provider;



public class VelocityEngineProvider implements Provider<VelocityEngine> {



    @Inject

    @ResourceLoaderPath

    private String resourceLoaderPath;

   

    private HashMap<String, String> properties = new HashMap<String, String>();

   

    @Inject

    public void injectInputEncoding(@InputEncoding String inputEncoding) {

        properties.put("input.encoding", inputEncoding);

    }

   

    @Inject

    public void injectOutputEncoding(@OutputEncoding String outputEncoding) {

        properties.put("output.encoding", outputEncoding);

    }

       

    public VelocityEngine get() {

        try {

            VelocityEngine velocityEngine = newVelocityEngine();

            if (resourceLoaderPath != null)

                initVelocityResourceLoader(velocityEngine, resourceLoaderPath);

           

            for (String property : properties.keySet()) {

                velocityEngine.setProperty(property, properties.get(property));

            }

       

            velocityEngine.init();

           

            return velocityEngine;

        } catch (Exception e) {

            e.printStackTrace();

            return null;

        }

    }



    protected VelocityEngine newVelocityEngine() throws IOException, VelocityException {

        return new VelocityEngine();

    }

   

    protected void initVelocityResourceLoader(VelocityEngine engine, String resourceLoaderPath) {

        engine.setProperty(RuntimeConstants.RESOURCE_LOADER, "file");        

        engine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_CACHE, "true");

        engine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, resourceLoaderPath);

    }

   

    public static Provider<VelocityEngine> initProvider() {

        return new VelocityEngineProvider();

    }

}

 


Основа кода провайдера - метод get(). В данном методе создается новый экземпляр класса VelocityEnginе и заполняется параметрами, которые инъектируются в класс. Инъекция параметров осуществляется с помощью соответствующих методов и фактически просиходит добавление ЗНАЧЕНИЙ параметров в специальный Map. Данный Map хранит соответствие имен параметров в терминологии VelocityEngine и их значений. Соответствие имен и значений задается аннотациями: @ResourceLoaderPath, @InputEncoding, @OutputEncoding. Данные аннотации - ничто иное как стандартные биндинг-аннотации. Все они имеют весьма тривиальный код, который покажу на примере @InputEncoding:

package ru.sposition.web.view.velocity.provider.annotations;



import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;



import com.google.inject.BindingAnnotation;



@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.FIELD, ElementType.PARAMETER})

@BindingAnnotation

public @interface InputEncoding {



}

 


Про настройку инъектирования с помощью аннотаций говорилось здесь.

Небольшое замечание по поводу параметра resourceLoaderPath. В ДАННОЙ реализации он должен быть равен АБСОЛЮТНОМУ пути к каталогу с шаблонами. В фабрике из Spring Framework такого ограничения нет, но там и более сложный процесс разрешения этого пути с использованием специальных лоадеров. Тянуть их из Spring Framework я посчитал нецелесообразным.

Ну и теперь пришло время собрать все эти механизмы в отдельный модуль - ViewModule (view - вид, в терминах MVC). Код модуля будет следующий:

package ru.sposition.web.view;



import org.apache.velocity.app.VelocityEngine;



import ru.sposition.web.view.annotations.TemplateEngine;

import ru.sposition.web.view.velocity.VelocityTemplateEngine;

import ru.sposition.web.view.velocity.annotations.Velocity;

import ru.sposition.web.view.velocity.provider.VelocityEngineProvider;

import ru.sposition.web.view.velocity.provider.annotations.InputEncoding;

import ru.sposition.web.view.velocity.provider.annotations.OutputEncoding;

import ru.sposition.web.view.velocity.provider.annotations.ResourceLoaderPath;



import com.google.inject.AbstractModule;

import com.google.inject.Scopes;



public class ViewModule extends AbstractModule {

   

    private static final String RESOURCE_LOADER_PATH = "/WEB-INF/velocity/";

    private static final String INPUT_ENCODING = "utf-8";

    private static final String OUTPUT_ENCODING = "utf-8";

   

    private final String resourcePathPrefix;

   

    public ViewModule(String resourcePathPrefix) {

        this.resourcePathPrefix = resourcePathPrefix;

    }

   

    @Override

    protected void configure() {

        bindConstant()

            .annotatedWith(ResourceLoaderPath.class)

            .to(resourcePathPrefix + RESOURCE_LOADER_PATH);

       

        bindConstant()

            .annotatedWith(InputEncoding.class)

            .to(INPUT_ENCODING);

       

        bindConstant()

            .annotatedWith(OutputEncoding.class)

            .to(OUTPUT_ENCODING);

       

        bind(VelocityEngine.class)

            .annotatedWith(Velocity.class)

            .toProvider(VelocityEngineProvider.initProvider())

            .in(Scopes.SINGLETON);;

       

        bind(ITemplateEngine.class)

            .annotatedWith(TemplateEngine.class)

            .to(VelocityTemplateEngine.class)

            .in(Scopes.SINGLETON);

    }

}

 


Как видим в конструкторе данный модуль получает параметр resourcePathPrefix - это префикс к каталогу /WEB-INF/velocity/ в файловой системе, фактически это каталог, в котором находится наше веб-приложение. Получать этот префикс мы будем с помощью сервлет-апи. В случае если пишется не веб-приложение, а, например, десктоп-приложение, получать каталог в котором оно установлено придется системными средствами.

Ну и продемонстрируем использование созданного механизма. Создадим несложный сервлет, который парсит Velocity-шаблон и выводит результат в окно браузера. Код сервлета следующий:

package ru.sposition.web.controller;



import java.io.IOException;

import java.util.HashMap;

import java.util.Map;



import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;



import ru.sposition.web.view.ITemplateEngine;

import ru.sposition.web.view.annotations.TemplateEngine;

import ru.sposition.web.view.exception.TemplateEngineException;



import com.google.inject.Inject;

import com.google.inject.servlet.InjectedHttpServlet;



@SuppressWarnings("serial")

public class VelocityDemoServlet extends InjectedHttpServlet {

   

    private ITemplateEngine templateEngine;



    @Inject

    public void injectTemplateEngine(@TemplateEngine ITemplateEngine engine) {     

        templateEngine = engine;

    }



    @Override

    protected void service(HttpServletRequest request, HttpServletResponse response)

            throws ServletException, IOException {



        Map<String, Object> model = new HashMap<String, Object>();

        model.put("hello", "Welcome to Hell!");

        model.put("title", "Demo servlet title");

       

        response.setContentType("text/html; charset=UTF-8");

        try {

            templateEngine.mergeTemplate("index.vm", model, response.getWriter());

        } catch (TemplateEngineException e) {      

            e.printStackTrace();

            throw new ServletException(e.getMessage());

        }

    }

}

 


Как видим сервлет даже не знает ничего о Velocity, он инъектирует шаблонный движок (ITemplateEngine) и его уже использует. Напомню, что для разрешения зависимостей сервлету нужен Injector, который он пытается взять из своего контекста. Создается Injector и помещается в контекст-сервлета с помощью ServletContextListener'а:

package ru.sposition.web.controller;



import javax.servlet.ServletContext;

import javax.servlet.ServletContextEvent;

import javax.servlet.ServletContextListener;



import ru.sposition.web.view.ViewModule;



import com.google.inject.Guice;

import com.google.inject.Injector;

import com.google.inject.Module;

import com.google.inject.servlet.ServletModule;



public class VelocityDemoServletContextListener implements ServletContextListener {

   

    public void contextInitialized(ServletContextEvent servletContextEvent) {

        ServletContext servletContext = servletContextEvent.getServletContext();

        servletContext.setAttribute(Injector.class.getName(), getInjector(servletContext));

    }

   

    public void contextDestroyed(ServletContextEvent servletContextEvent) {

        ServletContext servletContext = servletContextEvent.getServletContext();

        servletContext.removeAttribute(Injector.class.getName());

    }

       

    protected Injector getInjector(ServletContext servletContext) {    

        return Guice.createInjector(new Module[] {new ServletModule(),

                new ViewModule(servletContext.getRealPath("/"))});

    }

}

 


Код servletContext.getRealPath("/") как раз таки и служит для получения каталога, в который установлено веб-приложение.

Ну и напоследок - код демонстрационного шаблона:

<?xml version="1.0" encoding="windows-1251"?>

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru" lang="ru">

    <head>

        <title>${title}</title>

        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>

        <link rel="stylesheet" type="text/css" href="/style.css" />    

    </head>

    <body>

        <h1>${hello}</h1>

    </body>

</html>    

 


Вот собственно и все :) - веб приложение, использующее Velocity создано. Как всегда, готов ответить на ваши вопросы, с радостью приму любые коментарии, замечания, дополнения.

Понравилось сообщение - подпишись на блог

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

iZEN комментирует...

Приветствую.
Выразите, пожалуйста, Ваше отношение к Velocity vs. JSF. В каких вещах каждая из этих двух технологий превосходит друг друга.

Я так понял, что Velocity просто шаблонный движок, надстройка над JSP.
JSF это не только надстройка над JSP, но и компонентная технология, предоставляющая возможность управлять переходами между страницами, включать сложные валидаторы данных (в том числе подключать другие фреймворки), и аправлять рендерингом элементов на страницах.

В чём суть ажиотажа вокруг Velocity? Ведь то же самое можно делать и в JSF.

Samolisov Pavel комментирует...

Здравствуйте, большое спасибо за ваш вопрос. Попробую на него ответить.

Velocity это шаблонный движок, но надстройкой над JSP он не является, он вообще не имеет никакого отношения к JSP. Вообще Velocity можно использовать не только как View в веб-приложениях, но и везде где требуется генерация отчетов, платежек и прочей нечести. В частности в Naumen DMS мы используем Velocity для генерации почтовых сообщений.

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