在这个系列教程的第一部分我们介绍了有关Web Services的基本概念,包括SOAP及WSDL。我们在极短的时间来开发了一个Web Service,在开发过程中我们讲解了SOAP消息、实现java web service客户端及WSDL的结构。在这篇文章中我们将就SOAP的复杂类型、错误处理及远程对象引用等内容做探讨。
[i]提示:[/i]如果你还没有下载用于创建我们示例程序的软件,请参考第一部分安装一节。你还需要下载示例源程序。假设你将下载的包解压缩至c:\wasp_demo目录下。所有例子的java源程序均可在解压缩之后的文件目录的src子目录下找到,它们位于com.systinet.demos包中。除非你具备SOAP及WSDL的开发经验,否则我们建议你先读本系列教程的第一部分。
SOAP及复杂类型
到目录为止,我们开发的web services仅使用简单的数据类型如string、int、doubles。现在让我们来看看复杂数据类型是怎样转化成SOAP消息的。
SOAP协议推荐了所谓的SOAP编码方案将编程语言的复杂类型转化成XML。通常,如下的转化是自动进行的:
- Java 2 的简单类型
- 符合JavaBesna规范的自定义类。所有公有的变量及getters/setters都通过Java内省序列化器来转化成XML。
如下示例演示了JavaBean的序列化及Java 2集合类的序列化。
我们将向这个Web Service传送一个简单的名为OrderRequest数据结构。OrderRequest是一个极为简单的JavaBean,其中包含了对自有变量symbol、limitPrice、volume的赋值及取值方法。这个Web Service的processOrder方法接收OrderReqesut作为其唯一的参数。随后将向你展示怎样在SOAP消息中表示OrderRequest这个数据结构。服务的getOrders方法将服务接收到的所有订单作为一个集合(collection)返回给客户端。在java的类文件里,getOrders方法的返回类型为java.util.Hashtable,随后将介绍这个数据类型在XML中是怎样表示的。
我们继续在股票市场上转悠,现在来实现一个简单的股票交易(买股票)的Web Service。
package com.systinet.demos.mapping;
public class OrderService {
private java.util.HashMap orders = new java.util.HashMap(); public String processOrder(OrderRequest order) { String result = "PROCESSING ORDER"; Long id = new Long(System.currentTimeMillis()); result += "\n----------------------------"; result += "\nID: "+id; result += "\nTYPE: "+ ((order.getType()==order.ORDER_TYPE_SELL)?("SELL"):("BUY")); result += "\nSYMBOL: "+order.getSymbol(); result += "\nLIMIT PRICE: "+order.getLimitPrice(); result += "\nVOLUME: "+order.getVolume(); this.orders.put(id,order); return result; } public java.util.HashMap getOrders() { return this.orders; }
}
Figure 1: Complex types handling example (OrderService.java)
[i]提示[/i]:你可以在示例源码解压缩后的bin目录下找到所有的脚本(scripts)。 执行deployMapping.bat脚本以编译及布署这个买股票的服务。客户端程序简单地创建两个购买请求并将它们发送给web service。然后客户端程序获取一个包含了两个购买请求信息的Hashtable请将它们显示在控制台上。让我们来看一看客户端代码,我们又一次在科技股上投机:
package com.systinet.demos.mapping;
import org.idoox.wasp.Context; import org.idoox.webservice.client.WebServiceLookup;
public final class TradingClient {
public static void main( String[] args ) throws Exception { WebServiceLookup lookup = (WebServiceLookup)Context.getInstance(Context.WEBSERVICE_LOOKUP); OrderServiceProxy service = (OrderServiceProxy)lookup.lookup("http://localhost:6060/MappingService/",OrderServiceProxy.class);
com.systinet.demos.mapping.struct.OrderRequest order = new com.systinet.demos.mapping.struct.OrderRequest(); order.symbol = "SUNW"; order.type = com.systinet.demos.mapping.OrderRequest.ORDER_TYPE_BUY; order.limitPrice = 10; order.volume = 100000; String result = service.processOrder(order); System.out.println(result); order = new com.systinet.demos.mapping.struct.OrderRequest(); order.symbol = "BEAS"; order.type = com.systinet.demos.mapping.OrderRequest.ORDER_TYPE_BUY; order.limitPrice = 13; order.volume = 213000; result = service.processOrder(order); System.out.println(result); java.util.HashMap orders = service.getOrders(); java.util.Iterator iter = orders.keySet().iterator(); while(iter.hasNext()) { Long id = (Long)iter.next(); OrderRequest req = (OrderRequest)orders.get(id); System.out.println("\n----------------------------"); System.out.println("\nID: "+id); System.out.println("\nTYPE: "+ ((req.getType()==com.systinet.demos.mapping.OrderRequest.ORDER_TYPE_SELL)?("SELL"):("BUY"))); System.out.println("\nSYMBOL: "+req.getSymbol()); System.out.println("\nLIMIT PRICE: "+req.getLimitPrice()); System.out.println("\nVOLUME: "+req.getVolume()); } }
}
Figure 2: Ordering client source code (TradingClient.java)
深入研讨复杂数据类型的映射(Complex type mapping)
首先要介绍的是我们发布Web Service时产生的WSDL文件。如果你已经布署了这个mapping service(译者注:买股票服务的服务名),你可以通过如下链接查看其WSDL文件http://localhost:6060/MappingService/.
在这个教程的第一部分我们说过,WSDL描述了一个Web Service提供什么功能(WHAT部分),如何与其交互--如何调用它(HOW部分),以及它所在的地址(WHERE部分)。WSDL提供一个结构化的机制用于描述它所提供的功能、它能处理的消息格式(formats)、它支持的协议及这个Web Service实例所在的地址。在我们的例子中,最值得关注的是OrderRequest这个java类是怎样被映射成XML的:
<xsd:complexType name="OrderRequest"> <xsd:sequence> <xsd:element name="limitPrice" type="xsd:double"/> <xsd:element name="symbol" type="xsd:string"/> <xsd:element name="type" type="xsd:short"/> <xsd:element name="volume" type="xsd:long"/> </xsd:sequence> </xsd:complexType>
可以看到,OrderRequest被映射成一个简单数据类型的集合。从getOrders方法返回的HashMap被映射成从http:///containers:HashMap导入的类型。我们的WSDL文件导入了如下的定义:
<complexType name="HashMap"> <sequence> <element name="item" minOccurs="0" maxOccurs="unbounded"> <complexType> <sequence> <element name="key" type="anyType" /> <element name="value" type="anyType" /> </sequence> </complexType> </element> </sequence> </complexType>
现在让我们来看一下客户端与Web Service交互的SOAP消息。在一个HTTP浏览器中打开管理控制台,按一下刷新按钮,在控制台的MappingService区按一下"Enable"链接。接着,执行runMappingClient.bat脚本以运行客户端程序,请注意交互时的SOAP消息。如下示例了对processOrder方法调用的SOAP消息,其中包含了一个OrderRequest实例参数:
<?xml version="1.0" encoding="UTF-8"?> <ns0:Envelope xmlns:ns0="http://schemas./soap/envelope/"> <ns0:Body ns0:encodingStyle="http://schemas./soap/encoding/" xmlns:xsd="http://www./2001/XMLSchema" xmlns:xsi="http://www./2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas./soap/encoding/"> <ns0:processOrder xmlns:ns0= "http:///wasp/tools/java2wsdl/output/com/systinet/demos/mapping/OrderService"> <p0 xsi:type= "ns1:OrderRequest" xmlns:ns1="http:///wasp/tools/java2wsdl/output/com/systinet/demos/mapping/"> <limitPrice xsi:type="xsd:double">10.0</limitPrice> <symbol xsi:type="xsd:string">SUNW</symbol> <type xsi:type="xsd:short">1</type> <volume xsi:type="xsd:long">100000</volume> </p0> </ns0:processOrder> </ns0:Body> </ns0:Envelope>
下面示例的是getOrders方法返回时的SOAP消息(包含购买请求信息的HashMap):
<?xml version="1.0" encoding="UTF-8"?> <ns0:Envelope xmlns:ns0="http://schemas./soap/envelope/"> <ns0:Body ns0:encodingStyle="http://schemas./soap/encoding/" xmlns:xsd="http://www./2001/XMLSchema" xmlns:xsi="http://www./2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas./soap/encoding/"> <ns0:getOrdersResponse xmlns:ns0= "http:///wasp/tools/java2wsdl/output/com/systinet/demos/mapping/OrderService"> <response xsi:type="ns1:HashMap" xmlns:ns1="http:///containers"> <item> <key xsi:type="xsd:long">1006209071080</key> <value xsi:type= "ns2:com.systinet.demos.mapping.OrderRequest" xmlns:ns2="http:///package/"> <volume xsi:type="xsd:long">100000</volume> <symbol xsi:type="xsd:string">SUNW</symbol> <limitPrice xsi:type="xsd:double">10.0</limitPrice> <type xsi:type="xsd:short">1</type> </value> </item> <item> <key xsi:type="xsd:long">1006209071130</key> <value xsi:type="ns3:com.systinet.demos.mapping.OrderRequest" xmlns:ns3="http:///package/"> <volume xsi:type="xsd:long">213000</volume> <symbol xsi:type="xsd:string">BEAS</symbol> <limitPrice xsi:type="xsd:double">13.0</limitPrice> <type xsi:type="xsd:short">1</type> </value> </item></response> </ns0:getOrdersResponse></ns0:Body> </ns0:Envelope>
Java至XML的映射直接明了。可以看到外层的HashMap元素包含了多个key及value元素。注意到有一个OrderReqeust的数据类型在内部的XML定义中。
最后我们可以运行undeployMapping.bat以解除对刚才这个Web Service的布署。
SOAP错误处理
当服务器遇到错误时,SOAP定义了一个所谓的SOAP Fault的XML结构来代表这个错误。在本教程的第一部分我们简短地介绍过错误消息,现在让我们深入地钻研一下。SOAP Fault包括三个基本的元素(element):
- FAULTCODE 它包含一个错误的编码或ID。
- FAULTSTRING 它包含对错误的简单描述。
- DETAIL 对错误的比较详细的描述。
为了演示错误消息的处理,我们在先前的股票报价的例子中增加一些异常。在getQuote方法中我们提供对三种股票的报价,对于其它的股票,将抛出StockNotFoundException异常:
package com.systinet.demos.fault;
public class StockQuoteService {
public double getQuote(String symbol) throws StockNotFoundException { if(symbol!=null && symbol.equalsIgnoreCase("SUNW")) return 10; if(symbol!=null && symbol.equalsIgnoreCase("MSFT")) return 50; if(symbol!=null && symbol.equalsIgnoreCase("BEAS")) return 11; throw new StockNotFoundException("Stock symbol "+symbol+" not found."); } public java.util.LinkedList getAvailableStocks() { java.util.LinkedList list = new java.util.LinkedList(); list.add("SUNW"); list.add("MSFT"); list.add("BEAS"); return list; }
}
Figure 3: SOAP web service Java source (StockQuoteService.java)
执行deployFault.bat以布署这个web service。在一个HTTP浏览器中打开管理控制台,按一下刷新按钮,在控制台的StockQuoteService区按一下"Enable"链接。
在浏览器中打开http://localhost:6060/StockQuoteService/ 以显示布署时产生的WSDL文件,请注意SOAP Fault消息在WSDL中的定义:
<wsdl: message name=‘StockQuoteService_getQuote_com.systinet.demos.fault.StockNotFoundException_Fault‘> <wsdl:part name=‘idoox-java-mapping.com.systinet.demos.fault.StockNotFoundException‘ type=‘xsd:string‘/> </wsdl:message> 在WSDL的port type元素中,Fault消息是这样被getQuote操作所引用的:
<wsdl:operation name=‘getQuote‘ parameterOrder=‘p0‘> <wsdl:input name=‘getQuote‘ message=‘tns:StockQuoteService_getQuote_Request‘/> <wsdl:output name=‘getQuote‘ message=‘tns:StockQuoteService_getQuote_Response‘/> <wsdl:fault name=‘getQuote_fault1‘ message=‘tns:StockQuoteService_getQuote_com.systinet.demos.fault.StockNotFoundException_Fault‘/> </wsdl:operation>
如下是binding元素的片段:
<wsdl:operation name=‘getQuote‘> <soap:operation soapAction=‘‘ style=‘rpc‘/> <wsdl:input name=‘getQuote‘> <soap:body use=‘encoded‘ encodingStyle=‘http://schemas./soap/encoding/‘ namespace=‘http:///wasp/tools/java2wsdl/output/com/systinet/demos/fault/‘/> </wsdl:input> <wsdl:output name=‘getQuote‘> <soap:body use=‘encoded‘ encodingStyle=‘http://schemas./soap/encoding/‘ namespace=‘http:///wasp/tools/java2wsdl/output/com/systinet/demos/fault/‘/> </wsdl:output> <wsdl:fault name=‘getQuote_fault1‘> <soap:fault name=‘getQuote_fault1‘ use=‘encoded‘ encodingStyle=‘http://schemas./soap/encoding/‘ namespace=‘http:///wasp/tools/java2wsdl/output/com/systinet/demos/fault/‘/> </wsdl:fault> </wsdl:operation>
看得出来,当一个服务器端错误产生时,这个错误被映射成一个简单的SOAP消息,然后返回给客户端。
接下来让我们创建一个简单的web service客户程序:
package com.systinet.demos.fault;
import org.idoox.wasp.Context; import org.idoox.webservice.client.WebServiceLookup;
public final class StockClient {
public static void main( String[] args ) throws Exception { // lookup service WebServiceLookup lookup = (WebServiceLookup)Context.getInstance(Context.WEBSERVICE_LOOKUP); // bind to StockQuoteService StockQuoteServiceProxy quoteService = (StockQuoteServiceProxy)lookup.lookup( "http://localhost:6060/StockQuoteService/", StockQuoteServiceProxy.class );
// use StockQuoteService System.out.println("Getting available stocks"); System.out.println("------------------------"); java.util.LinkedList list = quoteService.getAvailableStocks(); java.util.Iterator iter = list.iterator(); while(iter.hasNext()) { System.out.println(iter.next()); } System.out.println(""); System.out.println("Getting SUNW quote"); System.out.println("------------------------"); System.out.println("SUNW "+quoteService.getQuote("SUNW")); System.out.println(""); System.out.println("Getting IBM quote (warning, this one doesn‘t exist, so we will get an exception)"); System.out.println("------------------------"); System.out.println("SUNW "+quoteService.getQuote("IBM")); System.out.println("");
}
}
Figure 4: SOAP client Java source (StockClient.java)
我们需要产生客户端的Java 接口,编译这些java类,然后运行客户端程序。所有这些工作都包含在runFaultClient.bat脚本里。
我们的股票报价系统所含的股票种类不多,它不包含IBM。执行客户端程序里,客户端将首先显示所有可获取股价的股票名,然后获取SUNW的股票价格,当想获得IBM的股票价格时,将抛出一个StockNotFound异常说“Stock symbol IBM not found"。请打开管理控制台,点击show SOAP conversation链接,一个新窗口被打开,显示如下的消息(高亮显示的是重要的消息部分):
==== INPUT ==== http://localhost:6060/StockQuoteService/ ==== 11/14/01 4:44 PM = <?xml version="1.0" encoding="UTF-8"?> <ns0:Envelope xmlns:ns0="http://schemas./soap/envelope/"> <ns0:Body ns0:encodingStyle="http://schemas./soap/encoding/" xmlns:xsd="http://www./2001/XMLSchema" xmlns:xsi="http://www./2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas./soap/encoding/"> [i] <ns0:getQuote xmlns:ns0="http:///wasp/tools/java2wsdl/output/com/systinet/demos/fault/"> <p0 xsi:type="xsd:string">IBM</p0> </ns0:getQuote>[/i] </ns0:Body> </ns0:Envelope> ==== CLOSE =====================================================================
==== OUTPUT ==== http://localhost:6060/StockQuoteService/ ====================== <?xml version="1.0" encoding="UTF-8"?> <ns0:Envelope xmlns:ns0="http://schemas./soap/envelope/"> <ns0:Body ns0:encodingStyle="http://schemas./soap/encoding/" xmlns:xsd="http://www./2001/XMLSchema" xmlns:xsi="http://www./2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas./soap/encoding/"> [i] <ns0:Fault xmlns:ns0="http://schemas./soap/envelope/"> <faultcode>ns0:Server</faultcode> <faultstring>Stock symbol IBM not found.</faultstring> <detail xmlns:ijm="urn:idoox-java-mapping"> <ijm:idoox-java-mapping.com.systinet.demos.fault.StockNotFoundException> <ijm:stack-trace> com.systinet.demos.fault.StockNotFoundException: Stock symbol IBM not found. at com.systinet.demos.fault.StockQuoteService.getQuote(StockQuoteService.java:24) at java.lang.reflect.Method.invoke(Native Method) at com.idoox.wasp.server.adaptor.JavaAdaptorInvoker.invokeService(JavaAdaptorInvoker.java:387) at com.idoox.wasp.server.adaptor.JavaAdaptorInvoker.invoke(JavaAdaptorInvoker.java:239) at com.idoox.wasp.server.adaptor.JavaAdaptorImpl.dispatch(JavaAdaptorImpl.java:164) at com.idoox.wasp.server.AdaptorTemplate.dispatch(AdaptorTemplate.java:178) at com.idoox.wasp.server.ServiceConnector.dispatch(ServiceConnector.java:217) at com.idoox.wasp.server.ServiceManager.dispatch(ServiceManager.java:231) at com.idoox.wasp.server.ServiceManager$DispatcherConnHandler.handlePost(ServiceManager.java:1359) at com.idoox.transport.http.server.Jetty$WaspHttpHandler.handle(Jetty.java:94) at com.mortbay.HTTP.HandlerContext.handle(HandlerContext.java:1087) at com.mortbay.HTTP.HttpServer.service(HttpServer.java:675) at com.mortbay.HTTP.HttpConnection.service(HttpConnection.java:457) at com.mortbay.HTTP.HttpConnection.handle(HttpConnection.java:317) at com.mortbay.HTTP.SocketListener.handleConnection(SocketListener.java:99) at com.mortbay.Util.ThreadedServer.handle(ThreadedServer.java:254) at com.mortbay.Util.ThreadPool$PoolThreadRunnable.run(ThreadPool.java:601) at java.lang.Thread.run(Thread.java:484) </ijm:stack-trace> </ijm:idoox-java-mapping.com.systinet.demos.fault.StockNotFoundException> </detail> </ns0:Fault>[/i] </ns0:Body> </ns0:Envelope> ==== CLOSE =====================================================================
请注意其中的FAULT结构。FAULTCODE包含所产生的错误编码,FAULTSTRING元素携带了这个异常消息,而DETAIL元素包含在栈中跟踪到的异常。所有的SOAP错误消息都遵从这种基本的格式。
最后,执行updeployFault.bat以解除刚才服务的布署。
远程引用
远程引用是一种用于许多分布式对象系统中的结构,如RMI、CORBA及DCOM。假定你有一个调用服务器端对象的客户程序。下面解释它们是如何工作的。假设服务器端对象创建一个新的对象,且它需要将这个对象传给远程的客户端(如Figure 5 所示)。它可以选择传值(by value)或传引用(by reference)的方式来传递这个对象。如果选择传值传递,需要将整个对象传过去;如果是传引用传递,则整整是传递了指向这个对象的指针。远程引用是工作在网络环境下的引用。远程引用在许多分布式设计模式中受到批判,特别是工厂模式(Facotry pattern)中。因为这个特性与许多分布式计算应用相矛盾,不是所有的SOAP实现支持这个它。
现在让我们来看一个远程引用的例子。在一个Order Web Service中定义一个createLineItem方法。这个方法用于创建一个新的LineItem对象,这个对象包含所购产品的类别、产品价格及购买数量的信息。Order Web Service包含许多LineItem对象的引用。LineItem对象需要返回给客户端程序给供客户端获取信息使用。
 Figure 5: Remote references
实现简单的远程引用
我们将创建一个新的例子以演示远程引用特性。我们用从股市上赚的钱来买一些商品。首先定义两个接口:Order及LineItem。客户端将使用这两个接口来引用远程对象:
package com.systinet.demos.interref;
public interface LineItem extends java.rmi.Remote {
public String getID(); public long getCount(); public String getProductID(); public void close();
}
Figure 6: LineItem interface
package com.systinet.demos.interref;
public interface Order {
public LineItem addItem(String productID, long count); public LineItem getItem(String id); public void removeItem(String id); }
Figure 7: Order interface
注意到LineItem接口继承至java.rmi.Remote接口。这是在WASP中操作远程引用的最简单方法。除此之外,IineItem接口是非常好懂的。Order接口的addItem方法创建一个新的购买项(order item)并将其返回。getItem返回一个已存在的项目(item)而removeItem则从买单中删除一个指定的项目(item)。
现在让我们来实现这两个接口:
package com.systinet.demos.interref;
import org.idoox.webservice.server.WebServiceContext; import org.idoox.webservice.server.LifeCycleService;
public class LineItemImpl implements LineItem {
private String pid; private String id; private long count; public LineItemImpl(String pid, long count) { System.err.println("Creating new LineItem."); this.id = pid+System.currentTimeMillis(); this.pid = pid; this.count = count; } public void close() { System.err.println("close()"); WebServiceContext context = WebServiceContext.getInstance(); LifeCycleService lc = context.getLifeCycleService(); lc.disposeServiceInstance(this); } public long getCount() { System.err.println("getCount()"); return this.count; } public String getProductID() { System.err.println("getProductID()"); return this.pid; } public String getID() { System.err.println("getID()"); return this.id; } }
Figure 8: LineItem implementation
package com.systinet.demos.interref;
public class OrderImpl implements Order {
private java.util.HashMap items = new java.util.HashMap();
public LineItem getItem(String id) { return (LineItem)this.items.get(id); }
public LineItem addItem(java.lang.String pid, long count) { LineItem item = new LineItemImpl(pid, count); this.items.put(item.getID(), item); return item; }
public void removeItem(java.lang.String id) { LineItem item = (LineItem)this.items.remove(id); item.close(); }
}
Figure 9: Order implementation
执行deployInterref.bat以布署这个web service。
这是标准的实现。客户端代码也是很标准的实现法:
package com.systinet.demos.interref;
import javax.wsdl.QName;
import org.idoox.wasp.Context; import org.idoox.webservice.client.WebServiceLookup;
public final class OrderClient {
public static void main( String[] args ) throws Exception { // lookup service WebServiceLookup lookup = (WebServiceLookup)Context.getInstance(Context.WEBSERVICE_LOOKUP); Order order = (Order)lookup.lookup("http://localhost:6060/OrderService/", new QName("http:///wasp/tools/java2wsdl/output/com/systinet/demos/interref/", "OrderService"), "OrderImpl", Order.class); String id1 = order.addItem("THNKPDT23", 2).getID(); String id2 = order.addItem("THNKPDT22", 2).getID(); System.out.println("ID1 "+id1); System.out.println("ID2 "+id2); LineItem item = order.getItem(id1); System.out.println("Line ITEM"); System.out.println("---------"); System.out.println("ID: "+item.getID()); System.out.println("Product ID: "+item.getProductID()); System.out.println("Count: "+item.getCount()); item = order.getItem(id2); System.out.println("Line ITEM"); System.out.println("---------"); System.out.println("ID: "+item.getID()); System.out.println("Product ID: "+item.getProductID()); System.out.println("Count: "+item.getCount());
}
}
Figure 10: Order client
这个简单的客户端程序创建了购买服务(ordering web service)的动态代理(proxy),然后创建两个买单项(order item):THNKPDT23及THNKPDT22。两个买单项都在服务端动态地创建,客户端只是获得他们的引用(reference)。这是一个我们先前提到的工厂模式的蛮好的例子。在我们的例子中,购买服务(ordering service)充当了买单项(order item)的工厂。
请注意买单项(line item)是有状态的(stateful),因为它们保存有买单项数据。
删除远程引用
不像无状态的web service,有状态的web service需要特别的代码以钝化。在我们的例子中我们使用一个特定的清除器。我们调用LifeCycle这个系统服务的disposeServiceInstance方法。请看如下的代码:
public void close() { System.err.println("close()"); WebServiceContext context = WebServiceContext.getInstance(); LifeCycleService lc = context.getLifeCycleService(); lc.disposeServiceInstance(this); }
最后执行undeployInterref.bat以解除我们刚才这个web service的布署。
下一步做什么?
在这部分我们研究了一下SOAP的复杂类型、SOAP错误消息以及远程对象引用。现在我们已经很好地理解了SOAP、WSDL及创建与使用web service的过程。我们希望能使这些东西显示简单易懂。在第三部分,我们将关注于web service的安全问题。
同时我们非常欢迎各种反馈、评论及意见。请联系: tutorial@systinet.com .
(原文地址:http://www./resources/article.jsp?l=Systinet-web-services-part-2)
|