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 Май 2008 г.

Строим сервлеты на базе Guice


Компания google - основной разработчик IoC контейнера GUICE позиционирует его как легковесное решение для построения Java-приложений различных типов, в том числе и веб-приложений. Для этого существует пакет com.google.inject.servlet, о котором мы сегодня и поговорим.

Прежде всего хочу отметить тот факт, что снапшот guice-servlet в SVN и пакет, включенный в guice 1.0 отличаются - версия в SVN имеет более расширенный функционал. В частности - базовый сервлет, обеспечивающий инъекцию зависимостей и контекст-листенер, обеспечивающий подключение Guice.Injector. Поэтому для примеров в данном посте я использовал версию из SVN.

Рассмотрим вариант сборки guice-servlet с минимальной выкачкой исходников. Для этого необходимо только зачекаутить каталог servlet из SVN trunk'а. Далее, в тот каталог, куда зачекаутен servlet необходимо скопировать common.xml из trunk'а GUICE. В каталоге servlet лежит каталог lib, его необходимо поместить в ТОТ каталог, В КОТОРОМ лежит каталог servlet. Далее необходимо в каталог lib/bin скопировать guice-1.0.jar Но тут наступает разочарование - guice-servlet не соберется... Потому что в текущей версии guice-1.0.jar нет класса OutOfScopeException. Однако и это лечится - создам в servlet/src пакет com.google.inject, в который помещаем класс OutOfScopeException следующего содержания:

package com.google.inject;



/**

 * Thrown from {@link Provider#get} when an attempt is made to access a scoped

 * object while the scope in question is not currently active.

 *

 * @author kevinb@google.com (Kevin Bourrillion)

 */


@SuppressWarnings("serial")

public class OutOfScopeException extends RuntimeException {

   

    public OutOfScopeException(String message) {

        super(message);

    }



    public OutOfScopeException(String message, Throwable cause) {

        super(message, cause);

    }



    public OutOfScopeException(Throwable cause) {

        super(cause);

    }

}

 


После этого стоит запустить ant jar и ву-а-ля получим build/guice-servlet-snapshot.jar, который и будем использовать.

Теперь будем писать небольшое веб-приложение. Начнем как всегда в случае guice с интерфейса простого сервиса. Создадим интерфейс IDemoService следующего содержания:

package com.blogspot.samolisov.service;



public interface IDemoService {

   

    public String invoke(RequestData data);

}

 


Реализация данного сервиса содержит ссылку на класс RequestData, который инкапсулирует некие данные (в нашем случае просто число). Данный класс нужен нам для дальнейшей демонстрации таких возможностей guice-servlet, как определение времени жизни класса. Но не будем забегать вперед.

Итак, класс SimpleDemoService:

package com.blogspot.samolisov.service;



public class SimpleDemoService implements IDemoService {   

   

    public String invoke(RequestData data) {

        data.incCount();

        return "SimpleDemoService " + data.getCount();

    }   

}

 


Класс RequestData:

package com.blogspot.samolisov.service;



import com.google.inject.servlet.RequestScoped;



@RequestScoped

public class RequestData { 

   

    private int count = 0;

   

    public int getCount() {

        return count;

    }

   

    public void incCount() {

        count++;

    }

}

 


Пока не стоит обращать внимания на аннотацию @RequestScoped, ее роль будет описана ниже.

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

package com.blogspot.samolisov.service;



import com.google.inject.AbstractModule;

import com.google.inject.Scopes;



public class MyModule extends AbstractModule {



    @Override

    protected void configure() {

        bind(IDemoService.class)

            .annotatedWith(DemoService.class)

            .to(SimpleDemoService.class)

            .in(Scopes.SINGLETON);

       

        bind(RequestData.class);       

    }

}

 


Как видим данная часть ничем не отличалась от создания обычного консольного либо GUI-приложения основанного на Guice. Теперь же пришло время создать систему разрешения зависимостей применительно к WEB. Для этого нам необходимо создать Guice.Injector, разрешающий зависимости, описанные в MyModule. Создавать Guice.Injector нужно в ServletContextListener'е. Существует абстрактный класс GuiceServletContextListener в котором уже реализована логика регистрации Injector'а в контексте сервлета. Необходимо создать его наследника, реализующего метод getInjector(), который собственно и создает нужный Injector. Код класса DemoServiceContextListener:

package com.blogspot.samolisov.service;



import com.google.inject.Guice;

import com.google.inject.Injector;

import com.google.inject.Module;

import com.google.inject.servlet.GuiceServletContextListener;

import com.google.inject.servlet.ServletModule;



public class DemoServiceContextListener extends GuiceServletContextListener {



    @Override

    protected Injector getInjector() {

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

    }

}

 


Ну и собственно венец программы - сервлет, который демонстрирует использование сервиса. Опять же в guice-servlet содержится класс InjectedHttpServlet - абстрактный сервлет в методе init которого осуществляется получение Injector'а и разрешение зависимостей. Любой сервлет, в котором будет использоваться инъекция зависимостей должен быть его наследником.

package com.blogspot.samolisov.service;



import java.io.IOException;



import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;



import com.google.inject.Inject;

import com.google.inject.Injector;

import com.google.inject.servlet.InjectedHttpServlet;



@SuppressWarnings("serial")

public class DemoServlet extends InjectedHttpServlet {

   

    private IDemoService service;



    @Inject

    public void injectServiceInstance(@DemoService IDemoService service) {

        this.service = service;

    }   



    @Override

    protected void service(HttpServletRequest request, HttpServletResponse response)

            throws ServletException, IOException {

        Injector injector = (Injector) getServletContext()

                .getAttribute(Injector.class.getName());

       

        RequestData data = injector.getInstance(RequestData.class);

       

        response.getWriter().println("Service instance : " + service.invoke(data));

    }

}

 


Осталось только собрать все вместе, используя дескриптор развертывания web.xml:

<?xml version='1.0' encoding='UTF-8'?>

<!DOCTYPE web-app PUBLIC

 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"

 "http://java.sun.com/dtd/web-app_2_3.dtd">



<web-app>

    <servlet>

        <servlet-name>DemoServlet</servlet-name>

        <servlet-class>com.blogspot.samolisov.service.DemoServlet</servlet-class>

        <load-on-startup>0</load-on-startup>

    </servlet>



    <servlet-mapping>

        <servlet-name>DemoServlet</servlet-name>

        <url-pattern>/demo/*</url-pattern>

    </servlet-mapping>

   

    <listener>

        <listener-class>com.blogspot.samolisov.service.DemoServiceContextListener</listener-class>

    </listener>    

           

    <filter>

        <filter-name>guice</filter-name>

        <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>

    </filter>



    <filter-mapping>

        <filter-name>guice</filter-name>

        <url-pattern>/*</url-pattern>

    </filter-mapping>

       

</web-app>


Структура дескриптора развертывания стандартна: определяем сервлет DemoServlet, подключаем контекст-листенер, который регистрирует Injector в контексте сервлета, затем подключаем фильтр guice, реализуемый классом com.google.inject.servlet.GuiceFilter. Данный фильтр нужен для корректной работы аннотаций @RequestScoped и @SessionScoped.

Пришло время поговорить об этих аннотациях подробнее. В guice-servlet реализованы две аннотации: @RequestScoped и @SessionScoped, которые задают время жизни инъенктированных объектов применительно к WEB'у. Аннотация @RequestData - задает время жизни, равное времени жизни HTTP-запроса. Это обозначает, что класс аннотированый @RequestData создается заново при каждом запросе. Класс же аннотированный @SessionData хранится в сессии, соответственно его время жизни равно времени жизни сессии.

Такая семантика применения аннотаций задает специфику применения объектов, которые их используют. Во первых, данные объекты привязываются к запросу, тем самым их создание с помощью фабрики Guice возможно только в методах обработки запроса, без запроса их создание невозможно. Поэтому нельзя инъектировать объекты, аннотированные @RequestScoped и @SessionScoped так же как и все остальные (имеется ввиду с помощью аннотации @Inject). Потому что разрешение объектов с помощью @Inject происходит при выполнении метода init сервлета. Как вы понимаете в этот момент запроса не существует, поэтому корректное разрешение зависимостей, аннотированных @RequestScoped или @SessionScoped в этот момент невозможно (результатом этой невозможности будет замечательный эксепшн).

Второй особенностью использования данных аннотаций является необходимость использования GuiceFilter при обработке запросов. Именно данный фильтр занимается извлечением объектов из сессии или запроса. В случае попытки получить инстанцы объектов, аннотированых @RequestScoped или @SessionScoped без использования данного фильтра будет сгенерировано OutOfScopeException.

Но вернемся к нашему DemoServlet'у. В результате его выполнения будет выведено в браузер:
Service instance : SimpleDemoService 0

Если обновлять страницу, то несмотря на то, что в коде класса SimpleDemoService есть строчка data.incCount(), будет выводится 0. Просто потому-что объект data при каждом запросе будет создан заново.

Если же же аннотировать класс RequestData с помощью @SessionScoped, то все объекты класса RequestData привязаны к сессии и теперь при каждом обновлении страницы число после SimpleDemoService будет увеличиваться на единицу.

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


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

23 Апрель 2008 г.

Пишем плагинную шину с использованием Guice


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

Сегодня мы поговорим о реализации примитивной плагинной шины с помощью IoC-контейнера от гугл - Guice. Реализация плагинной шины с помощью IoC-контейнера дает нам все преимущества паттерна Inversion Of Control. Мы легко можем реализовывать с использованием этого паттерна как базовые какие-то механизмы (API приложения, доступное плагинам), так и сами плагины, что очень удобно. Так-же использование Guice облегчает сам процесс подключения плагинов к системе, как именно - об этом поговорим чуть позже.

Начем с того, что такое вообще плагин, и какая особенность создания плагинов в приложениях, написаных на Java. Впрочем думаю, что ни для кого не секрет, что плагин - модуль приложения, который разрабатывается отдельно от основной программы, компилируется тоже отдельно. Взаимодействие плагина с программой строго ограничено набором определенных классов и интерфейсов (в терминах Java). Очень часто такой набор называют API. В самом примитивном случае - приложение выставляет некий набор интерфейсов, которые должны быть реализованы плагином. При реализации этих интерфейсов плагин может и должен использовать некий набор классов, предоставляемых приложением (например классы работы с БД, классы работы с GUI и т.д.). По реализации плагинной архитектуры на Java есть замечательная статья, пересказывать которую не имеет смысла, я лишь коротко опишу основные проблемы, которые необходимо решить при создании плагинной шины:

1. Загрузка классов из jar-файлов, не входящих в classpath-приложения. Дело в том, что зачастую плагины устанавливаются просто копированием в некий каталог, например plugins, при этом наше приложение не должно знать до своего запуска, какие там лежат плагины и в каких файлах. Эта информация должна быть доступна только в рантайме. Собственно дальше я расскажу как написать такой загрузчик и оформить его в виде guice-провайдера.

2. Разделение областей видимости классов плагина и классов основного прииложения. Плагин пишет сторонний разработчик и понятно, что хитрость человеческая безмерна. Поэтому плагин не должен влиять на приложение ни при каких условиях. Взаимодействие плагина и приложения ограничено рамками API, предоставляемого приложением плагину. В рамках Java данная проблема решается применением отдельных загрузчиков классов для общедоступного API, основного приложения и плагинов.

3. Разделение областей видимости между плагинами. В общем случае плагины пишутся разными людьми и поэтому возможно использование одинаковых имен для классов и даже пакетов. Решается данная проблема использованием для каждого плагина своего загрузчика классов.

С проблемами определились, теперь попробуем разобраться на примере как их решать, используя Guice для построения расширяемой плагинной шины на базе IoC. Рекомендую сразу набраться терпения, потому что проблемы весьма нетривиальны.

Сначала определимся с API для работы с плагинами. Как я уже писал ранее в основе Guice лежит понятия модуля, а что такое плагин, если не модуль. Соответственно каждый наш плагин должен реализовывать некий интерфейс (для примера возьмем очень простой интерфейс с одним методом invoke) и предоставлять свой контекст вызывающему приложению. Контекст плагина это фактически и есть модуль Guice, который описывает зависимости в плагине. Такой подход позволяет очень упростить как сам код плагина (вводя патерн IoC в плагин), так и взаимодействие плагина и приложения.

Итак, код интерфейса, который должен реализовывать плагин очень прост:
package ru.caffeineim.api.plugins;



public interface IPlugin { 

    public void invoke();

}

 


Вообще давайте сразу определимся с тем, что значит "плагин должен реализовывать интерфейс". Любой плагин должен иметь так называемый основной класс. Именно этот класс будет отвечать со стороны плагина за взаимодействие с вызывающим приложением. Так вот, этот основной класс должен реализовывать интерфейс IPlugin, через который будет осуществлятся взаимодействие приложение - плагин.

Контекст плагина тоже не должен вызывать сложностей при реализации. Это просто интерфейс с одним методов, возвращающим модуль (в терминах Guice). Данный модуль будет использоваться для инъекции зависимостей, а значит для корректного построения плагина. Код контекста плагина:
package ru.caffeineim.api.plugins;



import com.google.inject.Module;



public interface IPluginContext {

    public Module getModule()

}

 


Таким образом мы отделили использование плагина (IPlugin) и построение плагина (IPluginContext).

Теперь разберемся как раз таки с самым главным - загрузкой классов плагина. Чем хорош Guice - он позволяет создавать свои фабрики классов (в терминах Guice - провайдеры), которые будут использоваться для разрешения зависимостей (т.е. для создания классов, реализующих интерфейсы, описанные как зависимости). Напишем свой загрузчик классов плагинов и оформим его как Guice-провайдер. Приведу код провайдера:

package ru.caffeineim.guice.plugincontainer;



import java.io.File;

import java.io.FileFilter;

import java.net.URL;

import java.net.URLClassLoader;

import java.util.ArrayList;

import java.util.List;

import java.util.jar.JarFile;

import java.util.jar.Manifest;



import ru.caffeineim.api.plugins.IPluginContext;

import ru.caffeineim.core.manifest.IManifestParser;

import ru.caffeineim.core.manifest.ManifestParser;

import ru.caffeineim.core.manifest.Manifestor;



import com.google.inject.Inject;