пятница, 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 будет увеличиваться на единицу.

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


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

4 комментария:

Анонимный комментирует...

Не совсем по теме, но еще есть такая популярная штука как struts 2, для которой разработан плагин(http://cwiki.apache.org/S2PLUGINS/guice-plugin.html), позволяющий пользовать guice для как ioc container для стратсов. Имхо - это более правильное архитектурное решение чем использование обычных сервлетов.

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

Большое спасибо за ваш комментарий.

Я знаю про struts, struts2, spring mvc, tapestry и другие веб-фреймворки. Но не согласен с вами по поводу того, что их использование - единственно правильное решение. Не согласен по двум причинам:

1. Любой самый навороченный фреймворк в сердце своем имеет сервлет (сервлеты). Поэтому знать как они строятся в том числе с использованием Guice как минимум полезно. С целью рассказать об этом и была написана данная статья.

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

З.Ы. Сейчас пишу автоматический анализатор позиций (сказывается околосеошное прошлое). Решил разобраться с тем как вообще могут быть устроены фреймворки и выбрал связку Guice + Servlets + Velocity. Пока получается довольно не плохо.

Анонимный комментирует...

Согласен, что не стратсом единым жив человек, тем более для таких элементарных задач, как отправка почты из формы :)

Можно я подкину тему для еще одной статейки? В связи с бурным развитием RIA возможно вашим читателям будет интересно почитать про интеграцию BlazeDS и Guice. Некоторое время назад Jeff Vroom написал Spring Factory для интеграции со спринговыми приложениями, можно попытаться сделать то же самое для Guice. Думаю народ оценит :)

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

Что то маловато у guice контекстов для хранения объектов:) Например у Seam`а поболее будет:) http://docs.jboss.org/seam/2.1.0.SP1/api/org/jboss/seam/ScopeType.html

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!