原文作者: 陳亞強

原文鏈接:http://www.ibm.com/developerworks/cn/webservices/ws-handler/index.html
高級軟件工程師北京華園天一科技有限公司 2003  8  

一、Handler的基本概念

J2EE Web 
服務(wù)中的Handler技術(shù)特點非常像Servlet技術(shù)中的Filter。我們知道,在Servlet中,當(dāng)一個HTTP到達(dá)服務(wù)端時,往往要經(jīng)過多個Filter對請求進(jìn)行過濾,然后才到達(dá)提供服務(wù)的Servlet,這些Filter的功能往往是對請求進(jìn)行統(tǒng)一編碼,對用戶進(jìn)行認(rèn)證,把用戶的訪問寫入系統(tǒng)日志等。相應(yīng)的,Web服務(wù)中的Handler通常也提供一下的功能: 

對客戶端進(jìn)行認(rèn)證、授權(quán); 
把用戶的訪問寫入系統(tǒng)日志; 
對請求的SOAP消息進(jìn)行加密,解密; 
Web Services對象做緩存。 
SOAP
消息Handler能夠訪問代表RPC請求或者響應(yīng)的SOAP消息。在JAX-RPC技術(shù)中,SOAP消息Handler可以部署在服務(wù)端,也可以在客戶端使用。 

下面我們來看一個典型的SOAP消息Handler處理順序: 
某個在線支付服務(wù)需要防止非授權(quán)的用戶訪問或者撰改服務(wù)端和客戶端傳輸?shù)男畔ⅲ瑥亩褂孟⒄?/span>Message Digest)的方法對請求和響應(yīng)的SOAP消息進(jìn)行加密。當(dāng)客戶端發(fā)送SOAP?Ф?andler把請求消息中的某些敏感的信息(如信用卡密碼)進(jìn)行加密,然后把加密后的SOAP消息傳輸?shù)椒?wù)端;服務(wù)端的SOAP消息Handler截取客戶端的請求,把請求的SOAP 消息進(jìn)行解密,然后把解密后的SOAP消息派發(fā)到目標(biāo)的Web服務(wù)端點。 

Apache axis
是我們當(dāng)前開發(fā)Web服務(wù)的較好的選擇,使用axisWeb服務(wù)開發(fā)工具,可以使用Handler來對服務(wù)端的請求和響應(yīng)進(jìn)行處理。典型的情況下,請求傳遞如圖1所示。 



1 SOAP消息的傳遞順序


在圖中,軸心點(pivot point)是Apache與提供程序功能相當(dāng)?shù)牟糠郑ㄟ^它來和目標(biāo)的Web服務(wù)進(jìn)行交互,它通常稱為Provideraxis中常用的ProviderJavaRPCjavaMSGjavaEJB。一個Web服務(wù)可以部署一個或者多個Handler 

Apache axis
中的Handler體系結(jié)構(gòu)和JAX-RPC 1.0JSR101)中的體系結(jié)構(gòu)稍有不同,需要聲明的是,本文的代碼在axis中開發(fā),故需要在axis環(huán)境下運行。 

axis環(huán)境下,SOAP消息Handler必須實現(xiàn)org.apache.axis.Handler接口(在JAX-RPC 1.0規(guī)范中,SOAP消息Handler必須實現(xiàn)javax.xml.rpc.handler.Handler接口),org.apache.axis.Handler接口的部分代碼如下: 

例程1 org.apache.axis.Handle的部分代碼

public interface Handler extends Serializable {
    
public void init();  
    
public void cleanup();
    
public void invoke(MessageContext msgContext) throws AxisFault ;

    
public void onFault(MessageContext msgContext);
    
public void setOption(String name, Object value);    
    
public Object getOption(String name);
   
    
public void setName(String name);   
    
public String getName();     
    
public Element getDeploymentData(Document doc);
    
public void generateWSDL(MessageContext msgContext) throws AxisFault;
   …
}

 
為了提供開發(fā)的方便,在編寫Handler時,只要繼承org.apache.axis.handlers. BasicHandler即可,BasicHandlerHandler的一個模板,我們看它的部分代碼: 

例程2 BasicHandler的部分代碼

public abstract class BasicHandler implements Handler {
    
protected static Log log =
        LogFactory.getLog(BasicHandler.
class.getName());
    
protected Hashtable options;
    
protected String name;
    
//這個方法必須在Handler中實現(xiàn)。
public abstract void invoke(MessageContext msgContext) throws AxisFault;
public void setOption(String name, Object value) {
        
if ( options == null ) initHashtable();
        options.put( name, value );
    }

}
 

BasicHandler
中的(MessageContext msgContext)方法是Handler實現(xiàn)類必須實現(xiàn)的方法,它通過MessageContext來獲得請求或者響應(yīng)的SOAPMessage對象,然后對SOAPMessage進(jìn)行處理。 

在介紹Handler的開發(fā)之前,我們先來看一下目標(biāo)Web服務(wù)的端點實現(xiàn)類的代碼,如例程3所示。 

例程目標(biāo)Web服務(wù)的端點實現(xiàn)類

package com.hellking.webservice;
public class HandleredService 
{
 
//一個簡單的Web服務(wù)
 public String publicMethod(String name)
 {
  
return "Hello!"+name;
 }
}
//另一個Web服務(wù)端點:
package com.hellking.webservice;
public class OrderService 
{
       
//web服務(wù)方法:獲得客戶端的訂單信息,并且對訂單信息進(jìn)行對應(yīng)的處理,
通常情況是把訂單的信息寫入數(shù)據(jù)庫,然后可客戶端返回確認(rèn)信息。
 
public String orderProduct(String name,String address,String item,int quantity,Card card)
 {
  String cardId
=card.getCardId();
  String cardType
=card.getCardType();
  String password
=card.getPassword();
  String rderInfo
="name="+name+",address="+address+",item="+item+",quantity="+quantity+"
,cardId="+cardId+",cardType="+cardType+",password="+password;
  System.out.println("這里是客戶端發(fā)送來的信息:");
  System.out.println(orderInfo);  
  
return orderInfo;
 } 
}

 
二、下面我們分不同情況討論Handler的使用實例。

使用Handler為系統(tǒng)做日志

Handler
為系統(tǒng)做日志是一種比較常見而且簡單的使用方式。和Servlet中的Filter一樣,我們可以使用Handler來把用戶的訪問寫入系統(tǒng)日志。下面我們來看日志Handler的具體代碼,如例程4所示。 

例程4 LogHandler的代碼

package com.hellking.webservice;

import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.Date;

import org.apache.axis.AxisFault;
import org.apache.axis.Handler;
import org.apache.axis.MessageContext;
import org.apache.axis.handlers.BasicHandler;

public class LogHandler extends BasicHandler {
  
   
/**invoke,每一個handler都必須實現(xiàn)的方法。
  
*/
    
public void invoke(MessageContext msgContext) throws AxisFault
    {
       
//每當(dāng)web服務(wù)被調(diào)用,都記錄到log中。
        try {
            Handler handler 
= msgContext.getService();
            String filename 
= (String)getOption("filename");
            
if ((filename == null|| (filename.equals("")))
                
throw new AxisFault("Server.NoLogFile",
                                 
"No log file configured for the LogHandler!",
                                    
nullnull);
            FileOutputStream fos 
= new FileOutputStream(filename, true);            
            PrintWriter writer 
= new PrintWriter(fos);            
            Integer counter 
= (Integer)handler.getOption("accesses");
            
if (counter == null)
                counter 
= new Integer(0);
            
            counter 
= new Integer(counter.intValue() + 1);            
            Date date 
= new Date();
            msgContext.getMessage().writeTo(System.out);
           
            String result 
= ""+date + ": Web 服務(wù) " +
                            msgContext.getTargetService() 
+
                            
" 被調(diào)用,現(xiàn)在已經(jīng)共調(diào)用了 " + counter + " 次.";
            handler.setOption(
"accesses", counter);            
            writer.println(result);            
            writer.close();
        } 
catch (Exception e) {
            
throw AxisFault.makeFault(e);
        }
    }
}
 

前面我們說過,Handler實現(xiàn)類必須實現(xiàn)invoke方法,invoke方法是Handler處理其業(yè)務(wù)的入口點。LogHandler的主要功能是把客戶端訪問的Web服務(wù)的名稱和訪問時間、訪問的次數(shù)記錄到一個日志文件中。 

下面部署這個前面開發(fā)的Web服務(wù)對像,然后為Web服務(wù)指定Handler。編輯Axis_Home/WEB-INF/ server-config.wsdd文件,在其中加入以下的內(nèi)容:
<service name="HandleredService" provider="java:RPC">
  
<parameter name="allowedMethods" value="*"/>
  
<parameter name="className" value="com.hellking.webservice.HandleredService"/>
  
<parameter name="allowedRoles" value="chen"/>
  
<beanMapping languageSpecificType="java:com.hellking.webservice.Card"
 qname
="card:card" xmlns:card="card"/>
  
<requestFlow>
<handler name="logging" type="java:com.hellking.webservice.LogHandler">
  
<parameter name="filename" value="c:\\MyService.log"/>
 
</handler>
  
</requestFlow>
 
</service>

 



</globalConfiguration>

  
<handler name="logging" type="java:com.hellking.webservice.LogHandler">
  
<parameter name="filename" value="c:\\MyService.log"/>
 
</handler>

<service name="HandleredService" provider="java:RPC">

  
<requestFlow>
  
<handler type="logging"/>
   …
<!--在這里可以指定多個Handler-->
  
</requestFlow>
 
</service>


http://127.0.0.1:8080/handler/services/HandleredService?wsdl&method=publicMethod&name=chen
注意:這個URL需要根據(jù)具體情況改變。
Sun Jul 06 22:42:03 CST 2003: Web 服務(wù) HandleredService 被調(diào)用,現(xiàn)在已經(jīng)共調(diào)用了 1 .
Sun Jul 06 22:42:06 CST 2003: Web 服務(wù) HandleredService 被調(diào)用,現(xiàn)在已經(jīng)共調(diào)用了 2 .
Sun Jul 06 22:42:13 CST 2003: Web 服務(wù) HandleredService 被調(diào)用,現(xiàn)在已經(jīng)共調(diào)用了 3 .

使用Handler對用戶的訪問認(rèn)證

使用Handler為用戶訪問認(rèn)證也是它的典型使用,通過它,可以減少在Web服務(wù)端代碼中認(rèn)證的麻煩,同時可以在部署描述符中靈活改變用戶的訪問權(quán)限。 

對用戶認(rèn)證的Handler代碼如下:

例程認(rèn)證的Handler
package com.hellking.webservice;
import….

//此handler的目的是對用戶認(rèn)證,只有認(rèn)證的用戶才能訪問目標(biāo)服務(wù)。
public class AuthenticationHandler extends BasicHandler
{
 
/**invoke,每一個handler都必須實現(xiàn)的方法。
  
*/
 
public void invoke(MessageContext msgContext)throws AxisFault
 {  
        SecurityProvider provider 
= (SecurityProvider)msgContext.getProperty("securityProvider");
  
if(provider==null)
  {
   provider
= new SimpleSecurityProvider();
             msgContext.setProperty(
"securityProvider", provider);
         }
        
if(provider!=null)
        {         
         String userId
=msgContext.getUsername();
         String password
=msgContext.getPassword();
         
         
//對用戶進(jìn)行認(rèn)證,如果authUser==null,表示沒有通過認(rèn)證,
拋出Server.Unauthenticated異常。
            org.apache.axis.security.AuthenticatedUser authUser 
= provider.authenticate(msgContext);
            
if(authUser==null)
              
throw new AxisFault("Server.Unauthenticated"
Messages.getMessage(
"cantAuth01", userId), null,null);
            
//用戶通過認(rèn)證,把用戶的設(shè)置成認(rèn)證了的用戶。
            msgContext.setProperty("authenticatedUser", authUser);
        } 
    }
}


AuthenticationHandler代碼里,它從MessageContext中獲得用戶信息,然后進(jìn)行認(rèn)證,如果認(rèn)證成功,那么就使用msgContext.setProperty("authenticatedUser", authUser)方法把用戶設(shè)置成認(rèn)證了的用戶,如果認(rèn)證不成功,那么就拋出Server.Unauthenticated異常。 

部署這個Handler,同樣,在server-config里加入以下的內(nèi)容:


<handler name="authen" type="java:com.hellking.webservice.AuthenticationHandler"/>

<service name="HandleredService" provider="java:RPC">
<parameter name="allowedRoles" value="chen"/>

</service>
 

WEB-INF/users.lst
文件中加入以下用戶:

hellking hellking
chen chen

http://127.0.0.1:8080/handler/services/HandleredService?wsdl&method=publicMethod&name=chen

將會提示輸入用戶名和密碼,如圖2所示。

<!--[if !vml]--><!--[endif]-->
 

訪問web服務(wù)時的驗證 

如果客戶端是應(yīng)用程序,那么可以這樣在客戶端設(shè)置用戶名和密碼:

例程在客戶端設(shè)置用戶名和密碼

http://127.0.0.1:808
     
String endpointURL = "http://127.0.0.1:8080/handler/services/HandleredService?wsdl";            
            Service  service 
= new Service();
            Call     call    
= (Call) service.createCall();
            call.setTargetEndpointAddress( 
new java.net.URL(endpointURL) );
            call.setOperationName( 
new
 QName(
"HandleredService""orderProduct") );//設(shè)置操作的名稱。
            
//由于需要認(rèn)證,故需要設(shè)置調(diào)用的用戶名和密碼。
            call.getMessageContext().setUsername("chen");
            call.getMessageContext().setPassword(
"chen");  
 

使用Handler對用戶的訪問授權(quán)

對于已經(jīng)認(rèn)證了的用戶,有時在他們操作某個特定的服務(wù)時,還需要進(jìn)行授權(quán),只有授權(quán)的用戶才能繼續(xù)進(jìn)行操作。我們看對用戶進(jìn)行授權(quán)的Handler的代碼。 

例程對用戶進(jìn)行授權(quán)的代碼

package com.hellking.webservice;

import

//此handler的目的是對認(rèn)證的用戶授權(quán),只有授權(quán)的用戶才能訪問目標(biāo)服務(wù)。
public class AuthorizationHandler extends BasicHandler
{
 
/**invoke,每一個handler都必須實現(xiàn)的方法。
  
*/
 
public void invoke(MessageContext msgContext)
        
throws AxisFault
    {
      
        AuthenticatedUser user 
= (AuthenticatedUser)msgContext.getProperty("authenticatedUser");
        
if(user == null)
            
throw new AxisFault("Server.NoUser", Messages.getMessage("needUser00"), nullnull);
        String userId 
= user.getName();
        Handler serviceHandler 
= msgContext.getService();
        
if(serviceHandler == null)
            
throw new AxisFault(Messages.getMessage("needService00"));
        String serviceName 
= serviceHandler.getName();
        String allowedRoles 
= (String)serviceHandler.getOption("allowedRoles");
        
if(allowedRoles == null)
        {          
            
return;
        }
        SecurityProvider provider 
= (SecurityProvider)msgContext.getProperty("securityProvider");
        
if(provider == null)
            
throw new AxisFault(Messages.getMessage("noSecurity00"));
        
for(StringTokenizer st = new StringTokenizer(allowedRoles, ","); st.hasMoreTokens();)
        {
            String thisRole 
= st.nextToken();
            
if(provider.userMatches(user, thisRole))
            {
                
return;//訪問授權(quán)通過。
            }
        }
        
//沒有通過授權(quán),不能訪問目標(biāo)服務(wù),拋出Server.Unauthorized異常。
        throw new AxisFault("Server.Unauthorized"
Messages.getMessage(
"cantAuth02", userId, serviceName), nullnull);
    }     
}


service-config.wsdd文件中,我們?yōu)?/span>Web服務(wù)指定了以下的用戶:

<parameter name="allowedRoles" value="chen,hellking"/>


provider.userMatches(user, thisRole)
將匹配允許訪問Web服務(wù)的用戶,如果匹配成功,那么授權(quán)通過,如果沒有授權(quán)成功,那么拋出Server.Unauthorized異常。 

使用HandlerSOAP消息進(jìn)行加密、解密

由于SOAP消息在HTTP協(xié)議中傳輸,而HTTP協(xié)議的安全度是比較低的,怎么保證信息安全到達(dá)對方而不泄漏或中途被撰改,將是Web服務(wù)必須解決的問題。圍繞Web服務(wù)的安全,有很多相關(guān)的技術(shù),比如WS-SecurityWS-Trace等,另外,還有以下相關(guān)技術(shù): 

XML Digital Signature
XML數(shù)字簽名) 
XML Encryption 
XML加密) 
XKMS (XML Key Management Specification) 
XACML (eXtensible Access Control Markup Language) 
SAML (Secure Assertion Markup Language) 
ebXML Message Service Security 
Identity Management & Liberty Project 
不管使用什么技術(shù),要使信息安全到達(dá)對方,必須把它進(jìn)行加密,然后在對方收到信息后解密。為了提供開發(fā)的方便,可以使用Handler技術(shù),在客戶端發(fā)送信息前,使用客戶端的HandlerSOAP消息中的關(guān)鍵信息進(jìn)行加密;在服務(wù)端接收到消息后,有相應(yīng)的Handler把消息進(jìn)行解密,然后才把SOAP消息派發(fā)到目標(biāo)服務(wù)。 

下面我們來看一個具體的例子。加入使用SOAP消息發(fā)送訂單的信息,訂單的信息如下:

例程要發(fā)送的訂單SOAP消息

<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
    
<soap-env:Header/>
    
<soapenv:Body>
   
<ns1:orderProduct soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encod
 ing/" xmlns:ns1="HandleredService">
    <arg0 xsi:type="xsd:string">hellking</arg0>
    
<arg1 xsi:type="xsd:string">beijing</arg1>
    
<arg2 xsi:type="xsd:string">music-100</arg2>
    
<arg3 xsi:type="xsd:int">10</arg3>
    
<arg4 href="#id0"/>
   
</ns1:orderProduct>
   
<multiRef id="id0" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmls
 oap.org/soap/encoding/" xsi:type="ns2:card" xmlns:soapenc="http://schemas.xmlsoa
 p.org/soap/encoding/" xmlns:ns2="card">
    
        
<cardId xsi:type="xsd:string">234230572</cardId>
                
        
<cardType xsi:type="xsd:string">visa</cardType>
                
        
<password xsi:type="xsd:string">234kdsjf</password>
   
</multiRef>
  
</soapenv:Body>
   
</soap-env:Envelope>
     

上面的黑體字是傳輸?shù)拿舾行畔ⅲ市枰用堋N覀兛梢允褂?/span>Message Digest之類的方法進(jìn)行加密。加密之后的信息結(jié)構(gòu)如下: 

例程SOAP消息某些部分加密

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope …
<soapenv:Body>
  
<ns1:orderProduct …>
   …
   
<arg4 href="#id0"/>
  
</ns1:orderProduct>
  
<multiRef …>
   
<ns3:EncryptedData xmlns:ns3="http://www.w3.org/2000/11/temp-xmlenc">
    
<ns3:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
    
<ns3:DigestValue>rO0ABXQAkyA8Y2FyZ…….
</ns3:DigestValue>
   
</ns3:EncryptedData>
  
</multiRef>
 
</soapenv:Body>
</soapenv:Envelope>

 

3是使用HandlerSOAP消息進(jìn)行加密、解密后,SOAP消息在傳遞過程中結(jié)構(gòu)的改變。 

<!--[if !vml]--><!--[endif]-->
3 SOAP消息的加密和解密 

從上圖可以看出,通過使用加密、解密的Handler,可以確保消息的安全傳遞。進(jìn)一步說,如果把這種Handler做成通用的組件,那么就可以靈活地部署到不同的服務(wù)端和客戶端。 

客戶端的Handler的功能是把SOAP消息使用一定的規(guī)則加密,假如使用Message Digest加密方式,那么可以這樣對敏感的信息加密: 

例程10 SOAP消息的敏感部分加密

         SOAPElement ele
= soapBodyElement.addChildElement(envelope.createName
(
"EncryptedData","","http://www.w3.org/2000/11/temp-xmlenc")); 
ele.addChildElement(
"DigestMethod").addAttribute(envelope.createName
(
"Algorithm"),"http://www.w3.org/2000/09/xmldsig#sha1");
   
   
byte[] digest=new byte[100];
   ByteArrayOutputStream  out
=new  ByteArrayOutputStream (100);
   MessageDigest md 
= MessageDigest.getInstance("SHA");
   ObjectOutputStream oos 
= new ObjectOutputStream(out);
   
//要加密的信息
    String data = " <cardId xsi:type='xsd:string'>234230572
                        </cardId><cardType xsi:type='xsd:string'>visa</cardType>
                        
<password     xsi:type='xsd:string'>234kdsjf</password>";

   
byte buf[] = data.getBytes();
   md.update(buf);
   oos.writeObject(data);
   oos.writeObject(md.digest());  
   digest
=out.toByteArray();
   out.close();      
      ele.addChildElement(
"DigestValue").addTextNode(new 
sun.misc.BASE64Encoder().encode(digest));
//對加密的信息編碼


在客戶端發(fā)送出SOAP消息時,客戶端的Handler攔截發(fā)送的SOAP消息,然后對它們進(jìn)行加密,最后把加密的信息傳送到服務(wù)端。

服務(wù)端接收到加密的信息后,解密的Handler會把對應(yīng)的加密信息解密。服務(wù)端Handler代碼如例程11所示。 

例程11 服務(wù)端解密Handler

package com.hellking.webservice;
import
//此handler的目的是把加密的SOAP消息解密成目標(biāo)服務(wù)可以使用的SOAP消息。
public class MessageDigestHandler extends BasicHandler
{
 
/**invoke,每一個handler都必須實現(xiàn)的方法。
  
*/
 
public void invoke(MessageContext msgContext)throws AxisFault
 {
  
try
  {   
   
//從messageContext例取得SOAPMessage對象。
   SOAPMessage msg=msgContext.getMessage();
   SOAPEnvelope env
=msg.getSOAPPart().getEnvelope();
   Iterator it
=env.getBody().getChildElements();   
   SOAPElement multi
=null;
   
while(it.hasNext())
    {
     multi
=(SOAPElement)it.next();//multi是soapbody的最后一個child。
    }
   String value
="";//value表示加密后的值。
   SOAPElement digestValue=null;
   Iterator it2
=multi.getChildElements();
   
while(it2.hasNext())
   {
    SOAPElement temp
=(SOAPElement)it2.next();
    Iterator it3
=temp.getChildElements(env.createName("DigestValue",
"ns3","http://www.w3.org/2000/11/temp-xmlenc"));
    
if(it3.hasNext())
    value
=((SOAPElement)it3.next()).getValue();//獲得加密的值    
   }   
    
//把加密的SOAPMessage解密成目標(biāo)服務(wù)可以調(diào)用的SOAP消息。
    SOAPMessage   msg2=convertMessage(msg,this.decrypte(value));
    msgContext.setMessage(msg2);        
      }
      
catch(Exception e)
      {
       e.printStackTrace();
      }      
 } 
 
//這個方法是把加密的數(shù)據(jù)進(jìn)行解密,返回明文。
 public String decrypte(String value)
 {
  String data
=null;
  
try
  {
   ByteArrayInputStream fis 
= new 
ByteArrayInputStream(
new sun.misc.BASE64Decoder().decodeBuffer(value));
   ObjectInputStream ois 
= new ObjectInputStream(fis);
   Object o 
= ois.readObject();
   
if (!(o instanceof String)) {
    System.out.println(
"Unexpected data in string");
    System.exit(
-1);
   }
   data 
= (String) o;
   System.out.println(
"解密后的值:" + data);
   o 
= ois.readObject();
   
if (!(o instanceof byte[])) {
    System.out.println(
"Unexpected data in string");
    System.exit(
-1);
   }   
   
byte origDigest[] = (byte []) o;
   MessageDigest md 
= MessageDigest.getInstance("SHA");
   md.update(data.getBytes());
  }
         …
  
return data;
  }
    
//把解密后的信息重新組裝成服務(wù)端能夠使用的SOAP消息。
 public SOAPMessage convertMessage(SOAPMessage msg,String data)
 {    
   ….
 }
}  
  
可以看出,服務(wù)端解密的Handler和客戶端加密的Handler的操作是相反的過程。

總結(jié)
通過以上的討論,相信大家已經(jīng)掌握了Handler的基本使用技巧。可以看出,通過使用Handler,可以給Web服務(wù)提供一些額外的功能。在實際的開發(fā)中,我們可以開發(fā)出一些通用的Handler,然后通過不同的搭配方式把它們部署到不同的Web服務(wù)中。