分享

使用 WS-Notification

 LibraryPKU 2015-01-21

随着系统变得愈加松散耦合,开发者开始碰 到一些相互矛盾的问题。有些系统可以从事件驱动编程(比如 Web 服务应用)中获益,并不需要采用这样一种可以简化问题的方式进行构建。WS-Notification 规范试图解决这个问题,方法是通过为 Web 服务提供一种标准的方法,从而对希望接收有关特定主题的需求方发送特定的需求信号。本文将介绍 NotificationConsumer、NotificationProducer、订阅和通知的一个简单实现。本文还将简要介绍规范所涉及的其他领 域。

注意:本文使用 Java? servlets 来展示实现一个 WS-Notification 解决方案的必要步骤,但是其中的概念对于任何编程环境来说都是相同的。要采用这些代码,您需要熟悉 Java 编程的知识。如果具有 Java servlet 的编程经验,将会非常有帮助。如果具有 XML 方面的经验,也会非常有帮助,但这并不是必需的。

什么是 WS-Notification?

WS- Notification(WSN)是一系列规范,它制定了在 Web 服务环境中创建事件驱动系统的标准流程。它们是与 Web 服务资源框架(Web Services Resource Framework,WSRF)一起开发的(后者提供了一种在无状态环境中使用状态的方法),但是它们也可以在普通的 Web 服务中使用。

WSN 包括 3 个规范:

  • WS-BaseNotification —— 这个文档的目标是解决基本的功能,它定义了 NotificationProducers、 NotificationConsumers、通知和订阅,并将它们融合在一起。它还描述了暂停订阅和恢复订阅的过程,以及控制订阅的长度的方法。
  • WS-Topics —— 当用户订阅一个 NotificationProducer 时,该订阅就与一个特定的主题或多个主题关联在一起。这个文档就解释了用来定义和创建多个主题所使用的结构。
  • WS-BrokeredNotification —— 在有些情况下,创建通知的实体并不能管理各种订阅。这个文档定义了创建一个发布者的过程,发布者可以简单地创建一些消息,并通过一个单独的 NotificationBroker 来发布这些消息。

本文将着重介绍 WS-BaseNotification 规范的一些基本信息。

什么是 WS-Resource?

我们应该花一点时间来讨论一个主题:它超出了 WSRF 规范的范围,但对于 WS-Notification 来说却是至关重要的,这就是 WS-Resources。

Web 服务本质上是一个无状态 媒介。请求中惟一可用的信息就是这个请求中所包含的信息。这使得创建一些必须在多次交互之间保留信息的应用程序很困难。例如,我们很快就要使用一个 Web 服务来创建对某个特定主题 订阅 。如果该订阅只是在创建该请求时存在,那么它就不太有用了。

反之,我们需要订阅是有状态的。有状态的资源可以在交互过程之间存在,顾名思义,它是具有状态 的。状态是通过一组属性进行定义的。修改一个属性,状态就会发生变化,反之亦然。

WS-Resource 是一组有状态的资源和与之进行交互的 Web 服务的组合。在本文中,我们将创建几个 WS-Resource。

我们要实现什么?

为 了展示 WS-Notification 的用法,我们将创建一个简单的系统,它会跟踪搜索引擎(例如 google)中的第一个位置。 NotificationConsumer 发起对某个关键字的订阅,当该关键字的第一个位置发生变化时,NotificationProducer 就会发送一个通知来详细介绍这种变化。(实际对搜索引擎进行检查的过程已经超出了本文的范围。相反,我们会着重介绍整个通知的过程。不过如果您非常关注这 个问题,请参阅 参考资料,获得有关使用 Google 的 Web 服务 API 的教程)。

我 们首先使用一个简单的 HTML 表单,用户可以在这个表单中输入自己的 e-mail 地址和希望查看的关键字。这个表单会提交到消费者 Web 服务(consumer Web service),它会将每个用户保存为一个 WS-Resource。消费者服务调用生产者 Web 服务(producer Web service),后者创建一个订阅 WS-Resource。

当生产者看到一个针对关键字的变化时,就为每个监视这个关键字的订阅收集信息,并发送一个通知给消费者 Web 服务,而它又将信息传递给适当的 WS-Resource。

准备开始

为 了让一切都正常工作,我们需要为消费者和生产者各创建一个 Web 服务。为了简化这个过程,我们会将这两个 Web 服务各自创建为一个 servlet,它负责接收 POST 请求。在这个例子中,我们使用 Apache Jakarta 项目中的 Tomcat V5 的 servlet 容器,但是任何可以支持 servlet 的服务器都可以使用。这种设置需要执行以下操作:

  1. 下载并安装 Tomcat
  2. 下载并解压 Axis 包
  3. 如果需要,您可以创建自己的 Web 应用,也可以使用服务器中所提供的一个应用。
  4. <AXIS_HOME>/lib 目录中的文件拷贝到 Web 应用的 lib 目录中。
  5. 创建两个 servlet,并添加到您的应用程序所使用的 web.xml 文件中。给它们分别取名为 ConsumerServiceServlet.javaProducerServiceServlet.java

NotificationConsumer

在真正开始编码之前,让我们首先来了解一下消费者的结构。

NotificationConsumer 可以是一个普通的 Web 服务或是一个 WS-Resource。在这两种情况中,我们都是使用 Web 服务描述语言(Web Services Description Language,WSDL)来描述它们。在 ConsumerServiceServlet 的情况中,我们具有以下的 WSDL 文件:

清单 1. NotificationConsumer WSDL 文件
<?xml version="1.0" encoding="UTF-8"?>
<definitions name="Search"
    targetNamespace="http:///search"
    xmlns="http://schemas./wsdl/"
    xmlns:tns="http:///search"
    xmlns:wsa=
          "http://schemas./ws/2004/03/addressing"
    xmlns:wsdl="http://schemas./wsdl/"
    xmlns:wsrp="http://docs./wsrf/2004/06/wsr
f-WS-ResourceProperties-1.2-draft-01.xsd"
    xmlns:wsrpwsdl="http://docs./wsrf/2004/0
6/wsrf-WS-ResourceProperties-1.2-draft-01.wsdl"
    xmlns:wsnt="http://docs./wsn/2004/06/wsn-W
S-BaseNotification-1.2-draft-01.xsd"
    xmlns:wsntw="http://docs./wsn/2004/06/ws
n-WS-BaseNotification-1.2-draft-01.wsdl"
    xmlns:soap="http://schemas./wsdl/soap/">
    
    <wsdl:import namespace="http://docs./ws
rf/2004/06/wsrf-WS-ResourceProperties-1.2-draft-01.wsdl"
              location="WS-ResourceProperties.wsdl" />
    <wsdl:import namespace="http://docs./ws
n/2004/06/wsn-WS-BaseNotification-1.2-draft-01.wsdl"
        location="WS-BaseN.wsdl" />
    
    <types>
        <xsd:schema 
              targetNamespace="http:///search"
            xmlns:xsd="http://www./2001/XMLSchema">
                        
            <xsd:element name="email" 
                                     type="xsd:string" />
            <xsd:element name="keyword" 
                                     type="xsd:string" />
            
            <xsd:element 
                       name="SearchSubscriberProperties">
                <xsd:complexType>
                    <xsd:sequence>
                        <xsd:element ref="email" 
                            minOccurs="1" maxOccurs="1"/>
                        <xsd:element ref="keyword" 
                            minOccurs="1" maxOccurs="1"/>
                        <xsd:any/>
                    </xsd:sequence>
                </xsd:complexType>
            </xsd:element>            
            
        </xsd:schema>
        
    </types>

    <portType name="ConsumerPortType" 
wsrp:ResourceProperties="tns:SearchSubscriberProperties">

        <operation name="GetResourceProperty">
            <input message=
                       "wsrpwsdl:GetResourcePropertyRequest"
                 wsa:Action="http://docs./wsr
f/2004/06/WS-ResourceProperties/GetResourceProperty"/>
            <output message=
                      "wsrpwsdl:GetResourcePropertyResponse" 
             wsa:Action="http://docs./wsrf/200
4/06/WS-ResourceProperties/GetResourcePropertyResponse"/>
        </operation>

        <wsdl:operation name="Notify">
            <wsdl:input message="wsntw:Notify" 
                wsa:Action="http://docs./wsn/2
004/06/wsn-WS-BaseNotification/Notify"/>
        </wsdl:operation>
        
    </portType>
    
    <binding name="ConsumerSoapBinding" 
                            type="tns:/ConsumerPortType">
        <soap:binding style="document" transport=
                 "http://schemas./soap/http"/>
        <operation name="GetResourceProperty">
            <input>
                <soap:body use="literal"/>
            </input>
            <output>
                <soap:body use="literal"/>
            </output>
        </operation>
        <operation name="Notify">
            <input>
                <soap:body use="literal"/>
            </input>
        </operation>
    </binding>
    
    <service name="ConsumerService">
        <port name="ConsumerPort" 
                       binding="tns:ConsumerSoapBinding">
            <soap:address 
                location="http:///search"/>
        </port>
    </service>

</definitions>

下面我们逐步来讲解。首先,导入 WS-ResourceProperties 和 WS-BaseNotification WSDL 文件。这些文件定义了我们所需要的类型和消息,因此包括这些文件使我们可以简单地引用它们。

接下来,我们为每个使用 SearchSubscriberProperties 类型的订阅者定义资源属性文档。在本文中,我们并不想使用 Web 服务来操作这些属性,但是这对于理解总体的结构非常重要。

然后,我们创建 portType,它会作为这个服务的一种接口。注意另外一个属性:wsrp:ResourceProperties。该值指向资源属性文档类型,并指明所包含的操作将应用到的资源。

在 这种情况中,我们有两个操作。首先是 GetResourceProperty,它是所有的 WS-Resources 都需要的。顾名思义,它使客户机可以获取一个 WS-Resource 的属性的值。其次是 Notify 操作,NotificationProducer 会使用它来真正发送通知。这两个操作使用各自所包含的 WSDL 文件中定义的消息。

从现在开始,就是为每个操作指定一个绑定并将这个绑定应用到一个服务上的标准问题了。

现在,我们将着重介绍 Notify 操作,因为不管您是使用一个普通的 Web 服务还是 WS-Resource,所有的 NotificationConsumers 都需要执行这个操作。

基本的消费者 servlet

这个过程的第一个步骤是为每个订阅者搜集 e-mail 地址和关键字。要实现这种功能,可以创建一个简单的 HTML 表单,如清单 2 所示。

清单 2. 订阅表单
<html>
<head>
     <title>Create a subscription</title>
</head>
<body>
   <form method="GET"
      action="http://localhost:8080/servlets-examples/servle
t/ConsumerServiceServlet">

      <h2>Create a subscription</h2>

      Email <input type="text" name="email" />
      <br />
      Topic <input type="text" name="topic" />
      <br />
      <input type="submit">

   </form>
</body>
</html>

注意,我的消费者 servlet 位于 http://localhost:8080/servlets-examples/servlet/ConsumerServiceServlet

确保将 action 参数修改为匹配您自己的系统。这个页面会创建一个表单,我们可以在这个表单中输入订阅者的基本信息。

现在让我们来看一下基本的 servlet。对这些值的检索就是一个访问请求参数的简单问题。

清单 3. 基本的 servlet
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class ConsumerServiceServlet extends HttpServlet 
                                        implements Servlet {

   public ConsumerServiceServlet() {
       super();
   }

   protected void doGet(HttpServletRequest req, 
                        HttpServletResponse resp) 
                      throws ServletException, IOException {

       String email = req.getParameter("email");
       resp.getWriter().println("Email: "+email);
      
       String topic = req.getParameter("topic");
       resp.getWriter().println("Topic: "+topic);   
       
   }

   protected void doPost(HttpServletRequest req, 
                         HttpServletResponse resp) 
                      throws ServletException, IOException {

   }
}

这个 servlet 可以从客户机(在本例中是浏览器)接收 GET 或 POST 消息。通常,我们会作为一个 POST 消息提交这个表单,但是以后我们将使用 doPost() 方法来接收通知。因此,现在我们将使用 doGet() 来接收表单,该方法将简单地检索值,并将值输出到浏览器中。

编译这个 servlet,并将其部署到服务器上。(为了让这些变化生效,可能需要重新启动服务器。)输入一个 e-mail 地址和关键字,然后提交表单。现在这些信息应该在浏览器中重现了。

创建一个订阅者

消费者服务的目的是利用一些信息,并使用这些信息为这个用户创建一个订阅。当这个服务接收到该用户的一个通知时,就将其传递给这个用户。要使这一切正常操作,我们需要该服务使用一种方法来对每个用户进行跟踪。

在一个实际的应用程序中,我们将使用一些持久性的方法,例如将信息保存到一个文件或数据库中。在本例中,为了简单起见,创建了 SubscriptionConsumer 类。

清单 4. 基本的消费者类
public class SubscriptionConsumer {

   private String email;
   private String topic;
       
   public SubscriptionConsumer(String newEmail, 
                               String newTopic){
      setEmail(newEmail);
      setTopic(newTopic);
   }
   
   public void setEmail (String newEmail){
      email = newEmail;
   }
   public void setTopic (String newTopic){
      topic = newTopic;
   }

   public String getEmail(){
      return email;
   }
   public String getTopic(){
      return topic;
   }
}

对于每个新用户,我们都会创建一个 SubscriptionConsumer 对象,并将其添加到这个 servlet 中的一个静态数组中,如清单 5 所示。(是的,这意味着在重新启动服务器时,我们将失去所有的信息。这是简单性的代价。)

清单 5. 保存订阅者的信息
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class ConsumerServiceServlet extends HttpServlet 
                                        implements Servlet {

   static SubscriptionConsumer[] consumers = 
                             new SubscriptionConsumer[1000];
   static int nextId;   
   
   public ConsumerServiceServlet() {
      super();
      nextId = 1;
   }

   protected void doGet(HttpServletRequest req, 
                        HttpServletResponse resp) 
                      throws ServletException, IOException {

       String email = req.getParameter("email");
       resp.getWriter().println("Email: "+email);
      
       String topic = req.getParameter("topic");
       resp.getWriter().println("Topic: "+topic);
       
       SubscriptionConsumer thisConsumer = 
                     new SubscriptionConsumer(email, topic);
       int thisConsumerId = nextId++;
       consumers[thisConsumerId] = thisConsumer;
       
       resp.getWriter().println("Added consumer number "+
                                        thisConsumerId);
       
   }

   protected void doPost(HttpServletRequest req, 
                         HttpServletResponse resp) 
                      throws ServletException, IOException {

   }
}

当这个 servlet 第一次运行时,它会对这个数组和 nextId 值进行初始化。由于这个数组和 nextId 值都是静态的,因此它们可以在 servlet 实例之间进行共享。同样,虽然在生产环境中不会这样做,但是我们现在正试图获得真正的通知,而不构建数据库。

每 次用户提交表单时,我们都创建一个新的 SubscriptionConsumer 对象,并将其添加到这个数组中。数组带来的简便性是,使我们可以简单地检索一个特定的订阅者。只要我们知道 ID,检索实际对象就非常简单。当服务接收到一个特定消费者的通知并需要传递该通知时,这种功能就非常方便。

订阅一个主题

尽 管由于我们正在对一个 Web 服务进行处理,还没有真正创建 NotificationProducer,但是我们确切地知道需要发送的数据。根据 WS-BaseNotification 规范,我们需要发送一个 Simple Object Access Protocol(SOAP)消息,如清单 6 所示。

清单 6. Subscribe 消息
<soapenv:Envelope 
   xmlns:soapenv="http://schemas./soap/envelope/"
       xmlns:sat="http:///searchSystem"
       xmlns:wsa="http://www./2005/02/addressing"
       xmlns:wsnt="http://docs./wsn/2004/06/ws
n-WS-BaseNotification-1.2-draft-01.xsd">
    <soapenv:Header>
       <wsa:Action>
          http://docs./wsn/2004/06/WS-BaseNoti
fication/Subscribe
       </wsa:Action>
       <wsa:To soapenv:mustUnderstand="1">
http://localhost:8080/servlets-examples/servlet/ProducerServ
iceServlet
       </wsa:To>
     </soapenv:Header>
    <soapenv:Body>
       <wsnt:Subscribe>
          <wsnt:ConsumerReference>
              <wsa:Address>
http://localhost:8080/servlets-examples/servlet/ConsumerServ
iceServlet
              </wsa:Address>
              <wsa:ReferenceProperties>
                 <search:subId 
          xmlns:search="http://www./searcher">
                       23
                 </search:subId>
              </ReferenceProperties>
          </wsnt:ConsumerReference>
          <wsnt:TopicExpression Dialect="http://docs.oasi
s-open.org/wsn/2004/06/TopicExpression/Simple">
               cheese
          </wsnt:TopicExpression>
       </wsnt:Subscribe>
    </soapenv:Body>
</soapenv:Envelope>

SOAP 头中包含两部分信息:我们在干什么(Action 元素)和要在什么地方进行这种操作(To 元素)。在本例中,To 元素代表我们稍后将要构建的 NotificationProducer 服务。

实际的 Subscribe 消息包含两部分重要信息。首先是 ConsumerReference,这是一个端点引用,指向实际的 WS-Resource。这个结构是在 WS-Addressing 规范中定义的,包含 Web 服务的地址,以及一个或多个 ReferenceProperties,我们将使用它们来区分各个订阅者。在本例中,ReferenceProperty 代表该数组中的索引,但是接收方应用程序不应该试图对端点引用进行解释。它应该被当作一个单独的信息单元。Subscribe 消息还包括订阅操作要应用到的主题。

现在让我们构建并准备传递这个消息。

清单 7. 构建 Subscribe 消息
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import javax.xml.soap.*;

public class ConsumerServiceServlet extends HttpServlet 
                                        implements Servlet {

   static SubscriptionConsumer[] consumers = 
                             new SubscriptionConsumer[1000];
   static int nextId;
   
   SOAPFactory soapFactory;
   
   public ConsumerServiceServlet() {
      super();
      nextId = 1;
      try {
         soapFactory = SOAPFactory.newInstance();
      } catch (Exception e){
         e.printStackTrace();
      }
   }

   protected void doGet(HttpServletRequest req,
                        HttpServletResponse resp) 
                      throws ServletException, IOException {

       String email = req.getParameter("email");
       String topic = req.getParameter("topic");
       
       SubscriptionConsumer thisConsumer = 
                     new SubscriptionConsumer(email, topic);
       int thisConsumerId = nextId++;
       consumers[thisConsumerId] = thisConsumer;
       
       try {
      
          MessageFactory messageFactory = 
                               MessageFactory.newInstance();
          SOAPMessage message = 
                             messageFactory.createMessage();
     
          SOAPHeader header = message.getSOAPHeader();
          SOAPHeaderElement actionElement = 
               header.addHeaderElement(
                     soapFactory.createName("Action", "wsa", 
                   "http://www./2005/02/addressing"));
          actionElement.addTextNode(
"http://docs./wsn/2004/06/WS-BaseNotificatio
n/Subscribe");
          SOAPHeaderElement toElement = 
             header.addHeaderElement(
                         soapFactory.createName("To", "wsa", 
                   "http://www./2005/02/addressing"));
          toElement.addTextNode(
"http://localhost:8080/servlets-examples/servlet/ProducerSer
viceServlet");
          toElement.setMustUnderstand(true);

          SOAPBody body = message.getSOAPBody();
          SOAPElement bodyElement = 
              body.addChildElement(soapFactory.createName(
                                        "Subscribe", "wsnt",                  
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd"));

          SOAPElement subEndpoint=soapFactory.createElement(
                                "ConsumerReference", "wsnt", 
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd");
          
          SOAPElement address = soapFactory.createElement(
                                           "Address", "wsa", 
                    "http://www./2005/02/addressing");
          address.addTextNode(
"http://localhost:8080/servlets-examples/servlet/ConsumerSer
viceServlet");
          subEndpoint.appendChild(address);
          
          SOAPElement referenceProps = 
            soapFactory.createElement("ReferenceProperties", 
             "wsa", "http://www./2005/02/addressing");
          SOAPElement subscriberId = 
                soapFactory.createElement("subID", "search", 
                         "http://www./searcher");
          subscriberId.addTextNode(
                            String.valueOf(thisConsumerId));
          referenceProps.appendChild(subscriberId);
          subEndpoint.appendChild(referenceProps);

          bodyElement.appendChild(subEndpoint);
              
          SOAPElement topicExpression = 
             bodyElement.addChildElement("TopicExpression");
          topicExpression.addAttribute(
                       soapFactory.createName("Dialect", "", 
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd"), 
"http://docs./wsn/2004/06/TopicExpression/Simp
le");
          topicExpression.addTextNode(topic);
              
          bodyElement.appendChild(topicExpression);     

          message.saveChanges();
          
          message.writeTo(resp.getOutputStream());
          
       } catch (Exception e){
              e.printStackTrace(resp.getWriter());
       }
   }

   protected void doPost(HttpServletRequest req, 
                         HttpServletResponse resp) 
                      throws ServletException, IOException {
   }
}

现在我们来逐步讲解。首先,我们正在使用 SOAP with Attachments API for Java(SAAJ)中的类来构建 SOAP 消息,因此我们要做的第一件事情对于我们要使用的东西来说非常重要。在大部分情况下,我们都是使用 SOAPFactory 来创建这个对象。由于我们最终是使用各种方法来使用这个消息,因此将创建一个全局的实例,并在创建这个 servlet 时对其进行初始化。

接下来,我们将使用 MessageFactory 创建实际的消息。从现在开始,我们会获得一个对这个 SOAP 消息头的引用,并向其中添加适当的元素,包括它们的名称空间。注意,To 元素中 soapenv:mustUnderstand 属性的值必须为 1,因此我们需要设置这个值。

在检索对 Body 元素的一个引用之后,我们将构造其中的元素,首先从 Subscribe 元素开始,然后再是 ConsumerReferenceTopicExpression 元素。对于每一种情况,我们都(使用正确的名称空间)创建元素,添加所有必需的文本,并将该元素添加到它的父元素。

最后,我们将保存这个消息,并将其输出到浏览器中。再次提交这个表单,我们会看到一个如清单 8 所示的响应。

清单 8. 所生成的消息
<soapenv:Envelope 
   xmlns:soapenv="http://schemas./soap/envelope/" 
   xmlns:xsd="http://www./2001/XMLSchema" 
   xmlns:xsi="http://www./2001/XMLSchema-instance">
   <soapenv:Header>
      <wsa:Action soapenv:actor=
                "http://schemas./soap/actor/next" 
        soapenv:mustUnderstand="0" 
        xmlns:wsa="http://www./2005/02/addressing">
http://docs./wsn/2004/06/WS-BaseNotification/S
ubscribe
      </wsa:Action>
      <wsa:To soapenv:actor=
                "http://schemas./soap/actor/next" 
        soapenv:mustUnderstand="1" 
        xmlns:wsa="http://www./2005/02/addressing">
http://localhost:8080/servlets-examples/servlet/ProducerServ
iceServlet
      </wsa:To>
   </soapenv:Header>
   <soapenv:Body>
      <wsnt:Subscribe xmlns:wsnt=
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd">
        <wsnt:ConsumerReference>
           <wsa:Address xmlns:wsa=
                  "http://www./2005/02/addressing">
http://localhost:8080/servlets-examples/servlet/ConsumerServ
iceServlet
           </wsa:Address>
           <wsa:ReferenceProperties xmlns:wsa=
                  "http://www./2005/02/addressing">
               <search:subID xmlns:search=
                       "http://www./searcher">
                   7
               </search:subID>
           </wsa:ReferenceProperties>
        </wsnt:ConsumerReference>
        <wsnt:TopicExpression Dialect=
"http://docs./wsn/2004/06/TopicExpression/Simp
le">
            cheese
        </wsnt:TopicExpression>
      </wsnt:Subscribe>
   </soapenv:Body>
</soapenv:Envelope>

现在我们已经准备好创建 NotificationProducer 服务了,它将接收这个消息。

创建 NotificationProducer

NotificationProducer 也是一个 WS-Resource,这意味着我们可以使用一个 WSDL 文件来描述它。为了节省空间,我们就不再在本文中详细列出这个文件了;您可以从 参考资料 中下载这个文件。但是从形式上来说,它与 NotificationConsumer 使用的 WSDL 文件非常类似。

至于需求来说,NotificationProducer 必须能够满足以下要求:

  • GetResourceProperty —— 返回一个资源属性,例如 NotificationProducer 所支持的主题。
  • Subscribe —— 创建一个新的订阅。
  • GetCurrentMessage —— 返回为一个特定主题发送的最新消息(如果有的话)。

当 NotificationProducer 创建一个订阅时,SubscriptionManager 必须也可以执行 PauseSubscriptionResumeSubscription 操作。

在本文中,我们将着重介绍 Subscribe 操作。下面我们首先介绍基本的 servlet。

NotificationProducer servlet

现在我们已经准备好创建基本的 ProducerServiceServlet 了。此时,我们只是做总体介绍并接收实际的消息:

清单 9. 接收消息
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.xml.soap.*;

public class ProducerServiceServlet extends HttpServlet 
                                        implements Servlet {

   static SOAPFactory soapFactory;
   static Subscriber[] subscribers = new Subscriber[1000];
   static int nextId;
   
   public ProducerServiceServlet() {
      super();
      nextId = 1; 
      
      try {
         soapFactory = SOAPFactory.newInstance();
      } catch (Exception e){
         e.printStackTrace();
      }
   }

   protected void doGet(HttpServletRequest req, 
                        HttpServletResponse resp) 
                      throws ServletException, IOException {

   }

   protected void doPost(HttpServletRequest req, 
                         HttpServletResponse resp) 
                      throws ServletException, IOException {
       try {

          InputStream toSOAP = req.getInputStream();

          MessageFactory messageFactory = 
                               MessageFactory.newInstance();
          SOAPMessage message = 
                 messageFactory.createMessage(null, toSOAP);

          SOAPMessage reply = message;
               
          reply.writeTo(resp.getOutputStream());
               
       } catch (Exception e){
          e.printStackTrace(resp.getWriter());
       }

   }

}

首先从最上面开始,这个服务与消费者服务一样,最终都要保存资源,因此我们已经添加了一个 Subscriber 数组(作为一个静态变量),并初始化了计数器。稍后我们将编写实际的类。也与消费者服务类似,我们将创建很多 SOAP 对象,因此我们已经提前创建了 SOAPFactory。

在本例中,我们正在接收一个 SOAP 消息,因此需要使用 doPost() 方法来检索其内容。幸运的是,SAAJ 通过提供一个 createMessage() 方法(它会接收一个 InputStream)对此进行了简化。通过将其填充到表示请求的 InputStream,我们可以简单地获得原始的 SOAP 消息。

最终,我们将对这个消息进行响应,因此我们已经创建了第二个 SOAP 消息,将其作为响应消息使用,然后我们将其作为对原始请求的响应写入到 OutputStream 中。为了方便起见,我们可以将响应消息设置为与原始消息完全相同,这样就可以确保一切运行良好。但是首先我们需要将这个请求真正发送出去。

发送请求

SAAJ 使得发送 SOAP 消息并接收响应变得相当简单。

清单 10. 发送 SOAP 消息
...
          message.saveChanges();
          
          message.writeTo(resp.getOutputStream());
          
          String destination = 
"http://localhost:8080/servlets-examples/servlet/ProducerSer
viceServlet";

          SOAPConnectionFactory soapConnFactory = 
                        SOAPConnectionFactory.newInstance();
          SOAPConnection connection = 
                         soapConnFactory.createConnection();
          SOAPMessage reply = connection.call(message, 
                                               destination);
          
          reply.writeTo(resp.getOutputStream());
          
       } catch (Exception e){
              e.printStackTrace(resp.getWriter());
       }
   }
...

目标非常简单,就是这个生产者 servlet 的 URL。使用这个信息,我们可以创建一个新的 SOAPConnection 对象,并使用它来发送我们创建的 SOAPMessage。结果是一个 SOAPMessage 响应,之后我们可以将其输出到浏览器上。

要保证它可以工作,只要提交原始的 HTML 表单即可。消费者服务接收信息,使用所接收的信息来创建一个 SOAP 消息,并将其发送给生产者服务,生产者服务现在只是返回该消息。消费者服务然后会输出所产生的响应。

因此,现在您应该会在浏览器中看到原始 SOAP 消息的两个拷贝。

分析请求

我们已经得到了请求,现在需要对其进行分析并确定要做什么。首先,由于这个服务可以用于多种请求类型,我们需要确定要采取什么操作。

清单 11. 确定操作
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.xml.soap.*;

public class ProducerServiceServlet extends HttpServlet 
                                        implements Servlet {

  static SOAPFactory soapFactory;
  static Subscriber[] subscribers = new Subscriber[1000];
  static int nextId;
   
  public ProducerServiceServlet() {
     super();
     nextId = 1; 
      
     try {
        soapFactory = SOAPFactory.newInstance();
     } catch (Exception e){
        e.printStackTrace();
     }
  }

  protected void doGet(HttpServletRequest req, 
                       HttpServletResponse resp) 
                      throws ServletException, IOException {

  }
  
  private String getAction(SOAPMessage message){
     String action;
     try {
         SOAPElement request = (SOAPElement)message
                   .getSOAPBody().getChildElements().next();
         action = request.getLocalName();
     } catch (Exception e){
         action = e.getMessage();
     }
     return action;
      
  }

  private SOAPMessage Subscribe(SOAPMessage message){
     SOAPMessage reply = null;
     try {
        MessageFactory messageFactory =
                               MessageFactory.newInstance();
        reply = messageFactory.createMessage();
        reply.getSOAPBody().addBodyElement(
               soapFactory.createName("SubscribeResponse"));
     } catch (Exception e){
        e.printStackTrace();
     }
     return reply;
  }
   
...

  protected void doPost(HttpServletRequest req, 
                        HttpServletResponse resp) 
                      throws ServletException, IOException {
     try {

       InputStream toSOAP = req.getInputStream();

       MessageFactory messageFactory = 
                               MessageFactory.newInstance();
       SOAPMessage message = 
                 messageFactory.createMessage(null, toSOAP);

       SOAPMessage reply;
          
       String action = getAction(message);
       if (action.equals("Subscribe")){
          reply = Subscribe(message);
       } else if (action.equals("GetCurrentMessage")) {
          reply = GetCurrentMessage(message);
       } else if (action.equals("GetResourceProperty")){
          reply = GetResourceProperty(message);
       } else {
          reply = UnknownActionFault(message);
       }
               
       reply.writeTo(resp.getOutputStream());
               
    } catch (Exception e){
...

getAction() 方法对消息进行分析,从而确定它的内容。在本例中,我们只是依赖于负载,或者是 Body 元素的内容,但是您也可以查看消息头中的 Action 元素。从现在开始,我们确定要执行哪些方法。

在每一种情况中,我们将返回响应消息。在本例中,我们创建一个简单的响应,它将显示已经执行了哪些方法;为了节省空间,其他操作的类似方法都已经忽略了。现在我们可以创建真正的订阅了。

保存订阅

下一个步骤是创建真正的订阅。正如 ConsumerSubscribers 中的情况一样,我们将创建一个简单的类。

清单 12. Subscriber 类
import javax.xml.soap.SOAPElement;

public class Subscriber {

    private SOAPElement consumerEndpoint;
    private String topicExpression;
    private String useNotify;
       
   public Subscriber(SOAPElement consumer, String topic, 
                                             String notify){
        setConsumerEndpoint(consumer);
        setTopicExpression(topic);
        setUseNotify(notify);
   }
   
   public void setConsumerEndpoint(SOAPElement consumer){
      consumerEndpoint = consumer;
   }
   public void setTopicExpression (String topic){
      topicExpression = topic;
   }
   public void setUseNotify (String notify){
      useNotify = notify;
   }

    public SOAPElement getConsumerEndpoint(){
       return consumerEndpoint;
    }
    public String getTopicExpression(){
       return topicExpression;
    }
    public String getUseNotify(){
       return useNotify;
    }
}

Subscriber 类为每个订阅者保存了三部分信息:地址/端点,希望对哪些主题接收通知,以及格式选项。WS-Notification 允许用户指定是希望接收完整的 Notify-format 消息(正如我们此处看到的一样),还是只接收一个简化的版本。由于我们没有在 Subscribe 请求中包含这个信息,所以我们假设所有的订阅者都希望使用 Notify 格式,但是从技术上来说,NotificationProducer 必须可以按照订阅者到订阅者的原则满足这两个选项。

下一个步骤是分析 Subscribe 消息,从中提取出订阅消息。

清单 13. 分析 Subscribe 消息
...
   private SOAPMessage Subscribe(SOAPMessage message){
      SOAPMessage reply = null;
      try {
         
        Name subscribe = soapFactory.createName("Subscribe",
                                                "wsnt", 
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd");
        Name consumerReference = soapFactory.createName(
                                "ConsumerReference", "wsnt", 
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd");
        Name topicExpression = soapFactory.createName(
                                  "TopicExpression", "wsnt", 
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd");
             
        SOAPElement bodyElement = 
             (SOAPElement)message.getSOAPBody()
                        .getChildElements(subscribe).next();
         
        SOAPElement consumerElement = 
             (SOAPElement)bodyElement
                .getChildElements(consumerReference).next();
        SOAPElement topicElement = 
             (SOAPElement)bodyElement
                  .getChildElements(topicExpression).next();
        String topicString = 
                         topicElement.getValue().toString();

        Subscriber newSubscriber = 
                           new Subscriber(consumerElement, 
                                       topicString, "true");
        int thisId = nextId++;
        subscribers[thisId] = newSubscriber;
         
         
        MessageFactory messageFactory = 
                               MessageFactory.newInstance();
        reply = messageFactory.createMessage();
        SOAPElement sub = reply.getSOAPBody().
           addBodyElement(
               soapFactory.createName("SubscribeResponse"));

        sub.addChildElement(consumerElement);
        sub.addChildElement(topicElement);

      } catch (Exception e){
        e.printStackTrace();
      }
      return reply;
   }
...

我们不会遍历 SOAPMessage 来查找单个元素,而是创建 Name 对象(它直接指向要检索的元素),然后使用 getChildElements() 方法来检索元素。

在本例中,我们将消费者的端点直接保存在对象中 —— 稍后我们需要使用这个初始的信息 —— 不过我们要提取 topicExpression 的内容。然后我们可以创建一个新的 Subscriber 对象,并将其添加到 Subscribers 数组中。

最后,通过将所提出的元素临时添加到响应消息中,我们可以确保收集到想要的数据。

发送 SubscribeResponse

在创建订阅之后,我们需要为其创建一个端点引用,并发送回消费者。采用这种方法,消费者可以使用这个端点对订阅进行修改,例如暂停、恢复或取消订阅。

我们从创建端点并将其添加到消息开始。

清单 14. 添加端点
...
     Subscriber newSubscriber = 
                   new Subscriber(consumerElement, 
                                       topicString, "true");
     int thisId = nextId++;
     subscribers[thisId] = newSubscriber;
        
     MessageFactory messageFactory = 
                               MessageFactory.newInstance();
     reply = messageFactory.createMessage();
     SOAPElement sub = reply.getSOAPBody()
           .addBodyElement(
                 soapFactory.createName("SubscribeResponse",
                                        "wsnt", 
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd"));

     SOAPElement subEndpoint = 
          soapFactory.createElement("SubscriptionReference",
                                    "wsnt",
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd");
          
     SOAPElement address = 
                 soapFactory.createElement("Address", "wsa", 
                    "http://www./2005/02/addressing");
     address.addTextNode(
"http://localhost:8080/servlets-examples/servlet/ProducerSer
viceServlet");
     subEndpoint.appendChild(address);
          
     SOAPElement referenceProps = 
            soapFactory.createElement("ReferenceProperties", 
                                      "wsa", 
                    "http://www./2005/02/addressing");
     SOAPElement subId = 
          soapFactory.createElement("subscriptionID", "sub", 
                    "http://www./searchService");
     subId.addTextNode(String.valueOf(thisId));
     referenceProps.appendChild(subId);
     subEndpoint.appendChild(referenceProps);
          
     sub.appendChild(subEndpoint);
         
     reply = populateHeader(reply, 
                      newSubscriber.getConsumerEndpoint(), 
"http://docs./wsn/2004/06/WS-BaseNotificatio
n/SubscribeResponse");
         
   } catch (Exception e){
     e.printStackTrace();
   }
   return reply;
 }
...

这个过程与消费者中的过程是相同的:创建各个元素,并将其构建在一个层次结构中,然后将它们添加到消息中。

然而,在本例中,我们还需要另外一个步骤。当我们在订阅时,已经知道了 NotificationProducer 的地址,因此可以直接创建 SOAP 头。然而,在本例中,这个头是基于消费者的端点引用中的信息的,因此我们需要根据这些信息来构建这个头。

清单 15. 构建 SOAP 头
   private SOAPMessage populateHeader(SOAPMessage message, 
                                      SOAPElement endpoint, 
                                      String action){
     try {
        SOAPHeader header =  message.getSOAPHeader();

        SOAPHeaderElement actionElement = 
             header.addHeaderElement(
                soapFactory.createName("Action", "wsa", 
                   "http://www./2005/02/addressing"));
        actionElement.addTextNode(action);
           
        Name address = soapFactory.createName("Address", 
             "wsa", "http://www./2005/02/addressing");
        SOAPElement addressElement = (SOAPElement)endpoint
                          .getChildElements(address).next();
        String toAddr = addressElement.getFirstChild()
                                            .getNodeValue();

        SOAPHeaderElement toElement = 
           header.addHeaderElement(
              soapFactory.createName("To", "wsa", 
                   "http://www./2005/02/addressing"));
        toElement.addTextNode(toAddr);

        Name referenceProperties = 
           soapFactory.createName("ReferenceProperties",
             "wsa", "http://www./2005/02/addressing");
        SOAPElement refPropsElement =(SOAPElement)endpoint
              .getChildElements(referenceProperties).next();
           
        java.util.Iterator refProps = 
                         refPropsElement.getChildElements();
           
        while (refProps.hasNext()){
           Object thisPropertyObj = refProps.next();
           SOAPElement thisProperty = 
                               (SOAPElement)thisPropertyObj;
           header.addHeaderElement(soapFactory
                    .createName(thisProperty.getLocalName(), 
                       thisProperty.getPrefix(), 
                       thisProperty.getNamespaceURI()))
                          .addTextNode(thisProperty
                           .getFirstChild().getNodeValue());
           }           
        } catch (Exception e){
           try {
           SOAPBody replySoapBody = message.getSOAPBody();
           replySoapBody.addBodyElement(
                        soapFactory.createName("Response"));
                                                             .addTextNode(e.toString());
           } catch (Exception e2){}
        }
      return message;      
   }
...

有些信息,例如 ActionTo 元素,都非常简单。我们将 Action 元素的值传递到这个方法中,而 To 元素的值来自于端点的 Address 元素。

然而,ReferenceProperties 元素有点小问题。根据 WS-Addressing 规范, ReferenceProperties 元素的内容必须被添加到 SOAP 头中,因此在假设我们所有的 ReferenceProperties 都是没有属性或子孙的元素的同时,我们将其简单地拷贝到头中。(当然,生产解决方案要更加健壮。)

结果是 SubscribeResponse,这会发送回消费者。

清单 16. SubscribeResponse
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv=
                 "http://schemas./soap/envelope/" 
   xmlns:xsd="http://www./2001/XMLSchema" 
   xmlns:xsi="http://www./2001/XMLSchema-instance">
 <soapenv:Header>
  <wsa:Action 
        xmlns:wsa="http://www./2005/02/addressing">
http://docs./wsn/2004/06/WS-BaseNotification/S
ubscribeResponse
  </wsa:Action>
  <wsa:To soapenv:mustUnderstand="1" 
        xmlns:wsa="http://www./2005/02/addressing">
http://localhost:8080/servlets-examples/servlet/ConsumerServ
iceServlet
  </wsa:To>
  <search:subID 
          xmlns:search="http://www./searcher">
      1
  </search:subID>
 </soapenv:Header>
 <soapenv:Body>
  <wsnt:SubscribeResponse xmlns:wsnt=
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd">
   <wsnt:SubscriptionReference>
    <wsa:Address 
        xmlns:wsa="http://www./2005/02/addressing">
http://localhost:8080/servlets-examples/servlet/ProducerServ
iceServlet
    </wsa:Address>
    <wsa:ReferenceProperties 
        xmlns:wsa="http://www./2005/02/addressing">
      <sub:subscriptionID 
        xmlns:sub="http://www./searchService">
         1
      </sub:subscriptionID>
    </wsa:ReferenceProperties>
   </wsnt:SubscriptionReference>
  </wsnt:SubscribeResponse>
 </soapenv:Body>
</soapenv:Envelope>

接收响应

最后,为了完成这个循环,消费者需要接收响应,并保存适当订阅者的端点。

清单 17. 接收响应
...
          SOAPConnection connection = 
                         soapConnFactory.createConnection();
          SOAPMessage reply = connection.call(message, 
                                               destination);
          
          SOAPBody replyBody = reply.getSOAPBody();
          
          SOAPElement replyBodyElement = 
           (SOAPElement)replyBody.getChildElements().next();
          String responseName = 
                            replyBodyElement.getLocalName();
          
          if (responseName.equals("SubscribeResponse")){
             SOAPElement responseElement = 
    (SOAPElement)replyBodyElement.getChildElements().next();
             consumers[thisConsumerId]
                  .setSubscriptionEndpoint(responseElement);
             resp.getWriter()
                           .print("Added new subscription");
          } else {
             resp.getWriter().print("Improper Response: "
                                             +responseName);
          }
       } catch (Exception e){
              e.printStackTrace(resp.getWriter());
       }
   }

   protected void doPost(HttpServletRequest req, 
                         HttpServletResponse resp) 
                      throws ServletException, IOException {
...

首先,我们检查并确保响应确实是 SubscribeResponse。如果 NotificationProducer 有问题,它就产生一个 Fault 消息。(请参阅 参考资料 中有关通知错误的消息。)如果是这样,我们简单地获取它的第一个孩子,并将其添加到 SubscriptionConsumer 对象中。

在这种情况中,我们不必对 SOAP 头进行解释,因为我们正在处理一个同步请求,因此已经知道了正在处理的 SubscriptionConsumer。当我们接收到一个通知时,情况就不一样了。

发送通知

此时,我们已经创建了订阅,知道通知要发送到什么地方,但是还不知道如何发送。下面我们来看一个实际的通知消息。

清单 18. 通知消息
<soapenv:Envelope xmlns:soapenv="http://schemas./soap/envelope/"
        xmlns:sat="http:///searchSystem"
        xmlns:wsa="http://www./2005/02/addressing"
        xmlns:wsnt=
"http://docs./wsn/2004/06/wsn-WS-BaseN-1.2-dra
ft-01.xsd">
    <soapenv:Header>
       <wsa:Action>
http://docs./wsn/2004/06/WS-BaseNotification/N
otify
       </wsa:Action>
       <wsa:To soapenv:mustUnderstand="1" 
        xmlns:wsa="http://www./2005/02/addressing">
http://localhost:8080/servlets-examples/servlet/ConsumerServ
iceServlet
       </wsa:To>
       <search:subID 
          xmlns:search="http://www./searcher">
           1
       </search:subID>
    </soapenv:Header>
    <soapenv:Body>
       <wsnt:Notify>
          <wsnt:NotificationMessage>
              <wsnt:Topic Dialect="http://docs.oasis-ope
n.org/wsn/2004/06/TopicExpression/Simple">
                 cheese
              </wsnt:Topic>
              <wsnt:Message>
                <search:NewSearchResult>
                    The site ranked number one for this 
                    topic has changed.
                </search:NewSearchResult>
              </wsnt:Message>
          </wsnt:NotificationMessage>
       </wsnt:Notify>
    </soapenv:Body>
</soapenv:Envelope>

就像 SubscribeResponse 中一样,头中包含了有关消费者的信息。消息主体中包含了一个 Notify 元素,其中包含了主题和真正的消息。Notify 元素也可以包括对 NotificationProducer 的端点引用,但是我们这里不做介绍。

(如果消费者已经指定了 UseNotify 为 false,我们将简单地发送 search:NewSearchResult 元素,而不是 Notify 元素。)

生产者服务会为每个为适当目标注册的消费者生成这个消息。

清单 19. 创建通知消息
...
   public ProducerServiceServlet() {
      super();
      nextId = 1; 
      
      try {
         soapFactory = SOAPFactory.newInstance();
      } catch (Exception e){
         e.printStackTrace();
      }
   }

    private SOAPMessage makeNotification(int i){
       SOAPMessage message = null;
       try{
          MessageFactory messageFactory = 
                               MessageFactory.newInstance();
          message = messageFactory.createMessage();
              
          message = populateHeader(message, 
                       subscribers[i].getConsumerEndpoint(), 
  "http://docs./wsn/2004/06/WS-BaseNotificatio
n/Notify");

          SOAPBody body = message.getSOAPBody();
   
          SOAPElement bodyElement = 
              body.addChildElement(soapFactory.createName(
                                           "Notify", "wsnt", 
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd"));

          SOAPElement notificationMessage = 
                 bodyElement.addChildElement(
                      "NotificationMessage", "wsnt", 
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd");
               
          SOAPElement topicExpression = 
               notificationMessage.addChildElement("Topic");
          topicExpression.addAttribute(
                       soapFactory.createName("Dialect", "", 
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd"), 
"http://docs./wsn/2004/06/TopicExpression/Simp
le");
          topicExpression.addTextNode(
                       subscribers[i].getTopicExpression());
              
          SOAPElement messageElement = 
              notificationMessage.addChildElement(
                 "Message", "wsnt", 
"http://docs./wsn/2004/06/wsn-WS-BaseNotificat
ion-1.2-draft-01.xsd");
          messageElement.addChildElement("NewSearchResult", 
               "search", "http://www./searches")
                 .addTextNode("The site ranked number one "+
                             "for this topic has changed.");
              
          message.saveChanges();
              
       } catch (Exception e){
             e.printStackTrace();
       }
       return message;
    }

   protected void doGet(HttpServletRequest req, 
                        HttpServletResponse resp) 
                      throws ServletException, IOException {

       String topic = req.getParameter("topic");
       for (int i=1; i < nextId; i++){
       
         if (topic.equals(subscribers[i]
                                    .getTopicExpression())){
            SOAPMessage message = makeNotification(i);
               
            try {
               message.writeTo(resp.getOutputStream());
            } catch (Exception e){
               resp.getWriter().print(e.getMessage());      
            }    
               
         } 
      }
  }
...

在实际情况中,通知是必需的这一事实可能来自于一个进程,比如在本例中是一个对搜索引擎进行监视的进程。然而,为了简化问题,我们将使用 doGet() 方法获得特定的主题。(您可以创建一个调用这个 servlet 的表单,或者简单地在 URL 的末尾加上“topic=mytopic”。)

当您有了主题之后,我们就可以来看一下每个订阅者,了解他们是否都订阅了这个主题。如果他们的确都已经订阅了这个主题,就使用 makeNotification() 方法用这些订阅者的信息来创建适当的 SOAP 消息。这个进程实际上等于我们在创建 SubscribeResponse 消息时所使用的进程,当然,负载是不同的。

现在我们已经准备好实际发送消息了。虽然它试图简单地使用 SOAPConnection.call() 方法,正如我们在订阅时所做的一样,但是我们实际上并不会在此处调用这个方法,因为通知消息是单向的,这就意味着消费者并不会真正返回一个响应。因为 call() 方法要等待一个响应,而这个响应并不会产生。

因此,在我们的例子中,将简单地直接创建 HTTP 连接,并将消息写入其中。

清单 20. 写入 HTTP 连接
...
       for (int i=1; i < nextId; i++){
       
          if (topic.equals(subscribers[i]
                                    .getTopicExpression())){
             SOAPMessage message=makeNotification(i);
             String destination=getDestination(message);
             resp.getWriter().println("Sending to "
                                              +destination);
             try{               

               URL url = new URL(destination);
               HttpURLConnection connection = 
                    (HttpURLConnection)url.openConnection();
               connection.setRequestMethod("POST");
               message.writeTo(
                              connection.getOutputStream());
               InputStream in = 
                           connection.getInputStream();//));
               in.close(); 
                
             } catch (Exception e){
               resp.getWriter().print(e.getMessage());      
             }    
               
          } 
       }
...
    private String getDestination(SOAPMessage message){
        try {
        
           Name address=soapFactory.createName("To", "wsa", 
                    "http://www./2005/02/addressing");
           SOAPElement addressElement = 
              (SOAPElement)message.getSOAPHeader()
                          .getChildElements(address).next();
           
           return addressElement.getValue();
        
        } catch (Exception e){
           e.printStackTrace();
           return e.toString();
        }
 
    }
...

在我们可以发送任何消息之前,需要知道这些消息要发送到什么地方。因此,我们首先使用 getDestination() 方法来分析实际的消息,并返回 To 元素的值。一旦完成之后,就可以打开一个 URLConnection(更具体地说,就是 HttpURLConnection,这样我们就可以将请求方法设置为 POST),并将消息写入其中。注意,只写入这些消息是不够的,您还必须打开并关闭 InputStream,即使没有实际消息需要接收或发送。

接收通知

我们差不多要完成了。现在只需要对消费者进行设置,让他们真正接收通知,然后就完成任务了。

我们为最初的表单使用 doGet() 方法,但是通知是以 POST 请求的形式传来的,因此我们将使用 doPost() 来处理到达的消息:

清单 21. 处理通知
...
   private void Notify(String message){
      try {
        File file = new File("/home/myself/"+
         String.valueOf(System.currentTimeMillis())+".txt");
        file.createNewFile();
        FileWriter fwrite = new FileWriter(file);
        fwrite.write(message);
        fwrite.flush();
        fwrite.close();

      } catch (IOException e) {
        e.printStackTrace();
      }      
   }
   
   private int getIntendedSubscriber(SOAPMessage message){
      try {
        
        Name subId=soapFactory.createName("subID", "search", 
                         "http://www./searcher");
        SOAPElement subIdElement = 
              (SOAPElement)message.getSOAPHeader()
                            .getChildElements(subId).next();
           
        String subIdStr = subIdElement.getValue();
        return Integer.parseInt(subIdStr);

      } catch (Exception e){
        e.printStackTrace();
        return -1;
     }
   }
   
   protected void doPost(HttpServletRequest req, 
                         HttpServletResponse resp) 
                      throws ServletException, IOException {
        
       try {
             
          InputStream toSOAP = req.getInputStream();

          MessageFactory messageFactory = 
                               MessageFactory.newInstance();
          SOAPMessage message = 
                 messageFactory.createMessage(null, toSOAP);
            
          if (getAction(message).equals("Notify")){
             int subscriberId = 
                             getIntendedSubscriber(message);
             Notify(consumers[subscriberId].getEmail());
          }
            
       } catch (Exception e){
          e.printStackTrace(resp.getWriter());
       }
   }
...

正如我们在发送原始的 Subscribe 消息时所做的一样,我们将使用这个请求来拼装 SOAP 消息,并进行检查以确保这是我们期望的类型。如果是我们期望的类型,我们就使用 getIntendedSubscriber() 方法从 SOAP 头中提取 subId 的值。这给了我们适当 SubscriptionConsumer 对象的索引,该对象给我们适当的 e-mail。

在这种应用程序中,我们通常都会使用 e-mail 地址并发送一个消息,但是我们无疑没有篇幅去介绍这些内容。相反,我们提供了一个简单的 Notify() 方法,因此可以看到它正在工作,所创建的每个对特定主题的订阅都会给我们一个单独的通知。

WS-Notification 的其他方面

在本文中,我们使用 WS-Notification 系列规范中描述的消息模式设置了一个简单的系统,用于订阅和接收对某个特定主题的通知。我们创建了适当的 SOAP 消息,将其在基于 Java servlet 的 Web 服务之间来回发送,并对其内容进行分析。

但是即使到现在为止所有的工作都完成之后,我们仍然只是很浅地涉及了创建这样一个利用 WS-Notification 的系统的知识。下面是一些需要记住的特性:

  • 一旦消费者具有了订阅的端点 —— 或者更适当地说,是订阅管理程序 —— 他就可以选择暂停订阅或恢复订阅。他也可以通过销毁代表订阅的 WS-Resource 来停止订阅,或者通过修改它的 TerminationTime 资源属性来扩展其生命周期。
  • 主题通常并不是任意的。WSRF 规定,资源属性应该被指定为消费者为了接收变化的通知而订阅的主题。
  • 我 们已经创建了一个(非常)简单的实现,目的是用来展示 WS-Notification 可以用来干什么,但是这并不需要再添加更多内容来完成这个应用程序。Globus Toolkit V4 Java Core 包含了一个(几乎)完整的 WS-Notification 实现,同时还提供了一个 WSRF 的实现。(Globus Toolkit 目前并不支持转发通知,在这种情况中,发布者和 NotificationProducer 不在同一实体上。)

要更多地理解 WS-Notification,最好是不仅要理解规范本身,还要理解 WSRF 背后的概念。您可以查看 参考资料 中 developerWorks 的教程和其他信息。

下载

描述名字大小
Source codeproducer.wsdl6.17 KB

参考资料

WSN 和 WSRF 文档 的主要位置位于 The Globus Alliance,但是最新的 WS-Notification 规范可以在 Oasis 处找到。文档包括:

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多