Советы по программированию Web-сервисов: Изучение шаблонов проектирования Web-сервисов, Часть 1
Данный документ поможет вам разобраться в том, как применять четко определенные и проверенные стратегии проектирования Web-приложений к области Web-сервисов. В первой части советов рассказывается, как реализовать асинхронные операции запросов, используя очереди Java Messaging Service (JMS).
James M. Snell (jasnell@us.ibm.com)
Инженер-программист, IBM
19 Oct 2004
Содержание
1 Шаблон асинхронного запроса
2 Проектирование интерфейса сервиса
3 Реализация сервиса
4 Выводы
5 Ресурсы
6 Об авторах
Если представить себе реализацию 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, описывающий интерфейс сервиса.
<?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
На стороне сервера необходимо реализовать два компонента – обработчик запросов и реализация 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-соединения и сессии.
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 к данному документу прилагается исходный код, который может быть загружен по ссылке в конце документа.
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 нет смысла.
В заключении, можно легко представить расширение области применения данного примера для выполнения запрашивающими сторонами детальных запросов статуса в длительных операциях или даже для отмены уже выполняемых операций. Иллюстрация таких возможностей не является задачей данной статьи, вам предлагается исследовать их самостоятельно.
Ресурсы
- Другие документы из серии "Практическое обучение использованию шаблонов Web-сервисов":
- Статья, рассказывающая о шаблоне асинхронных запросов "Asynchronous Queries in J2EE", где представлены теоретические основы, необходимые для обсуждения шаблонов асинхронных запросов. публикована на сайте Java Ranch, автор Kyle Brown.
- Ссылка на загрузку программы WebSphere Studio Application Developer, используемой для написания данного примера приложения.
- Ссылка на информацию о программе с открытым исходным кодом OpenJMS provider.
- Обсуждения вопросов Emerging Technology, включая Web-сервисы, в блоге разработчиков developerWorks blog.
- Книжный магазин Developer Bookstore, содержащий сотни книг о Web-сервисах.
- Множество информативных статей и руководств по разработке приложений Web-сервисов на сайте Web Services and SOA zone.
Ссылки по теме
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.