动态发现和调用 Web 服务

如何使用 UDDI 和 Axis 动态地开发和调用一个 Web 服务。

Comments

在本文中,我将根据 Web 服务的基本原则进行构建。如果您不是很了解 UDDI、WSDL 和 Axis,或者说您以前未曾创建过一个基本的 Web 服务,那么您可能不会觉得这篇文章很有用。如果是这种情况,那么我建议您先看看 参考资料,那里解释了 Web 服务及相关的一些技术。另一方面,如果您确实了解 Web 服务并且对本文中出现的任何资料感到困惑或不确定,那么请在 Web Services Technical 论坛上张贴您的意见,我将定期进行查看。

服务器实现


虽然本文不是关于 Web 服务的服务器端实现的,但还是值得提一下我将使用的 Web 服务的组成。我将以一个简单的 Java 类作为我的 Web 服务的基础,这个 Java 类所带的方法将接受两个整数作为参数,并返回一个整数(两个参数的乘积)。当然,这个类将被作为一个 Web 服务公开,而且这个服务将需要被发布到 UDDI。

如果您正在使用 WebSphere SDK for Web Services(WSDK),那么只需下载我已经为您创建的服务器端实现(请参阅 参考资料部分),并遵循下列步骤:

  1. 将已下载的文件解压缩到 WSDK 的 services\applications 目录下。(即 services\applications\WebMath)。
  2. 运行 wsdkconfig 并安装 WebMath。
  3. 启动应用程序服务器。
  4. 在 services\applications\WebMath\client 目录下的命令行中输入 run publish。

这样就可以了!Web 服务被发布到 UDDI 并等待客户机请求。

如果您没有 WebSphere SDK for Web Services,那么您可以免费下载它(请参阅 参考资料部分)。否则,您将需要在您想要使用的任何一个平台上从零开始创建上面描述的 Web 服务。只要确定当这个 Web 服务被发布到 UDDI 时,业务实体为“ Damian Hagge Ltd.”且业务服务为“ WebMathService” 就可以了。如果您这么做了,那么用于本文的包中提供的客户机代码(请参阅 参考资料)应该很好用。


我现在可以谈论有趣的部分了 — 动态发现和调用。我将讨论的代码是 DynamicInvoke.java ,您可以在 下载文件WebMath\client 目录下找到该代码。虽然这个代码稍微有点儿长,但我还是会努力尽可能简练地解释它。

那么,我需要做什么才能动态地发现和调用这个服务?首先,我需要发现来自 UDDI 的关于此服务的信息。其次,我将需要阅读来自服务提供者的 WSDL 实现文件并进行解析,以获取各种信息。最后,我将拥有足够的信息来用 Axis 编入(编码)对 Web 服务实现的 SOAP 请求。

在客户机代码中,这是通过使用开放源码包 uddi4jwsdl4j 和 Apache Axis 实现的。我将用 uddi4j 来浏览 UDDI 注册中心,因为它能提供一个使用户能够查询和发布到任何 UDDI 2.0 注册中心的 API。 wsdl4j 包将被用于以编程方式表示 WSDL 文件的元素,所以我可以浏览和收集来自该文件的各种信息。然后我将用 Axis 向服务器发出真正的 SOAP 请求并等待应答。

在 UDDI 中查找 Web 服务


为了动态地调用 Web 服务,您需要知道四条信息。您必须知道 UDDI 注册中心的查询 URL、UDDI 的发布 URL、业务实体的名称以及业务服务的名称。您可以在 清单 1中找到这些信息。

清单 1. DynamicInvoke.java — 第 38 行

    public class DynamicInvoke {
        private String uddiInquiryURL = "http://localhost:80/uddisoap/inquiryapi";
        private String uddiPublishURL = "http://localhost:80/uddisoap/publishapi";
        private String businessName = "Damian Hagge Ltd.";
        private String serviceName  = "WebMathService";

这段代码说明了 UDDI 的位置、要查找哪个业务实体以及找到了这个业务实体后选择哪个业务服务。我使用 UDDI URL 创建到 UDDI 注册中心的代理,然后我用这个代理对注册中心进行查询以查找业务服务。这是如 清单 2中所示完成的。

清单 2. DynamicInvoke.java — 第 65 行

    // create a proxy to the UDDI
    UDDIProxy proxy = new UDDIProxy(
                     new URL(uddiInquiryURL), new URL(uddiPublishURL));
    // we need to find the business in the UDDI
    // we must first create the Vector of business name
    Vector names = new Vector();
    names.add(new Name(businessName));
    // now get a list of all business matching our search criteria
    BusinessList businessList = 
           proxy.find_business(names, null, null, null, null, null,10);
    // now we need to find the BusinessInfo object for our business
    Vector businessInfoVector  = 
           businessList.getBusinessInfos().getBusinessInfoVector();
    BusinessInfo businessInfo = null;
    for (int i = 0; i < businessInfoVector.size(); i++) {
       businessInfo = (BusinessInfo)businessInfoVector.elementAt(i);
       // make sure we have the right one
       if(businessName.equals(businessInfo.getNameString())) {
          break;
       }
    }

您在此处可以看到我创建了一个 UDDIProxy 。接着,我创建了一个包含所期望的业务名称的 Vector 并将它作为参数传送给了 UDDIProxy.find_business 。这个调用返回了一个包含 BusinessInfos 对象的 BusinessList 对象。 BusinessInfos 仅包含若干个 BusinessInfo 对象的一个向量。这是 uddi4j API 的共性,在这个 API 中,一个对象有到另一个仅包装一个向量的对象的引用。整个客户机代码中将使用这种编程模式,所以在本文接下来的部分中,我不想解释这段代码的语义,它只是查找向量中的一个类,而这个向量包装在一个父对象中。

然后,我在所有的 BusinessInfo 对象中反复查找名为 Damian Hagge Ltd.BusinessInfo 对象。这就确保了我得到了正确的业务实体。

现在我需要查找名为 WebMathService 的业务服务。这是通过搜索 BusinessInfo 对象引用的(虽然是间接地)所有 BusinessService 对象实现的。当我找到名为 WebMathServiceBusinessService 时,我就得到了正确的类。 清单 3中是实现该查找的代码。

清单 3. DynamicInvoke.java — 第 88 行

    // now find the service info
    Vector serviceInfoVector  = 
            businessInfo.getServiceInfos().getServiceInfoVector();
    ServiceInfo serviceInfo = null;
    for (int i = 0; i < serviceInfoVector.size(); i++) {
        serviceInfo = (ServiceInfo)serviceInfoVector.elementAt(i);
        // make sure we have the right one
        if(serviceName.equals(serviceInfo.getNameString())) {
           break;
        }
    }
    // we now need to get the business service object for our service
    // we do this by getting the ServiceDetail object first, and
    // getting the BusinessService objects through it
    ServiceDetail serviceDetail = proxy.get_serviceDetail(
                                           serviceInfo.getServiceKey());
    Vector businessServices = serviceDetail.getBusinessServiceVector();
    BusinessService businessService = null;
    for (int i = 0; i < businessServices.size(); i++) {
    businessService = (BusinessService)businessServices.elementAt(i);
        // make sure we have the right one
        if(serviceName.equals(businessService.getDefaultNameString())) {
            break;
        }
    }

同样,您可以看到一个对象引用另一个对象(它仅包含我正在查找的对象的一个向量)的编程方式的模式。

既然我拥有了业务服务,我就需要以某种方式收集服务提供者机器上的 WSDL 实现的 URI。这将由 UDDI 注册中心中的 tModle 的 Overview URL 元素表示。要查找这个元素,我必须先找到业务服务的绑定模板,它包含了一个基于 http 的访问点。一旦我找到了这个模板,那么我就可以找到由绑定的模板引用的适当的 tModel 了。在 清单 4中,我以编程方式实现了这个查找。

清单 4. DynamicInvoke.java — 第 115 行

    // ok, now we have the business service so we can get the binding template
    Vector bindingTemplateVector = 
            businessService.getBindingTemplates().getBindingTemplateVector();
    AccessPoint accessPoint = null;
    BindingTemplate bindingTemplate = null;
    for(int i=0; i<bindingTemplateVector.size(); i++) {
        // find the binding template with an http access point
        bindingTemplate = (BindingTemplate)bindingTemplateVector.elementAt(i);
        accessPoint = bindingTemplate.getAccessPoint();
        if(accessPoint.getURLType().equals("http")) {
            break;
        }
    }
    // ok now we know which binding template we're dealing with
    // we can now find out the overview URL
    Vector tmodelInstanceInfoVector = 
        bindingTemplate.getTModelInstanceDetails().getTModelInstanceInfoVector();
    String wsdlImplURI = null;
    for(int i=0; i<tmodelInstanceInfoVector.size(); i++) {
        TModelInstanceInfo instanceInfo = 
                (TModelInstanceInfo)tmodelInstanceInfoVector.elementAt(i);
        InstanceDetails details = instanceInfo.getInstanceDetails();
        OverviewDoc wsdlImpl = details.getOverviewDoc();
        wsdlImplURI = wsdlImpl.getOverviewURLString();
        if(wsdlImplURI != null) break;
    }

我在第一个代码块中查看业务服务引用的所有绑定模板,并找到具有“http”URL 类型的绑定模板。然后,我用绑定模板反复进行对 tModel 的所有引用,在找到第一个 overview URL(即 WSDL 实现的位置)时停止。既然我知道 WSDL 实现的 URL,那我就可以继续用 wsdl4j 直接请求来自服务提供者的文档并对其进行解析,从而获得我为了调用服务所需要传送给 Axis 的参数。

请注意:根据您正在使用的遵循 UDDI 2.0 注册中心的实现,tModle 中包含的 overview URL 可能并不指向 WSDL 实现。如果是这种情况,那么您将需要考虑更多事情,比如怎样才能从 UDDI 或 overview URL 指向的位置收集到服务提供者的 WSDL 实现。

解析 WSDL


既然您拥有了用于 Web 服务的 WSDL 实现的 URL,那么您就可以用 wsdl4j 来解析它。为了使用 Axis 调用该服务,您将需要从 WSDL 收集下列信息:

  1. 目标名称空间
  2. 服务名称
  3. 端口名称
  4. 操作名称
  5. 操作输入参数

在我的示例中,为了收集这些信息,我必须首先获取表示 WSDL 实现和 WSDL 接口的 Definition 对象。我的代码如 清单 5所示。

清单 5. DynamicInvoke.java — 第 159 行

 // first get the definition object got the WSDL impl
 try {
     WSDLFactory factory = new WSDLFactoryImpl();
     WSDLReader reader = factory.newWSDLReader();
     implDef = reader.readWSDL(implURI);
 } catch(WSDLException e) {
     e.printStackTrace();
 }
 if(implDef==null) {
        throw new WSDLException(
            WSDLException.OTHER_ERROR,"No WSDL impl definition found.");
 }
 // now get the Definition object for the interface WSDL
 Map imports = implDef.getImports();
 Set s = imports.keySet();
 Iterator it = s.iterator();
 while(it.hasNext()) {
            Object o = it.next();
     Vector intDoc = (Vector)imports.get(o);
     // we want to get the ImportImpl object if it exists
     for(int i=0; i<intDoc.size(); i++) {
  Object obj = intDoc.elementAt(i);
      if(obj instanceof ImportImpl) {
          interfaceDef = ((ImportImpl)obj).getDefinition();
      }
     }
 }

如同您可以看到的,我从细读 UDDI 注册中心时收集到的实现 URL 处获取了 WSDL 实现的一个 Definition 对象。然后,我通过搜索实现 WSDL 中定义的所有 imports 获取了另一个 Definition 对象,它表示 WSDL 接口。一个格式良好的实现 WSDL 应该拥有一个指向其对应的 WSDL 接口的 imports。

现在,查找要被传送给 Axis 的目标名称空间很简单。我只要调用 WSDL 实现对象上的 Definition.getTargetNamespace() 就可以了。接下来,我们要查找包含了我想要调用的操作的端口。 清单 6中实现了这个查找。

清单 6. DynamicInvoke.java — 第 195 行

    // great we've got the WSDL definitions now we need to find the PortType so
 // we can find the methods we can invoke
 Vector allPorts = new Vector();
        Map ports = interfaceDef.getPortTypes();
 s = ports.keySet();
 it = s.iterator();
 while(it.hasNext()) {
        Object o = it.next();
     Object obj = ports.get(o);
     if(obj instanceof PortType) {
      allPorts.add((PortType)obj);
     }
 } 
 // now we've got a vector of all the port types - normally some logic would
 // go here to choose which port type we want to use but we'll just choose 
 // the first one
 PortType port = (PortType)allPorts.elementAt(0);
 List operations = port.getOperations();

我获得了接口 WSDL 中声明的所有端口且只选择第一个端口。当然,如果这是一个真正的应用程序,我就会希望开发某种类型的算法来选择期望的端口和操作。但是,为了简练起见,我不会这样做。

既然我已经选择了要使用的端口,我就要查找我将提供给 Axis 的服务名称和端口名称。我通过在对应于选定的端口的 WSDL 接口中查找绑定来查找服务名称和端口名称。然后,我在 WSDL 实现中收集服务和端口,该实现为这个绑定提供一个端点。

您可能发现这有点儿过于复杂,所以我将简化它。请看一看 WebMath_Impl.wsdl 文件和 WebMath_Interface.wsdl 文件,您可以在 WebMath\webapp 目录下找到这两个文件。您可以在实现文件中看到有一个 <service> 标记,它包含一个 <port> 标记。在接口文件中,您可以看到有一个 <binding> 标记,它包含一个 <operation> 标记和一个表示我已经选择的端口的 <port> 标记。在这两个文件中,可能有这些端口中的几个端口,所以我需要做的就是确保我在实现文件中选择的服务包含引用绑定(它包含在接口文件中选择的端口)的端口。

换言之,我需要确保我在接口 WSDL 中选择的绑定包含我已经选择的同一个端口(在上面的代码片断中)。然后,我需要确保在实现 WSDL 中选择的服务包含一个端口,这个端口的 binding 属性值和接口 WSDL 绑定的 name 属性值相同。如果这些是相同的,那么我就已经找到了引用我选择的端口的服务和绑定。

请在 清单 7中看看上面的算法是什么样的。

清单 7. DynamicInvoke.java — 第 215 行

 // let's get the service in the WSDL impl which contains this port
 // to do this we must first find the QName of the binding with the 
 // port type that corresponds to the port type of our chosen part
 QName bindingQName = null;
 Map bindings = interfaceDef.getBindings();
 s = bindings.keySet();
 it = s.iterator();
 while(it.hasNext()) {
     Binding binding = (Binding)bindings.get(it.next());
     if(binding.getPortType()==port) {
      // we've got our binding
      bindingQName = binding.getQName();
     }
 }
 if(bindingQName==null) {
     throw new WSDLException(WSDLException.OTHER_ERROR,
                        "No binding found for chosen port type.");         
 }
 // now we can find the service in the WSDL impl which provides an 
 // endpoint for the service we just found above
 Map implServices = implDef.getServices();
 s = implServices.keySet();
 it = s.iterator();
 while(it.hasNext()) {
     Service serv = (Service)implServices.get(it.next());
     Map m = serv.getPorts();
     Set set = m.keySet(); 
     Iterator iter = set.iterator();
     while(iter.hasNext()) {
      Port p = (Port)m.get(iter.next());
      if(p.getBinding().getQName().toString().equals(
                                        bindingQName.toString())) {
          // we've got our service store the port name and service name
          portName = serv.getQName().toString();
          serviceName = p.getName();
          break;
         }
     } 
     if(portName != null) break;
 }

通过执行上面的代码,我收集到了需要传送给 Axis 的端口名称和服务名称。现在,我需要的唯一其他的几条信息就是要传送到方法调用中的方法名称和几个参数。

清单 8中的代码将发现那些参数以及这些参数映射的 Java 类型(即 Java 类,或称本机类型)。

清单 8. DynamicInvoke.java — 第 256 行

    // ok now we got all the operations previously - normally we would have some 
    logic here to
 // choose which operation, however, for the sake of simplicity we'll just 
 // choose the first one
 Operation op = (Operation)operations.get(0);
 operationName = op.getName();
 // now let's get the Message object describing the XML for the input and output
 // we don't care about the specific type of the output as we'll just cast it to 
 an Object
 Message inputs = op.getInput().getMessage();
 // let's find the input params 
 Map inputParts = inputs.getParts();
 // create the object array which Axis will use to pass in the parameters
 inputParams = new Object[inputParts.size()];
 s = inputParts.keySet();
 it = s.iterator();
 int i=0;
 while(it.hasNext()) {
     Part part = (Part)inputParts.get(it.next());
     QName qname = part.getTypeName();
     // if it's not in the http://www.w3.org/2001/XMLSchema namespace then
     // we don't know about it - throw an exception
     String namespace = qname.getNamespaceURI();
     if(!namespace.equals("http://www.w3.org/2001/XMLSchema")) {
     throw new WSDLException(
                    WSDLException.OTHER_ERROR,"Namespace unrecognized");
     }
     // now we can get the Java type which the the QName maps to 
     // we do this by using the Axis tools which map WSDL types
            // to Java types in the wsdl2java tool
     String localPart = qname.getLocalPart();
     javax.xml.rpc.namespace.QName wsdlQName = 
                    new javax.xml.rpc.namespace.QName(namespace,localPart);
     TypeMapping tm = DefaultTypeMappingImpl.create();
     Class cl = tm.getClassForQName(wsdlQName);
     // if the Java type is a primitive, we need to wrap it in an object
     if(cl.isPrimitive()) {
      cl = wrapPrimitive(cl); 
     }
     // we could prompt the user to input the param here but we'll just
     // assume a random number between 1 and 10. First we need to 
     // find the constructor which takes a string representation of a number
     // if a complex type was required we would use reflection to break it 
     // down and prompt the user to input values for each member variable
            // in Object representing the complex type
     try {
      Constructor cstr = cl.getConstructor(
                        new Class[] { Class.forName("java.lang.String") });
      inputParams[i] = cstr.newInstance(
                        new Object [] { ""+new Random().nextInt(10) });
     } catch(Exception e) {
      // shoudn't happen
     e.printStackTrace();
     }
     i++;
 }

我来一步步解释所发生的一切。我已经有了一个 Operation 对象,是从我在端口对象对 Port.getOperations() 的调用中获得的。接下来,只需调用 Operation.getName() 就会产生方法名称。

现在,我需要获取表示 <input> 标记的 Message 对象,这个 input 标记由所选的端口引用。 <input> 标记指定几个应该被传送到方法调用中的参数。这是由代码 Operation.getInput().getMessage() 完成的。一旦我收集到了这个 Message 对象,我就需要查找 Message 对象包含的所有部件。

我反复查看所有的部件并确保名称空间是 XML Schema 名称空间,这样我就能确保可以把部件类型映射到 Java 代码。当然,在一个真正的应用程序中,这将被扩展以便尽可能地支持其他类型。

然后,我稍微向 Axis 方向转换一点儿以便发现类型映射到哪个 Java 类上。 Axis 包含一个 TypeMapping.getClassForQName() 方法,用在它的 wsdl2java 工具中。我所需要做的就是创建一个表示类型的 javax.xml.rpc.namespace.QName 对象。一旦我进行了调用,那么我就拥有了一个表示类型的 Java Class 对象。

如果 Class 是基本类型,那么我需要将它包装在一个 Java 对象中。这里没有介绍实现这个包装的代码,因为它太简单了,但是,如果您想要查看此代码,您可以在 DynamicInvoke.java 找到它。当我拥有了要传送给 Axis 的 Java 对象后,我需要对它进行实例化,给构造程序提供一个值,然后存储它。我使用反射(reflection)查找这个对象的构造程序,并提供 1 到 10 之间的一个随机数字对其实例化。当然,如果这不仅仅是一个演示的话,某个其他的逻辑将选择参数,可能会提示用户提供该信息。最后,我将实例化的对象存储在 Object 数组中。

好!我已经收集到了调用服务所需的所有信息。现在我就可以用一个 Axis 调用来调用 Web 服务了。

用 Axis 调用 Web 服务


Axis 调用 Web 服务是一个相对简单的过程, 清单 9中是此过程的编码。

清单 9. DynamicInvoke.java — 第 365 行

    public void axisInvoke(String targetNamespace, String serviceName, 
            String portName, String operationName, Object[] inputParams, 
            String implURI) {
     try {
         // first, due to a funny Axis idiosyncrasy we must strip portName of
         // it's target namespace so we can pass it in as 
            // targetNamespace, localPart
         int index = portName.indexOf(":",
                    portName.indexOf("http://")+new String("http://").length());
         String portNamespace = portName.substring(0,index);
            portName = portName.substring(
                        index==0?index:index+1); // to strip the :
         javax.xml.rpc.namespace.QName serviceQN = 
      new javax.xml.rpc.namespace.QName( portNamespace, portName );
         org.apache.axis.client.Service service = 
      new org.apache.axis.client.Service(new URL(implURI), serviceQN);
         javax.xml.rpc.namespace.QName portQN = 
      new javax.xml.rpc.namespace.QName( targetNamespace, serviceName );
         // This Call object will be used the invocation
         Call call = (Call) service.createCall();
         // Now make the call...
         System.out.println("Invoking service>> " + serviceName + " <<...");
         call.setOperation( portQN, operationName );
         Object ret = (Integer) call.invoke( inputParams );
            System.out.println("Result returned from call to "+
                                            serviceName+" -- "+ret);
     } catch(java.net.MalformedURLException e) {
         System.out.println("Error invoking service : "+e);
     } catch(javax.xml.rpc.ServiceException e2) {
         System.out.println("Error invoking service : "+e2);
     } catch(java.rmi.RemoteException e3) {
         System.out.println("Error invoking service : "+e3);
     }
   }

那么,我现在在做什么呢?首先,我创建了一个表示端口名称的 QNameAxis 不提供从一个完整的名称空间创建 QName 的构造程序,所以我不得不把端口名称分割成名称空间和本地部件。然后,我创建一个 Service 对象(它提供 WSDL 实现的 URL)和 QName (它表示端口名称)。

然后我创建另一个 QName 对象,这一次它表示服务名称。现在我从 Service 对象创建 Call 对象。我通过传入这个 Call 对象要调用的端口 QName 和操作名称来设置它将调用的操作。最后,我实际调用了在解析 WSDL 时创建的、传入 Object[] 参数中的操作。这样就可以了!现在我将返回值作为一个 Java Object ,并把它打印出来。

总结


我希望您已经理解了我在本文中讲述的所有代码。这可能有点儿多,但是我已经说过了:理解 uddi4jwsdl4j 和 Axis API 如何工作对于有效编写定制的 Web 服务客户机是很重要的。现在,您最好是研究研究代码,然后试着找出 UDDI 注册中心或 WSDL 文档中包含的信息。如果您对 UDDI 元素的结构或 WSDL 的结构有些模糊的话,那么请尝试着研读它们。有一些关于这些主题的优秀 参考资料。再重申一遍,如果您对于本文所介绍的任何内容有任何疑问的话,请在 Web Services Technical 论坛上张贴意见。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=SOA and web services
ArticleID=21756
ArticleTitle=动态发现和调用 Web 服务
publish-date=11012002