понедельник, 12 апреля 2010 г.

Сервлеты и OSGi: о любви и дружбе между сервлетами и OSGi


Как известно, одной из основных областей применения языка и платформы Java является разработка веб-приложений, по крайней мере в России, если ищут программиста на данном языке, то в описании вакансии как правило указывают J2EE, Servlets, JSP, JSF и прочие умные слова. В сферу моих интересов входит разработка модульных приложений с использованием технологии OSGi и у меня есть хорошая новость: сервлеты тоже могут быть модульными и могут интегрироваться с данной технологией.

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



Введение


Сервлет - это Java-класс, который реализует интерфейс javax.servlet.Servlet. Сервлет, который осуществляет взаимодействие по протоколу HTTP может наследоваться от класса javax.servlet.HttpServlet и, в случае необходимости, перегружать его методы (например, метод init, отвечающий за инициализацию сервлета, метод doGet, содержащий логику обработки GET-запросов, метод doPost, содержащий логику обработки POST-запросов и т.д.). В качестве примера рассмотрим код простого сервлета, который говорит нам "Добро пожаловать" в одно очень приятное место:

package name.samolisov.servlet.demo2.servlet;



import java.io.IOException;

import java.io.PrintWriter;



import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;



public class WelcomeServlet extends HttpServlet

{

    private static final long serialVersionUID = -5860886627046557680L;



    @Override

    protected void doGet(HttpServletRequest request, HttpServletResponse response)

    {

        String name = request.getParameter("name");

        try

        {

            response.setStatus(HttpServletResponse.SC_OK);

            PrintWriter writer = response.getWriter();

            if (name == null || name.length() < 1)

                writer.write("<h1>Welcome to Hell</h1>");

            else

                writer.write("<h1>" + name + " welcome to Hell</h1>");

            writer.flush();

            writer.close();

        }

        catch (IOException e)

        {

            e.printStackTrace();

        }

    }

}

 


Чтобы сервлет был доступен по протоколу HTTP, его необходимо зарегистрировать в сервлет-контейнере. В мире J2EE для этого используется дескриптор развертывания - файл web.xml, однако, при интеграции сервлета в OSGi-контейнер необходимо поступить иначе: зарегистрировать сервлет в OSGi-сервисе HttpService. Сделать это можно двумя путями:

1. С помощью механизма точек расширения.

2. С помощью динамического ServiceTracker'а.

Рассмотрим каждый вариант подробнее.

Использование точек расширения


В бандле org.eclipse.equinox.http.registry определена точка расширения org.eclipse.equinox.http.registry.servlets с помощью которой можно регистрировать сервлеты. Каждый сервлет описывается как минимум своим псевдонимом (фактически - путем, по которому он будет доступен) и классом, содержащим его реализацию. Пример описания точки расширения может быть таким:

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

<?eclipse version="3.4"?>

<plugin>

    <extension point="org.eclipse.equinox.http.registry.servlets">

        <servlet alias="/welcome" class="name.samolisov.servlet.demo.servlet.WelcomeServlet" />

    </extension>

</plugin>

 


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

package name.samolisov.servlet.demo;



import java.util.Dictionary;

import java.util.Hashtable;



import org.eclipse.equinox.http.jetty.JettyConfigurator;

import org.osgi.framework.BundleActivator;

import org.osgi.framework.BundleContext;



public class Activator implements BundleActivator {



    private static final String HTTP_PORT_KEY = "http.port";



    private static final int HTTP_PORT = 8080;



    private static final String SERVER_NAME = "demojetty";



    @Override

    public void start(BundleContext context) throws Exception {

        Dictionary<String, Object> properties = new Hashtable<String, Object>();

        properties.put(HTTP_PORT_KEY, HTTP_PORT);

        JettyConfigurator.startServer(SERVER_NAME, properties);

        System.out.println("Server " + SERVER_NAME + " has been started");

    }



    @Override

    public void stop(BundleContext context) throws Exception {

        JettyConfigurator.stopServer(SERVER_NAME);

        System.out.println("Server " + SERVER_NAME + " has been stoped");

    }

}

 


Здесь в методе start создается конфигурация запуска: устанавливается порт, на котором будет работать сервер и, соответственно, происходит непосредственно его старт. В методе stop сервер останавливается.

Чтобы данный код успешно компилировался, необходимо в активаторе бандла указать следующие импортируемые пакеты:


  • javax.servlet

  • javax.servlet.http

  • org.eclipse.equinox.http.jetty

  • org.osgi.framework



Теперь необходимо сказать несколько слов о запуске бандла. В конфигурацию запуска обязательно нужно включить бандлы, содержащие импортируемые пакеты, бандлы, предоставляющие Jetty и реестр точек расширения. При этом, чтобы точка расширения была обнаружена, необходимо стартовать бандлы org.eclipse.equinox.registry и org.eclipse.equinox.http.registry, а чтобы зарегистрировался демонстрационный сервлет - разрабатываемый бандл. Итого, список бандлов, необходимых для запуска следующий:


  • name.samolisov.servlet.demo (Auto-Start: true)

  • javax.servlet

  • org.apache.commons.logging

  • org.eclipse.core.runtime.compatibility.registry

  • org.eclipse.equinox.common

  • org.eclipse.equinox.http.jetty

  • org.eclipse.equinox.http.registry (Auto-Start: true)

  • org.eclipse.equinox.http.servlet

  • org.eclipse.equinox.registry (Auto-Start: true)

  • org.eclipse.osgi

  • org.eclipse.osgi.services

  • org.mortbay.jetty.server

  • org.mortbay.jetty.util



Использование динамического ServiceTracker'а


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

Прежде всего следует создать класс HttpServiceConnector, являющийся наследником класса ServiceTracker:

package name.samolisov.servlet.demo2;



import javax.servlet.http.HttpServlet;



import org.osgi.framework.BundleContext;

import org.osgi.framework.ServiceReference;

import org.osgi.service.http.HttpService;

import org.osgi.util.tracker.ServiceTracker;

import org.osgi.util.tracker.ServiceTrackerCustomizer;



public class HttpServiceConnector extends ServiceTracker implements ServiceTrackerCustomizer

{

    private String path;



    private HttpServlet servlet;



    public HttpServiceConnector(BundleContext context, String path, HttpServlet servlet)

    {

        super(context, HttpService.class.getName(), null);



        this.path = path;

        this.servlet = servlet;



        open();

    }



    @Override

    public Object addingService(ServiceReference reference)

    {

        HttpService httpService = (HttpService) super.addingService(reference);



        if (httpService == null)

          return null;



        try

        {

          System.out.println("Registering servlet at " + path);

          httpService.registerServlet(path, servlet, null, null);

        }

        catch (Exception e)

        {

          e.printStackTrace();

        }



        return httpService;

    }



    @Override

    public void removedService(ServiceReference reference, Object service)

    {

        HttpService httpService = (HttpService) service;



        System.out.println("Unregistering " + path);

        httpService.unregister(path);



        super.removedService(reference, service);

    }

}

 


В конструктор данного класса передаются следующие параметры: context - контекст бандла, в котором происходит регистрация сервлета, path - путь к сервлету или другими словами - его алиас и servlet - непосредственно экземпляр регистрируемого сервлета.

Метод addingService вызывается внутри реестра сервисов OSGi после регистрации HttpService-сервиса. Именно в данном методе осуществляется регистрация сервлета, при этом для удобства отладки выдается сообщение в OSGi-консоль.

Метод removedService вызывается перед удалением сервиса HttpService из реестра. Код данного метода так же тривиален - удаляется сервлет и на консоль выдается сообщение об этом событии. Подробнее об использовании динамических сервис-трекеров можно почитать в статье.

Создание экземпляра данного трекера и управление его жизненным циклом осуществляется в активаторе бандла. Там же осуществляется запуск и останов Jetty. Код метода start следующий:

    /*

     * (non-Javadoc)

     * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)

     */


    public void start(BundleContext context) throws Exception

    {

        httpServiceConnector = new HttpServiceConnector(context, SERVLET_PATH, new WelcomeServlet());



        Dictionary<String, Object> properties = new Hashtable<String, Object>();

        properties.put(HTTP_PORT_KEY, HTTP_PORT);

        JettyConfigurator.startServer(SERVER_NAME, properties);

    }


Так как в конструкторе класса HttpServiceTracker осуществляется вызов метода open(), делать данный вызов в активаторе ненужно.

В методе stop происходит закрытие сервис-трекера и останов сервера Jetty:

    /*

     * (non-Javadoc)

     * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)

     */


    public void stop(BundleContext context) throws Exception

    {

        httpServiceConnector.close();



        JettyConfigurator.stopServer(SERVER_NAME);

        System.out.println("Server " + SERVER_NAME + " has been stoped");

    }


Чтобы код активатора и сервис-трекера компилировался, необходимо в манифесте бандла определить следующие импортируемые пакеты:


  • javax.servlet

  • javax.servlet.http

  • org.eclipse.equinox.http.jetty

  • org.osgi.framework

  • org.osgi.service.http

  • org.osgi.util.tracker



Запуск в данном случае требует меньших усилий, нежели в предыдущем. Необходимо лишь добавить в конфигурацию бандлы:


  • name.samolisov.servlet.demo2 (Auto-Start: true)

  • javax.servlet

  • org.eclipse.equinox.http.jetty

  • org.eclipse.equinox.http.servlet

  • org.eclipse.osgi

  • org.eclipse.osgi.services

  • org.mortbay.jetty.server

  • org.mortbay.jetty.util



В результате после запуска можно наблюдать такую веб-страницу:



Итак, рассмотрены два способа регистрации сервлетов в OSGi-окружении. В случае, если используется сервлет-контейнер отличный от Jetty, можно воспользоваться Equinox Servlet Bridge.

Статья написана по мотивам Peter Friese OSGi & Servlets: A Happy Marriage и Radoslaw H. Urbas Running servlets inside Equinox/Eclipse.

Как всегда буду рад ответить на ваши вопросы.

Понравилось сообщение - подпишитесь на блог или читайте меня в twitter

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

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

Как всегда, очень интересный пост. Несмотря на то, что в ближайшем будущем не буду использовать это в работе, прочел с большим удовольствием. Спасибо!

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

Несколько запоздалое спасибо за ваш комментарий. Значит буду развивать тему еще :)

Евгений Конурбаев комментирует...

Павел, по своему опыту использования OSGi с Jetty что можете сказать, годится ли эта связка для высоконагруженных Enterprise-решений?
Какие плюсы, минусы выявили для себя?
Мы сейчас думаем взять за основу будущего решения приложение, которое сейчас разработано в этой связке, хочется понять, насколько это вообще подходящие технологии.

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

Здравствуйте. Я данную связку использовал только для экспериментов, в продакшне OSGi запускали на Tomcat: http://samolisov.blogspot.ru/2010/05/equinox-equinox-servletbridge.html Работало стабильно, проблем в этой части не было.

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

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