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

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

James M. Snell (jasnell@us.ibm.com)
Инженер-программист, IBM
19 Oct 2004

Содержание

Шаблон асинхронного запроса
Проектирование интерфейса сервиса
Реализация сервиса
Выводы
Ресурсы
Об авторах

Если представить себе реализацию Web-сервисов с SOAP, скорее всего, вам придут в голову простые синхронные операции типа запрос-ответ. На самом деле Service-Oriented Architecture (SOA) может охватывать гораздо больший диапазон моделей обмена сообщениями и стратегий проектирования. В данном документе мы сфокусируемся на применении основных шаблонов проектирования Web-приложений с использованием Web-сервисов. Представляемые шаблоны не являются новыми, они используются в традиционных Web-приложениях уже много лет, однако многие разработчики даже не подозревают о том, как реализовать такие методики в области Web-сервисов или же не до конца понимают, как их применять. Автор ставит перед собой задачу представления набора простых, открытых альтернатив проектирования модели запрос-ответ. Для изучения примера вам необходимо иметь хотя бы основные представления о реализации Web-сервисов в среде J2EE.

Шаблон асинхронного запроса

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

Рисунок 1. Шаблон асинхронного запроса

Шаблон асинхронного запроса

Поток в данной модели весьма прост:

О применении данного шаблона в приложениях сервлетов J2EE для Java Ranch (Web-сайта Java-разработчиков) написана превосходная статья, автором которой является Kyle Brown (См. Ресурсы по теме). В данной статье, кроме всего прочего, обсуждаются мотивации и основные вопросы проектирования касательно базовой реализации данного шаблона. Не удивительно, что реализация не сильно изменилась в шаблоне, применимом для Web-сервисов.

Проектирование интерфейса сервиса

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

Для реализации данного сервиса доступны две операции: submitRequest и checkResponse. Действие этих операций не требует объяснения. В Листинге 1 представлен WSDL, описывающий интерфейс сервиса.

Листинг 1. AsyncService.wsdl

<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions 
  targetNamespace="http://one.wspattern.developerworks.ibm.com" 
  xmlns:impl="http://one.wspattern.developerworks.ibm.com" 
  xmlns:intf="http://one.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://one.wspattern.developerworks.ibm.com" 
    xmlns="http://www.w3.org/2001/XMLSchema" 
    xmlns:impl="http://one.wspattern.developerworks.ibm.com" 
    xmlns:intf="http://one.wspattern.developerworks.ibm.com" 
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <complexType name="ResponseCheck">
    <sequence>
     <element name="correlationID" nillable="true" type="xsd:string"/>
    </sequence>
   </complexType>
   <element name="ResponseCheck" nillable="true" type="impl:ResponseCheck"/>
   <complexType name="Response">
    <sequence>
     <element name="type" type="xsd:int"/>
     <element name="correlationID" nillable="true" type="xsd:string"/>
     <element name="refresh" type="xsd:int"/>
     <element name="a" nillable="true" type="xsd:string"/>
     <element name="b" nillable="true" type="xsd:string"/>
     <element name="c" nillable="true" type="xsd:string"/>
    </sequence>
   </complexType>
   <element name="Response" nillable="true" type="impl:Response"/>
   <complexType name="Request">
    <sequence>
     <element name="a" nillable="true" type="xsd:string"/>
     <element name="b" nillable="true" type="xsd:string"/>
     <element name="c" nillable="true" type="xsd:string"/>
    </sequence>
   </complexType>
   <element name="Request" nillable="true" type="impl:Request"/>
  </schema>
 </wsdl:types>

   <wsdl:message name="submitRequestRequest">
      <wsdl:part name="request" type="intf:Request"/>
   </wsdl:message>

   <wsdl:message name="checkResponseResponse">
      <wsdl:part name="checkResponseReturn" type="intf:Response"/>
   </wsdl:message>

   <wsdl:message name="checkResponseRequest">
      <wsdl:part name="check" type="intf:ResponseCheck"/>
   </wsdl:message>

   <wsdl:message name="submitRequestResponse">
      <wsdl:part name="submitRequestReturn" type="intf:Response"/>
   </wsdl:message>

   <wsdl:portType name="AsyncService">
      <wsdl:operation name="checkResponse" parameterOrder="check">
         <wsdl:input 
           message="intf:checkResponseRequest" 
           name="checkResponseRequest"/>
         <wsdl:output 
           message="intf:checkResponseResponse" 
           name="checkResponseResponse"/>
      </wsdl:operation>
      <wsdl:operation name="submitRequest" parameterOrder="request">
         <wsdl:input 
           message="intf:submitRequestRequest" 
           name="submitRequestRequest"/>
         <wsdl:output 
           message="intf:submitRequestResponse" 
           name="submitRequestResponse"/>
      </wsdl:operation>
   </wsdl:portType>

   <wsdl:binding name="AsyncServiceSoapBinding" type="intf:AsyncService">
      <wsdlsoap:binding 
        style="rpc" 
        transport="http://schemas.xmlsoap.org/soap/http"/>
      <wsdl:operation name="checkResponse">
         <wsdlsoap:operation soapAction=""/>
         <wsdl:input name="checkResponseRequest">
            <wsdlsoap:body 
              namespace="http://one.wspattern.developerworks.ibm.com" 
              use="literal"/>
         </wsdl:input>
         <wsdl:output name="checkResponseResponse">
            <wsdlsoap:body 
              namespace="http://one.wspattern.developerworks.ibm.com" 
              use="literal"/>
         </wsdl:output>
      </wsdl:operation>
      <wsdl:operation name="submitRequest">
         <wsdlsoap:operation soapAction=""/>
         <wsdl:input name="submitRequestRequest">
            <wsdlsoap:body 
              namespace="http://one.wspattern.developerworks.ibm.com" 
              use="literal"/>
         </wsdl:input>
         <wsdl:output name="submitRequestResponse">
            <wsdlsoap:body 
              namespace="http://one.wspattern.developerworks.ibm.com" 
              use="literal"/>
         </wsdl:output>
      </wsdl:operation>
   </wsdl:binding>

   <wsdl:service name="AsyncServiceService">
      <wsdl:port binding="intf:AsyncServiceSoapBinding" name="AsyncService">
         <wsdlsoap:address 
           location="http://localhost:9080/WSPattern1/services/AsyncService"/>
      </wsdl:port>
   </wsdl:service>

</wsdl:definitions> 

Здесь следует отметить несколько моментов:

В Листинге 2 представлен типичный обмен сообщениями для данного сервиса.

Листинг 2. Обмен сообщениями AsyncService

<i>Initial 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:submitRequest 
      xmlns:m="http://one.wspattern.developerworks.ibm.com">
      <request>
        <a>String</a>
        <b>String</b>
        <c>String</c>
      </request>
    </m:submitRequest>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>


<i>submitRequest Response
Ответ submitRequest </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>
    <p155:submitRequestResponse 
      xmlns:p155="http://one.wspattern.developerworks.ibm.com">
      <submitRequestReturn>
        <type>0</type>
        <correlationID>1097517621904</correlationID>
        <refresh>10000</refresh>
        <a xsi:nil="true"/>
        <b xsi:nil="true"/>
        <c xsi:nil="true"/>
      </submitRequestReturn>
    </p155:submitRequestResponse>
  </soapenv:Body>
</soapenv:Envelope>


<i>Initial checkResponse attempt, no response available, submitted after 10000 miliseconds
Первичная попытка checkResponse, ответ не поступил, выполняется после задержки в 10000 мс</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>
    <p155:checkResponseResponse 
      xmlns:p155="http://one.wspattern.developerworks.ibm.com">
      <checkResponseReturn>
        <type>0</type>
        <correlationID>1097517621904</correlationID>
        <refresh>10000</refresh>
        <a xsi:nil="true"/>
        <b xsi:nil="true"/>
        <c xsi:nil="true"/>
      </checkResponseReturn>
    </p155:checkResponseResponse>
  </soapenv:Body>
</soapenv:Envelope>


<i>Second checkResponse attempt, submitted after 10000 miliseconds
Вторая попытка checkResponse, выполняется после задержки в 10000 мс</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>
    <p155:checkResponseResponse 
      xmlns:p155="http://one.wspattern.developerworks.ibm.com">
      <checkResponseReturn>
        <type>1</type>
        <correlationID xsi:nil="true"/>
        <refresh>0</refresh>
        <a>STRING</a>
        <b>STRING</b>
        <c>STRING</c>
      </checkResponseReturn>
    </p155:checkResponseResponse>
  </soapenv:Body>
</soapenv:Envelope>

Реализация сервиса

Реализацией сервиса асинхронного шаблона запроса является применение Java Messaging Service. Для иллюстрации примера здесь используется OpenJMS – реализация поставщика JMS с открытым исходным кодом (см. Ресурсы по теме) и IBM® WebSphere Application Server V5 (сервер приложений). В примере используется конфигурация OpenJMS, определенная по умолчанию, а также реализация Web-сервиса J2EE, совместимого с JSR-109. Код для данного примера написан с использованием WebSphere Studio Application Developer (Application Developer) V5.1, который можно загрузить по ссылке с сайта developerWorks (см. Ресурсы по теме). Здесь также прилагается EAR-файл (см. Ресурсы по теме) на тот случай, если у вас нет доступа к Application Developer.

На стороне сервера необходимо реализовать два компонента – обработчик запросов и реализация Web-сервиса. Задачей обработчика запросов является снятие запросов из очереди и выполнение 10-секундной задержки процесса по преобразованию нижнего регистра символов в верхний. Задачей реализации сервиса является получение запросов от клиентов Web-сервиса и постановка их в очередь для обработки и для доставки ответов клиентам, следуя операции checkResponse.

В типичном приложении J2EE, согласно спецификации JMS, обработчик запросов должен быть реализован как Message Driven Bean (компонент, управляемый сообщениями). В данном примере используется простой HTTP-сервлет, реализующий JMS-интерфейс MessageListener. Сервлет настроен на инициализацию при загрузке сервера, что позволяет слушателю быть доступным для всех возможных входящих запросов. После постановки запроса в очередь тот доставляется сервлету-слушателю.

Листинг 3. JNDIListenerServlet.java

package com.ibm.developerworks.wspattern.one.helper;

import java.util.Enumeration;
import java.util.Hashtable;

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

public class JNDIListenerServlet extends HttpServlet implements Servlet,
    MessageListener
{

 
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.getConnection
(context);
      session = JNDIHelper.getSession
(connection);
      queue = JNDIHelper.getQueue
(context);
      receiver = JNDIHelper.getQueueReceiver
(session, queue);
      receiver.setMessageListener
(this);
      System.out.println
("Listener servlet is Listening");
   
} catch (Exception e) {
    }
  }

 
public void destroy() {
   
try {
     
connection.close();
   
} catch (Exception e) {
    }
  }

 
public void onMessage(Message message) {
   
try {
     
System.out.println("Processing message "
         
+ message.getJMSCorrelationID());
      Thread.sleep
(10 * 1000); // sleep for ten seconds
     
Queue responseQueue = JNDIHelper.getResponseQueue(context);
      QueueSender sender = JNDIHelper.getQueueSender
(session,
          responseQueue
);
      MapMessage request =
(MapMessage) message;
      MapMessage response = session.createMapMessage
();
      response.setJMSCorrelationID
(request.getJMSCorrelationID());
     
for (Enumeration e = request.getMapNames(); e.hasMoreElements();) {
       
String name = (String) e.nextElement();
       
try {
         
response.setString(name, request.getString(name)
             
.toUpperCase());
       
} catch (Exception ex) {
        }
      }
     
sender.send(response);
   
} catch (Exception e) {
     
System.out.println("==================");
     
try {
       
System.out
            .println
("THERE WAS AN ERROR PROCESSING THE MESSAGE! "
               
+ message.getJMSCorrelationID());
     
} catch (Exception ex) {
      }
     
e.printStackTrace(System.out);
      System.out.println
("==================");
   
}
  }

}

Сервлет JNDIListenerServlet и реализация сервиса используют один простой вспомогательный класс, созданный для этого приложения, который скрывает детали инициализации JMS-соединения и сессии.

Листинг 4. JNDIHelper.java

package com.ibm.developerworks.wspattern.one.helper;

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 getConnection(Context context)
     
throws NamingException, JMSException {
   
QueueConnectionFactory factory = (QueueConnectionFactory) context
        .lookup
("JmsQueueConnectionFactory");
    QueueConnection connection = factory.createQueueConnection
();
    connection.start
();
   
return connection;
 
}

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

 
public static Queue getQueue(Context context) throws NamingException {
   
Queue queue = (Queue) context.lookup("queue1");
   
return queue;
 
}

 
public static Queue getResponseQueue(Context context)
     
throws NamingException {
   
Queue queue = (Queue) context.lookup("queue2");
   
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;
 
}

}

После создания сервлета отредактируйте файл Web-приложения web.xml таким образом, чтобы сервлет инициализировался при загрузке сервера. При инициализации сервлет создает JMS-соединение и регистрирует себя в качестве слушателя в очереди сообщений OpenJMS, заданной по умолчанию.

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

Листинг 5. AsyncService.java

package com.ibm.developerworks.wspattern.one;

import javax.jms.MapMessage;
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.naming.Context;

import com.ibm.developerworks.wspattern.one.helper.JNDIHelper;

public class AsyncService {

 
public Response submitRequest(Request request) {
   
Response response = null;
   
try {
     
Context context = JNDIHelper.getInitialContext();
      QueueConnection connection = JNDIHelper.getConnection
(context);
      QueueSession session = JNDIHelper.getSession
(connection);
      Queue queue = JNDIHelper.getQueue
(context);
      QueueSender sender = JNDIHelper.getQueueSender
(session, queue);

      MapMessage message = session.createMapMessage
();
      String corrID = Long.toString
(System.currentTimeMillis());
      message.setJMSCorrelationID
(corrID);

      message.setString
("one", request.getA());
      message.setString
("two", request.getB());
      message.setString
("three", request.getC());
      sender.send
(message);

      response =
new Response();
      response.setType
(Response.TYPE_REFRESH);
      response.setCorrelationID
(corrID);
      response.setRefresh
(10 * 1000);
     
return response;
   
} catch (Exception e) {
     
response = new Response();
      response.setType
(Response.TYPE_RESPONSE);
      response.setA
(e.getMessage());
   
}
   
return response;
 
}

 
public Response checkResponse(ResponseCheck check) {
   
String corrID = check.getCorrelationID();
    Response response =
null;
   
try {
     
Context context = JNDIHelper.getInitialContext();
      QueueConnection connection = JNDIHelper.getConnection
(context);
      QueueSession session = JNDIHelper.getSession
(connection);
      Queue queue = JNDIHelper.getResponseQueue
(context);
      String selector =
"JMSCorrelationID = '" + corrID + "'";
      QueueReceiver receiver = JNDIHelper.getQueueReceiver
(session,
          queue, selector
);
      Message message = receiver.receiveNoWait
();
     
if (message == null) {
       
response = new Response();
        response.setType
(Response.TYPE_REFRESH);
        response.setRefresh
(10 * 1000);
        response.setCorrelationID
(corrID);
     
} else {
       
MapMessage resp = (MapMessage) message;
        response =
new Response();
        response.setType
(Response.TYPE_RESPONSE);
        response.setA
(resp.getString("one"));
        response.setB
(resp.getString("two"));
        response.setC
(resp.getString("three"));
     
}
    }
catch (Exception e) {
    }
   
return response;
 
}
}

В этом листинге нет ничего необычного. Метод submitRequest подготавливает JMS MapMessage на базе входящих параметров. Это сообщение состоит из трех строковых значений. После этого сообщение отправляется в очередь и подготавливается Refresh Response содержащий ID корреляции, после чего тот возвращается клиенту.

Операция checkResponse принимает ID корреляции из входных параметров и устанавливает соединение с очередью ответа, запрашивая доставку любых сообщений с соответствующим ID корреляции. Если таких сообщений не существует, операция не ждет их, а подготавливает другой Refresh Response с новым периодом интервала обновления и возвращает его обратно запрашивающей стороне. Если сообщение доставлено, операция подготавливает и возвращает соответствующий Request Response.

Для запуска Web-сервиса асинхронного шаблона запроса необходимо разместить Web-сервис и запустить серверы OpenJMS и WebSphere.

Выводы

Основным преимуществом шаблона асинхронных запросов является возможность корреляции запросов и ответов клиентом Web-сервисов и поставщиком сервиса. В представленном здесь примере был рассмотрен простой ID корреляции однократного использования и механизм обновления таймера, присущего этому отдельному примеру приложения. Для этой же цели можно использовать, к примеру, несколько комбинаций WS-* спецификаций. WS-Addressing Endpoint Reference или WS-Transaction Coordination Context также могут содержать ID корреляции и обновлять значения таймера. Использование данного шаблона индивидуально для отдельных приложений, и независимо от того, используете ли вы стандартные элементы заголовка SOAP и различные WS-* спецификации, поведение каждой реализации операции должно быть четко определено и задокументировано.

В реализуемом здесь примере для отправки запросов и получения ответов используется традиционный шаблон обмена сообщениями SOAP типа запрос-ответ. В качестве альтернативы можно использовать шаблон в стиле REST, в которой запросы отправляются сервлету методом HTTP POST, а ответы извлекаются с использованием метода HTTP GET. Каждый из этих подходов является в равной степени допустимым и обладает собственными преимуществами и недостатками. Выбор подхода осуществляется в зависимости от требования вашего приложения. Например, если операция checkResponse будет требовать использования аутентификации WS-Security, то в использование модели взаимодействия в стиле REST нет смысла.

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

Ресурсы

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

Part 2: Encapsulate business logic with a command facade pattern
Part 3: Creating flexible Web service implementations with the Router pattern
Part 4: Understand and implement the message bus pattern

Об авторах

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