Советы и приемы программирования Web-служб: Простые и полезные модели проектирования Web-служб, часть 4

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

http://www-106.ibm.com/developerworks/webservices/library/ws-tip-altdesign4/

James M.Snell (frankb@ca.ibm.com)
Инженер-программист, IBM
14 Dec 2004

Содержание

Модель шины сообщений
Реализация шины сообщений
Заключительное слово
Ресурсы
Об авторах

Модель шины сообщений

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

Рисунок 1. Модель шины сообщений

Рисунок 2. Общие компоненты шины сообщений

Приложение взаимодействует с шиной сообщений через один или несколько каналов. Каналы перемещают сообщение либо в шину сообщений, либо из нее. Существует два способа получения приложением сообщений из шины: push (толкать) или pull (тянуть). При push-сообщениях, называемых также уведомлениями о событии, шина инициирует передачу сообщения к какому-либо ожидающему приложению. При pull-сообщениях приложение инициирует передачу, посылая запрос какой-либо формы шине и получая в ответ одно или несколько сообщений по очереди. Push-модель идеальна для приложений, способных поддерживать постоянное соединение или прослушивающих канал, по которому сообщение может быть передано. Pull-модель идеальна для приложений, соединяющихся с шиной только периодически или не поддерживающих постоянное соединение с шиной.

Рисунок 3. Варианты соединения шины приложение-сообщение

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

Реализация шины сообщений

Существует довольно много способов реализации шины сообщений, так же как и программных средств поставщиков, созданных для облегчения этой реализации. Например, IBM предлагает продукты WebSphere MQ и WebSphere Business Integration Message Broker, основанные на стандартах Java Messaging Service API. Вместе эти продукты обеспечивают надежную, качественную архитектуру шины сообщений, которая отвечает требованиям большинства бизнес-ситуаций. Другим важным моментом является то, что большой объем работы переместился в определение набора стандартов Web-служб, обеспечивающих модель для реализации служб, основанных на событиях и уведомлениях. Семейство спецификаций WS-Notification определяет полную модель для Web-служб, которая может использоваться для реализации служб обмена сообщениями по шине. (См. раздел Ресурсы для получения ссылки на спецификации семейства WS-Notifiation.) В данной статье, однако, я сосредоточусь на объяснении того, как работает фундаментальная модель проектирования. Я создаю свою собственную реализацию, используя JMS-реализацию с открытыми исходными кодами OpenJMS и Java-сервлеты и совсем не переживаю за стандарты или продукты.

Данная реализация состоит из трех JMS-тем (каналы модели публикация-подписка):

Рисунок 4. Пример шины сообщений

Web-служба содержит интерфейс, предоставляющий две операции: send и receive. Операция send в качестве входного параметра принимает объект под названием Case Model, являющийся представителем простого типа зависящих от приложений сообщений, которые обрабатывает шина сообщений. На листинге 1 приведено WSDL-описание Web-службы.

Листинг 1. MessageBusService.wsdl
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions 
  targetNamespace="http://four.wspattern.developerworks.ibm.com" 
  xmlns:impl="http://four.wspattern.developerworks.ibm.com" 
  xmlns:intf="http://four.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://four.wspattern.developerworks.ibm.com" 
    xmlns="http://www.w3.org/2001/XMLSchema" 
    xmlns:impl="http://four.wspattern.developerworks.ibm.com" 
    xmlns:intf="http://four.wspattern.developerworks.ibm.com" 
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <complexType name="CaseModel">
    <sequence>
     <element name="text" nillable="true" type="xsd:string"/>
    </sequence>
   </complexType>
   <element name="CaseModel" nillable="true" type="impl:CaseModel"/>
  </schema>
 </wsdl:types>

   <wsdl:message name="receiveResponse">
      <wsdl:part name="receiveReturn" type="intf:CaseModel"/>
   </wsdl:message>

   <wsdl:message name="sendRequest">
      <wsdl:part name="model" type="intf:CaseModel"/>
   </wsdl:message>

   <wsdl:message name="receiveRequest">
   </wsdl:message>

   <wsdl:message name="sendResponse">
   </wsdl:message>

   <wsdl:portType name="MessageBusService">
      <wsdl:operation name="receive">
         <wsdl:input message="intf:receiveRequest" name="receiveRequest"/>
         <wsdl:output message="intf:receiveResponse" name="receiveResponse"/>
      </wsdl:operation>

      <wsdl:operation name="send" parameterOrder="model">
         <wsdl:input message="intf:sendRequest" name="sendRequest"/>
         <wsdl:output message="intf:sendResponse" name="sendResponse"/>
      </wsdl:operation>
   </wsdl:portType>

   <wsdl:binding name="MessageBusServiceSoapBinding" type="intf:MessageBusService">
      <wsdlsoap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>

      <wsdl:operation name="receive">
         <wsdlsoap:operation soapAction=""/>
         <wsdl:input name="receiveRequest">
            <wsdlsoap:body 
              namespace="http://four.wspattern.developerworks.ibm.com" 
              use="literal"/>
         </wsdl:input>
         <wsdl:output name="receiveResponse">
            <wsdlsoap:body 
              namespace="http://four.wspattern.developerworks.ibm.com" 
              use="literal"/>
         </wsdl:output>
      </wsdl:operation>

      <wsdl:operation name="send">
         <wsdlsoap:operation soapAction=""/>
         <wsdl:input name="sendRequest">
            <wsdlsoap:body 
              namespace="http://four.wspattern.developerworks.ibm.com" 
              use="literal"/>
         </wsdl:input>

         <wsdl:output name="sendResponse">
            <wsdlsoap:body 
              namespace="http://four.wspattern.developerworks.ibm.com" 
              use="literal"/>
         </wsdl:output>

      </wsdl:operation>
   </wsdl:binding>


   <wsdl:service name="MessageBusServiceService">
      <wsdl:port 
          binding="intf:MessageBusServiceSoapBinding" 
          name="MessageBusService">
         <wsdlsoap:address 
            location="http://localhost:9080/WSPattern4/services/MessageBusService"/>
      </wsdl:port>
   </wsdl:service>

</wsdl:definitions>

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

Листинг 2. MessageBusService.java

package com.ibm.developerworks.wspattern.four;

import java.io.Serializable;
import java.util.Enumeration;
import java.util.Iterator;

import javax.jms.Message;
import javax.jms.ObjectMessage;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueReceiver;
import javax.jms.QueueSession;
import javax.jms.Topic;
import javax.jms.TopicConnection;
import javax.jms.TopicPublisher;
import javax.jms.TopicSession;
import javax.naming.Context;
import javax.servlet.http.HttpServlet;
import javax.xml.rpc.ServiceException;
import javax.xml.rpc.server.ServiceLifecycle;

public class MessageBusService implements ServiceLifecycle {
 
Context context = null;
  QueueConnection connection =
null;
  TopicConnection tconnection =
null;
  QueueSession session =
null;
  TopicSession tsession =
null;

 
public void init(Object serviceContext) throws ServiceException {
   
try {
     
context = JNDIHelper.getInitialContext();
      connection = JNDIHelper.getQueueConnection
(context);
      session = JNDIHelper.getQueueSession
(connection);
      tconnection = JNDIHelper.getTopicConnection
(context);
      tsession = JNDIHelper.getTopicSession
(tconnection);
   
} catch (Exception e) {
    }
  }

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

 
public void send(CaseModel model) {
   
sendMessage(JNDIHelper.INPUT_TOPIC, model);
 
}

 
public CaseModel receive() {
   
return receiveMessage(JNDIHelper.OUTPUT_QUEUE);
 
}

 
private void sendMessage(String topicName, CaseModel model) {
   
try {
     
ObjectMessage message = tsession.createObjectMessage(model);
      Topic topic = JNDIHelper.getTopic
(context, topicName);
      TopicPublisher publisher = JNDIHelper.getTopicPublisher
(tsession,
          topic
);
      publisher.publish
(message);
   
} catch (Exception e) {
     
throw new RuntimeException(e.getMessage());
   
}
  }

 
private CaseModel receiveMessage(String queueName) {
   
CaseModel model = null;
   
try {
     
Queue queue = JNDIHelper.getQueue(context, queueName);
      QueueReceiver receiver = JNDIHelper
          .getQueueReceiver
(session, queue);
      Message message = receiver.receiveNoWait
();
     
if (message != null && message instanceof ObjectMessage) {
       
ObjectMessage objMessage = (ObjectMessage) message;
        Serializable obj = objMessage.getObject
();
       
if (obj instanceof CaseModel) {
         
model = (CaseModel) obj;
       
}
      }
    }
catch (Exception e) {
     
throw new RuntimeException(e.getMessage());
   
}
   
return model;
 
}

}

Модель, реализованная в примере, является прямой. Web-служба принимает запрос send и публикует модель во входную JMS-тему. Четыре различных компонента приложения прослушивают эту тему и выполняют в ответ некоторые действия над сообщением. Двое из этих компонент генерируют ответные сообщения, доставляемые в выходную JMS-тему. Поскольку клиент Web-служб не поддерживает прямое постоянное соединение типа публикация-подписка к JMS-теме, специальный перехватчик получает сообщения из выходной темы и сохраняет эти сообщения в JMS-очереди ответов. Когда клиент Web-служб вызывает сообщение invoke, ожидающие в этой очереди ответы доставляются по назначению.

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

В листинге 3 приведен исходный код одного или двух перехватчиков, которые принимают CaseModel через шину сообщений и преобразуют текст в нижний или верхний регистр. Они реализованы как простые HTTP-сервлеты, реализующие JMS-интерфейс MessageListener. В качестве альтернативы я мог бы реализовать их как управляемые JMS-сообщениями JavaBeans-компоненты, но решил этого не делать, поскольку это вызвало бы небольшое увеличение сложности приложения.

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

package com.ibm.developerworks.wspattern.four;

import java.io.Serializable;

import javax.jms.Message;
import javax.jms.ObjectMessage;
import javax.jms.Topic;
import javax.jms.TopicPublisher;
import javax.servlet.Servlet;

public class UppercaseTopicListenerServlet extends TopicListenerServlet
   
implements Servlet {

 
protected String getTopic() {
   
return JNDIHelper.INPUT_TOPIC;
 
}

 
protected String getSelector() {
   
return "";
 
}

 
public void onMessage(Message message) {
   
try {
     
if (message instanceof ObjectMessage) {
       
ObjectMessage objMessage = (ObjectMessage) message;
        Serializable obj = objMessage.getObject
();
       
if (obj instanceof CaseModel) {
         
CaseModel model = (CaseModel) obj;
         
if (model.getText() != null) {
           
model.setText(model.getText().toUpperCase());
         
}
         
ObjectMessage response = session.createObjectMessage(model);
          Topic responseTopic = JNDIHelper.getTopic
(context,
              JNDIHelper.OUTPUT_TOPIC
);
          TopicPublisher responsePublisher = JNDIHelper
              .getTopicPublisher
(session, responseTopic);
          responsePublisher.publish
(response);
       
}
      }
    }
catch (Throwable t) {
    }
  }

}

Отметим, что эти перехватчики идентичны во всех отношениях, за исключением выполняемых операций toUpperCase() и toLowerCase(). Каждый из них генерирует сообщение response с объектом CaseModel, содержащим преобразованный текст. Каждое этих ответных сообщений доставляется в выходную JMS-тему.

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

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

package com.ibm.developerworks.wspattern.four;

import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.servlet.Servlet;
import javax.servlet.ServletException;

public class ResponseTopicListenerServlet extends TopicListenerServlet
   
implements Servlet {

 
protected QueueConnection qconnection;
 
protected QueueSession qsession;
 
protected Queue queue;

 
protected String getTopic() {
   
return JNDIHelper.OUTPUT_TOPIC;
 
}

 
protected String getSelector() {
   
return "";
 
}

 
public void init() throws ServletException {
   
super.init();
   
try {
     
qconnection = JNDIHelper.getQueueConnection(context);
      qsession = JNDIHelper.getQueueSession
(qconnection);
      queue = JNDIHelper.getQueue
(context, JNDIHelper.OUTPUT_QUEUE);
   
} catch (Exception e) {
    }
  }

 
public void destroy() {
   
super.destroy();
   
try {
     
qsession.close();
      qconnection.close
();
   
} catch (Exception e) {
    }
  }

 
public void onMessage(Message message) {
   
try {
     
QueueSender sender = JNDIHelper.getQueueSender(qsession, queue);
      sender.send
(message);
   
} catch (Exception e) {
    }
  }

}

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

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

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

package com.ibm.developerworks.wspattern.four;

import java.io.Serializable;

import javax.jms.Message;
import javax.jms.ObjectMessage;
import javax.jms.Topic;
import javax.jms.TopicPublisher;
import javax.servlet.Servlet;
import javax.xml.namespace.QName;
import javax.xml.rpc.Call;
import javax.xml.rpc.ParameterMode;
import javax.xml.rpc.Service;
import javax.xml.rpc.ServiceFactory;
import javax.xml.rpc.encoding.TypeMapping;
import javax.xml.rpc.encoding.TypeMappingRegistry;

import com.ibm.ws.webservices.engine.encoding.ser.BeanDeserializerFactory;
import com.ibm.ws.webservices.engine.encoding.ser.BeanSerializerFactory;

public class RequestTopicListenerNotifierServlet extends TopicListenerServlet
   
implements Servlet {

 
private static final String NSURI = "http://four.wspattern.developerworks.ibm.com";

 
protected String getTopic() {
   
return JNDIHelper.INPUT_TOPIC;
 
}

 
protected String getSelector() {
   
return "";
 
}

 
public void onMessage(Message message) {
   
try {
     
if (message instanceof ObjectMessage) {
       
ObjectMessage objMessage = (ObjectMessage) message;
        Serializable obj = objMessage.getObject
();
       
if (obj instanceof CaseModel) {
         
CaseModel model = (CaseModel) obj;
          ServiceFactory factory = ServiceFactory.newInstance
();
          Service service = factory.createService
(new QName(NSURI,
             
"RemoteService"));

          QName cmq =
new QName(NSURI, "CaseModel");
          TypeMappingRegistry tmreg = service
              .getTypeMappingRegistry
();
          TypeMapping tm = tmreg.createTypeMapping
();
          tm.register
(CaseModel.class, cmq,
             
new BeanSerializerFactory(CaseModel.class, cmq),
             
new BeanDeserializerFactory(CaseModel.class, cmq));
          tmreg.register
("", tm);

          Call call = service.createCall
();
          call
              .setTargetEndpointAddress
("http://localhost:9080/WSPattern4/services/RemoteService");
          call.addParameter
("model", cmq, CaseModel.class,
              ParameterMode.IN
);
          call.invoke
(new QName(NSURI, "notifyRequest"),
             
new Object[] { model });
       
}
      }
    }
catch (Throwable t) {
    }
  }

}

Последним компонентом приложения, подключенным к шине сообщений, является простое терминальное приложение, отображающее на stdout уведомления о доставке сообщения во входную или выходную JMS-темы.

Листинг 6. ListenerApp.java

package com.ibm.developerworks.wspattern.four;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Serializable;

import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.ObjectMessage;
import javax.jms.Topic;
import javax.jms.TopicConnection;
import javax.jms.TopicSession;
import javax.jms.TopicSubscriber;
import javax.naming.Context;

public class ListenerApp implements MessageListener {

 
private static Context context;
 
private static TopicConnection connection;
 
private static TopicSession session;
 
private static Topic requestTopic;
 
private static Topic responseTopic;
 
private static TopicSubscriber requestSubscriber;
 
private static TopicSubscriber responseSubscriber;

 
public static void main(String[] args) throws Exception {

   
ListenerApp listener = new ListenerApp();
    context = JNDIHelper.getInitialContext
();
    connection = JNDIHelper.getTopicConnection
(context);
    session = JNDIHelper.getTopicSession
(connection);
    requestTopic = JNDIHelper.getTopic
(context, JNDIHelper.INPUT_TOPIC);
    responseTopic = JNDIHelper.getTopic
(context, JNDIHelper.OUTPUT_TOPIC);
    requestSubscriber = JNDIHelper
        .getTopicSubscriber
(session, requestTopic);
    responseSubscriber = JNDIHelper.getTopicSubscriber
(session,
        responseTopic
);
    requestSubscriber.setMessageListener
(listener);
    responseSubscriber.setMessageListener
(listener);

    String input =
"";
   
while (!"exit".equalsIgnoreCase(input)) {
     
BufferedReader br = new BufferedReader(new InputStreamReader(
         
System.in));
      System.out.print
("> ");
      input = br.readLine
();
   
}

   
requestSubscriber.close();
    responseSubscriber.close
();
    session.close
();
    connection.close
();
 
}

 
public void onMessage(Message message) {
   
synchronized (System.out) {
     
try {
       
if (message instanceof ObjectMessage) {
         
ObjectMessage objMessage = (ObjectMessage) message;
          Serializable obj = objMessage.getObject
();
         
if (obj instanceof CaseModel) {
           
CaseModel model = (CaseModel) obj;
            System.out.println
("\nMessage Received: " + model);
            System.out.println
("\tSent To: "
               
+ message.getJMSDestination());
            System.out.print
("> ");
         
}
        }
      }
catch (Exception e) {
      }
    }
  }

}

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

Заключительное слово

Приведенный в данной статье материал дает только беглый взгляд на базовую реализацию модели шины сообщений. Существует множество вопросов, которые должна решать рабочая реализация, например, система безопасности, соответствующая система маршрутизации (посредничество), модели доставки и т.д., каждый из которых является важным, но выходит за рамки данной серии статей. Нашей целью здесь и в предыдущих трех статьях была стимуляция ваших размышлений о реализации Web-служб. Простые службы котировок в RPC-стиле не показывают реальной мощи семейства спецификаций и стандартов Web-служб.

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

Ресурсы

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

Part 1: Asynchronous Web services operations using JMS
Part 2: Encapsulate business logic with a command facade pattern
Part 3: Creating flexible Web service implementations with the Router pattern

Об авторах

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