Советы по программированию Web-сервисов: Изучение шаблонов проектирования Web-сервисов, Часть 3

Добавлено : 29 Mar 2009, 09:50

Содержание

Шаблон Router
Реализация маршрутизатора Web-сервисов
Заключение
Ресурсы
Об авторах

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

Шаблон Router

Шаблон Router строится вокруг простой концепции маршрутизации запросов к отдельным частям бизнес-логики на базе некоторого определенного критерия. В программировании этот шаблон лучше всего представлен такими конструкциями, как switch-case и if-then-else. Рассмотрим Рисунок 1.

Рисунок 1. Шаблон Router

Шаблон Router

Router (маршрутизатор), в сущности, является компонентом, прозрачным для клиента. Он отвечает за получение запроса и выяснение, куда его следует передать для обработки. Заданный набор критериев определяет место назначения сообщения. Критерии маршрутизации могут быть статическими или динамическими. Статическая маршрутизация подразумевает то, что правила маршрутизации остаются неизменными. Это означает, что одному и тому же бизнес-компоненту отправляется одно и тоже сообщение, независимо от текущих состояний. Динамическая маршрутизация подразумевает, что использование некоторого набора текущих состояний влияет на маршрут сообщения. Имеется в виду, что при многократной отправке, одно и тоже сообщение может (или не может) быть отправлено для обработки одному и тому же бизнес-компоненту.

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

Если клиент отправляет наборы сообщений или вызывает несколько операций по отношению к Web-сервису, вы не можете гарантировать, что реализация, построенная на использовании маршрутизатора, использует отдельный бизнес-компонент для обработки каждого из этих сообщений или запросов. Кроме этого, вы не можете подразумевать, что маршрутизированные компоненты имеют представление о том, что клиент обращается к ним не напрямую или что они направлены на коллекцию бизнес-компонентов. Самое простое из того, что можно сделать в данной ситуации – не позволить Web-сервисам поддерживать какого-либо рода внутреннее состояние, однако это не всегда возможно или же нежелательно. Более сложным решением является разработка некоего механизма позволения бизнес-компонентам хранить состояние сессии внешним образом. Для этого можно использовать некую форму общедоступного сервиса сессии, доступ к которому могут получить все компоненты бизнеса. Для простых Web-приложений, где все бизнес-компоненты инкапсулированы в отдельное приложение, выполнямое на отдельной машине это осуществить легко. Однако, использование распределенных бизнес-компонентов на множестве компьютеров может вызвать проблемы. Некоторые серверы приложений обеспечивают использование распределенных сервисов сессии, однако вам необходимо заранее планировать и моделировать ваши приложения в соответствии с использованием такого рода сервисов.

Рисунок 2. Шаблон Router с общим контекстом сессии

Шаблон Router с общим контекстом сессии

На Рисунке 2 представлена реализация HTTP-сессий в большинстве серверов Web-приложений. Слушатель запросов получает HTTP-сообщения и предает эти запросы определенным бизнес-компонентам (Java-сервлету, JSP-странице, и т.д.). При этом обеспечивается общий контекст сессии, который хранится независимо от компонента, к которому направлен запрос.

Еще одним важным аспектом шаблона Router является то, что размещение бизнес-компонентов, к которым осуществляется маршрутизация, не зависит от размещения самого маршрутизатора. Другими словами, бизнес-компоненты являются полностью независимыми частями приложения, которые можно довольно легко разместить:

Рассмотрим Рисунок 3.

Рисунок 3. Шаблон Router, использующий промежуточные запросы

Шаблон Router, использующий промежуточные запросы

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

Реализация маршрутизатора Web-сервисов

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

В отдельном Web-приложении размещаются различные компоненты приложения, однако они используют промежуточные очереди Java Messaging Service (JMS). Поддержка сессии в данном примере не доступна, так как для иллюстрации основных принципов данной модели она просто не нужна.

Для рассмотрения примера вам потребуются установленный J2EE Web Application Server и поставщик JMS. При создании кода примера мы использовали поставщик JMS с открытым исходным кодом OpenJMS, доступный на сайте Sourceforge.net (См. Ресурсы по теме).

После настроек операционного окружения можно перейти к написанию кода. Как и предполагалось (см. Листинг 1) код примера не является сложным. Начнем с двух бизнес-компоненов, отвечающих за обработку сообщений запроса. Каждый из них реализован как HTTP-сервлет, инициализирующийся при загрузке сервера и реализующий стандартный интерфейс JMS MessageListener. Каждый из этих сервлетов прослушивает определенную очередь JMS на предмет появления сообщений запроса. При получении запроса сервлет выполняет назначенное ему действие, помещая сообщения ответа в отдельную общую очередь отправляемых ответов. Разница в коде обоих слушателей заключается в том, что один из них преобразует строки входных параметров в верхний регистр символов, а другой – в нижний.

Листинг 1. UppercaseListenerServlet.java и LowercaseListenerServlet.java
package com.ibm.developerworks.wspattern.three;

import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueReceiver;
import javax.jms.QueueSession;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

public class UppercaseListenerServlet
 
extends HttpServlet
 
implements MessageListener, Servlet {

 
private Context context;
 
private QueueConnection connection;
 
private QueueSession session;
 
private Queue queue;
 
private QueueReceiver receiver;

 
public void init()
   
throws ServletException {

   
super.init();

   
try {
     
context = JNDIHelper.getInitialContext();
      connection = JNDIHelper.getQueueConnection
(context);
      session = JNDIHelper.getQueueSession
(connection);
      queue = JNDIHelper.getQueue
(context, "upperqueue");
      receiver = JNDIHelper.getQueueReceiver
(session, queue);
      receiver.setMessageListener
(this);
      System.out.println
("UppercaseListenerServlet is listening");
   
} catch (Exception e) {
     
System.out.println("UppercaseListenerServlet init Error");
      e.printStackTrace
(System.out);
   
}

  }
 
 
public void onMessage(Message message) {
   
try {
     
if (message instanceof TextMessage) {
       
TextMessage txtMessage = (TextMessage) message;
        String txt = txtMessage.getText
().toUpperCase();
        TextMessage response = session.createTextMessage
();
        response.setJMSCorrelationID
(txtMessage.getJMSCorrelationID());
        response.setText
(txt);
        Queue queue = JNDIHelper.getQueue
(context, "queue6");
        JNDIHelper.getQueueSender
(session, queue).send(response);
     
}
    }
catch (Exception e) {
     
System.out.println("UppercaseListenerServlet onMessage Error");
      e.printStackTrace
(System.out);
   
}
  }

}

package com.ibm.developerworks.wspattern.three;

import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueReceiver;
import javax.jms.QueueSession;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

public class LowercaseListenerServlet
 
extends HttpServlet
 
implements MessageListener, Servlet {

 
private Context context;
 
private QueueConnection connection;
 
private QueueSession session;
 
private Queue queue;
 
private QueueReceiver receiver;

 
public void init()
   
throws ServletException {

   
super.init();

   
try {
     
context = JNDIHelper.getInitialContext();
      connection = JNDIHelper.getQueueConnection
(context);
      session = JNDIHelper.getQueueSession
(connection);
      queue = JNDIHelper.getQueue
(context, "lowerqueue");
      receiver = JNDIHelper.getQueueReceiver
(session, queue);
      receiver.setMessageListener
(this);
      System.out.println
("LowercaseListenerServlet is listening");
   
} catch (Exception e) {
     
System.out.println("LowercaseListenerServlet init Error");
      e.printStackTrace
(System.out);
   
}

  }
 
 
public void onMessage(Message message) {
   
try {
     
if (message instanceof TextMessage) {
       
TextMessage txtMessage = (TextMessage) message;
        String txt = txtMessage.getText
().toLowerCase();
        TextMessage response = session.createTextMessage
();
        response.setJMSCorrelationID
(txtMessage.getJMSCorrelationID());
        response.setText
(txt);
        Queue queue = JNDIHelper.getQueue
(context, "responsequeue");
        JNDIHelper.getQueueSender
(session, queue).send(response);
     
}
    }
catch (Exception e) {
     
System.out.println("LowercaseListenerServlet onMessage Error");
      e.printStackTrace
(System.out);
   
}
  }

Маршрутизатор, код которого кратко представлен в Листинге 2, раскрывает операцию с именем process, которая принимает на входе два параметра. Первый параметр определяет операцию, которая производится по запросу клиента. Значениями параметра могут быть upper или lower. Второй параметр является строкой, которая преобразуется, соответственно, в верхний или нижний регистр символов. При получении запроса на обработку маршрутизатор помещает строку в очереди upperqueue или lowerqueue, в зависимости от выбранного действия. Соответствующий слушатель получает сообщение и обрабатывает запрос, помещая обработанный текст в очередь responsequeue. Маршрутизатор извлекает сообщение из очереди responsequeue и возвращает преобразованный текст обратно клиенту. Весь этот процесс схематично отображен на Рисунке 4.

Рисунок 4. Пример приложения RouterService

Пример приложения RouterService
Листинг 2. RouterService.java
package com.ibm.developerworks.wspattern.three;

import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueReceiver;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.TextMessage;
import javax.naming.Context;

public class RouterService {

 
public String process(String action, String target) {
   
String response = null;
   
try {
     
Context context = JNDIHelper.getInitialContext();
      QueueConnection connection = JNDIHelper.getQueueConnection
(context);
      QueueSession session = JNDIHelper.getQueueSession
(connection);
      String queueName =
("upper".equalsIgnoreCase(action)) ? "upperqueue"
         
: "lowerqueue";
      Queue queue = JNDIHelper.getQueue
(context, queueName);
      QueueSender sender = JNDIHelper.getQueueSender
(session, queue);

      TextMessage message = session.createTextMessage
();
      String corrID = Long.toString
(System.currentTimeMillis());
      message.setJMSCorrelationID
(corrID);
      message.setText
(target);

      sender.send
(message);

      Queue rqueue = JNDIHelper.getQueue
(context, "responsequeue");
      String selector =
"JMSCorrelationID = '" + corrID + "'";
      QueueReceiver rec = JNDIHelper.getQueueReceiver
(session, rqueue,
          selector
);
      TextMessage resp =
(TextMessage) rec.receive(10 * 1000);
      response = resp.getText
();
   
} catch (Exception e) {
     
response = e.getMessage();
   
}
   
return response;
 
}

}

Сервлеты-слушатели и класс RouterService используют общий класс-утилиту для инкапсуляции работы с JMS API. Данный класс приведен в Листинге 3.

Листинг 3. JNDIHelper.java
package com.ibm.developerworks.wspattern.three;

import java.util.Hashtable;

import javax.jms.JMSException;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueReceiver;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDIHelper {

 
private static Context context;

 
public static Context getInitialContext() throws NamingException {
   
if (context == null) {
     
Hashtable properties = new Hashtable();
      properties.put
(Context.INITIAL_CONTEXT_FACTORY,
         
"org.exolab.jms.jndi.InitialContextFactory");
      properties.put
(Context.PROVIDER_URL, "rmi://localhost:1099");
      context =
new InitialContext(properties);
   
}
   
return context;
 
}

 
public static QueueConnection getQueueConnection(Context context)
     
throws NamingException, JMSException {
   
QueueConnectionFactory factory = (QueueConnectionFactory) context
        .lookup
("JmsQueueConnectionFactory");
    QueueConnection connection = factory.createQueueConnection
();
    connection.start
();
   
return connection;
 
}

 
public static QueueSession getQueueSession(QueueConnection connection)
     
throws JMSException {
   
QueueSession session = connection.createQueueSession(false,
        Session.AUTO_ACKNOWLEDGE
);
   
return session;
 
}

 
public static Queue getQueue(Context context, String name)
     
throws NamingException {
   
Queue queue = (Queue) context.lookup(name);
   
return queue;
 
}

 
public static QueueSender getQueueSender(QueueSession session, Queue queue)
     
throws JMSException {
   
QueueSender sender = session.createSender(queue);
   
return sender;
 
}

 
public static QueueReceiver getQueueReceiver(QueueSession session,
      Queue queue
) throws JMSException {
   
QueueReceiver receiver = session.createReceiver(queue);
   
return receiver;
 
}

 
public static QueueReceiver getQueueReceiver(QueueSession session,
      Queue queue, String selector
) throws JMSException {
   
QueueReceiver receiver = session.createReceiver(queue, selector);
   
return receiver;
 
}

}

После создания RouterService.java финальным шагом является размещение его как Web-сервиса. Способ размещения зависит от используемого вами сервера приложения и инструментов разработки. В данном примере мы использовали IBMreg/>WebSpherereg/> Application Server Version 5.1 и программу WebSphere Studio Application Developer. Независимо от используемого ПО класс RouterService становится основным классом реализации Web-сервиса. Экземпляр маршрутизатора получает все запросы, проходящие через интерфейс сервиса (См. Листинг 4).

Листинг 4. WSDL-описание класса RouterService
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions 
  targetNamespace="http://three.wspattern.developerworks.ibm.com" 
  xmlns:impl="http://three.wspattern.developerworks.ibm.com" 
  xmlns:intf="http://three.wspattern.developerworks.ibm.com" 
  xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
  xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <wsdl:types>
  <schema 
    targetNamespace="http://three.wspattern.developerworks.ibm.com" 
    xmlns="http://www.w3.org/2001/XMLSchema" 
    xmlns:impl="http://three.wspattern.developerworks.ibm.com" 
    xmlns:intf="http://three.wspattern.developerworks.ibm.com" 
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <element name="action" nillable="true" type="xsd:string"/>
   <element name="target" nillable="true" type="xsd:string"/>
   <element name="processReturn" nillable="true" type="xsd:string"/>
  </schema>
 </wsdl:types>

   <wsdl:message name="processRequest">
      <wsdl:part name="action" type="xsd:string"/>
      <wsdl:part name="target" type="xsd:string"/>
   </wsdl:message>

   <wsdl:message name="processResponse">
      <wsdl:part name="processReturn" type="xsd:string"/>
   </wsdl:message>

   <wsdl:portType name="RouterService">
      <wsdl:operation name="process" parameterOrder="action target">
         <wsdl:input message="intf:processRequest" name="processRequest"/>
         <wsdl:output message="intf:processResponse" name="processResponse"/>
      </wsdl:operation>
   </wsdl:portType>

   <wsdl:binding name=
   "RouterServiceSoapBinding" type="intf:RouterService">
      <wsdlsoap:binding style=
      "rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
      <wsdl:operation name="process">
         <wsdlsoap:operation soapAction=""/>
         <wsdl:input name="processRequest">
            <wsdlsoap:body 
              namespace="http://three.wspattern.developerworks.ibm.com" 
              use="literal"/>
         </wsdl:input>
         <wsdl:output name="processResponse">
            <wsdlsoap:body 
              namespace="http://three.wspattern.developerworks.ibm.com" 
              use="literal"/>
         </wsdl:output>
      </wsdl:operation>
   </wsdl:binding>

   <wsdl:service name="RouterServiceService">
      <wsdl:port binding="intf:RouterServiceSoapBinding" name="RouterService">
         <wsdlsoap:address 
           location="http://localhost:9080/WSPattern3/services/RouterService"/>
      </wsdl:port>
   </wsdl:service>
</wsdl:definitions>

После реализации сервиса необходимо разместить ваше Web-приложение, после чего запустить вначале поставщик сервиса Java Naming and Directory Interface (JNDI), а затем сервер приложения. Обратите внимание, что последовательность запуска имеет значение, в противном случае пример работать не будет. Теперь можно протестировать сервис (См. Листинг 5).

Листинг 5. Пример обмена сообщениями при помощи класса RouterService
<i>Request</i>
<SOAP-ENV:Envelope 
  xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 
  xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <SOAP-ENV:Body>
    <m:process xmlns:m="http://three.wspattern.developerworks.ibm.com">
      <action>upper</action>
      <target>this is a test</target>
    </m:process>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

<i>Response</i>
<soapenv:Envelope 
  xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
  xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soapenv:Header/>
  <soapenv:Body>
    <p317:processResponse xmlns:p317=
    "http://three.wspattern.developerworks.ibm.com">
      <processReturn>THIS IS A TEST</processReturn>
    </p317:processResponse>
  </soapenv:Body>
</soapenv:Envelope>

<i>Request</i>
<SOAP-ENV:Envelope 
  xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 
  xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <SOAP-ENV:Body>
    <m:process xmlns:m="http://three.wspattern.developerworks.ibm.com">
      <action>lower</action>
      <target>THIS IS A TEST</target>
    </m:process>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

<i>Response</i>
<soapenv:Envelope 
  xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
  xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soapenv:Header/>
  <soapenv:Body>
    <p317:processResponse xmlns:p317=
    "http://three.wspattern.developerworks.ibm.com">
      <processReturn>this is a test</processReturn>
    </p317:processResponse>
  </soapenv:Body>
</soapenv:Envelope>

Заключение

В заключение рассмотрим несколько основных моментов представленного в данной статье примера. Во-первых, для определения вида выполняемых действий в примере используется один из входных параметров. В качестве альтернативы для этой цели можно использовать заголовок действия SOAP спецификации WS-Addressing.

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

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

Ресурсы

Ссылки по теме

Part 1: Asynchronous Web services operations using JMS
Part 2: Encapsulate business logic with a command facade pattern
Part 4: Understand and implement the message bus pattern

Об авторах

James Snell является членом команды разработчиков IBM Emerging Technologies Toolkit. Последние несколько лет он занимался исследованиями технологий и стандартов Web-сервисов. На данный момент занимается поддержкой раздела weblog на сайте developerWorks.