Как известно, одной из основных областей применения языка и платформы 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();
}
}
}
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>
<?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");
}
}
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);
}
}
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);
}
* (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");
}
* (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 комментария:
Как всегда, очень интересный пост. Несмотря на то, что в ближайшем будущем не буду использовать это в работе, прочел с большим удовольствием. Спасибо!
Несколько запоздалое спасибо за ваш комментарий. Значит буду развивать тему еще :)
Павел, по своему опыту использования OSGi с Jetty что можете сказать, годится ли эта связка для высоконагруженных Enterprise-решений?
Какие плюсы, минусы выявили для себя?
Мы сейчас думаем взять за основу будущего решения приложение, которое сейчас разработано в этой связке, хочется понять, насколько это вообще подходящие технологии.
Здравствуйте. Я данную связку использовал только для экспериментов, в продакшне OSGi запускали на Tomcat: http://samolisov.blogspot.ru/2010/05/equinox-equinox-servletbridge.html Работало стабильно, проблем в этой части не было.
Отправить комментарий
Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!