WebService 设计总结

接触过很多电商的WebService,有种一看就蛋疼的设计,今天要从这个反例说一说 WebService 的设计。

[WebMethod]
public string QueryOrderDetail(string xml)
{
   ...
}

如上代码输入是一个XML,输出也是一个XML,方法内部自己在做序列化和反序列化。放着成熟的SOAP标准不用,自己再实现一套数据标准。
反而XML成为一个黑盒,调用双方不得不依赖于接口文档,真是吃力不讨好。

因此好的WebService接口,应该从下面几个方面仔细考虑:

一. 参数
(1) 参数应该直接使用简单的数据类型(POCO、POJO),甚至时间类型都可以考虑用string,只要双方约束好时间字符串的格式。
(2) 如果参数个数超过3个,那就需要考虑设计一个Class了,避免参数列表过长,当然这没有硬性规定。
(3) 设计统一的参数规则。比如对外提供的查询接口就要考虑分页相关的数据。保证类似的接口都有统一的参数定义,形成习惯是提升效率最好方式。
      业务参数和非业务参数应该分开,比如分页的数据就可以抽象出基类。


二. 异常
(1) 使用框架中定义的Exception类型,比如:SoapException, FaultException(WCF)。
(2) 尽量避免将异常定义在返回值中,通过返回值定义错误那么无论服务端还是客户端都要写很多if ... else 分支。
(3) 系统异常和业务异常要区分好,比如使用 SoapException 可以用 Code 来区分,比如:System.Error 表示系统错误,Bussiness.Error 表示业务错误。
(4) 补充:.net framework  如果没有包装那么默认有两种 fautCode:  soap:Client 和 soap:Server。假设客户端传入BadRequest 基本就是 soap:Client 错误,其他 没有自定义code的则就是 soap:Server 的错误。

三. 安全
无论何时都要保证系统的安全性,我觉得安全也分系统安全和业务安全两种:
(1) 系统安全主要是指客户端的认证授权,调用次数(需要考虑会不会拖垮业务系统) 等
(2) 业务安全主要是指数据查询/操作权限,当然这个主要是从业务角度考虑的。


四. 日志
日志可以方便排查错误,还可以通过日志来分析服务基本信息(比如:调用次数,失败次数等),必要时还可以通过日志来进行重试。
另外要考虑开发的便捷,设计统一的日志拦截处理。

以 WebService Application (.NET 3.5) 为例,记录几种常用的编程技巧。
原始的 WebService 如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;
using WebService1.Entity;
using WebService1.Service;
using System.Web.Services.Protocols;

namespace WebService1
{
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [System.ComponentModel.ToolboxItem(false)]
    public class Service1 : System.Web.Services.WebService
    {
        [WebMethod]       
        public PageResult QueryOrder(Query queryInfo)
        {
            OrderService service = new OrderService();
            return service.Query(queryInfo);
        }
    }
}

PageResult, Query  将统一的业务部分抽取出来,这样定义其他的业务对象就能简化了。

using System;
using System.Collections.Generic;

namespace WebService1.Entity
{
    [Serializable]
    public class PageResult
    {
        public int PageNo { get; set; }
        public int PageSize { get; set; }
        public int TotalCount { get; set; }
        public int PageCount { get; set; }
        public bool HasNextPage { get; set; }
        public List Data { get; set; }
    }
}
using System;
using System.Collections.Generic;

namespace WebService1.Entity
{
    [Serializable]
    public class Query
    {
        public int PageNo { get; set; }
        public int PageSize { get; set; }
        public T Condition { get; set; }
    }
}

跳过业务处理部分,来关注一下应用框架考虑的日志和安全拦截。可以利用 .NET framework 的 Soap Extensions ( msdn)  很容易地实现对 WebMethod 的 AOP。
Soap Extensions 可以通过两种方式“注入”: 自定义Atrribute 或者通过 Web.config 里的 soapExtensionTypes 进行声明。

TraceExtension 的实现:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Web.Services.Protocols;
using log4net;
using System.Xml;

namespace WebService1.Common
{
    public class TraceExtension : SoapExtension
    {
        private ILog logger = LogManager.GetLogger(typeof(TraceExtension));

        Stream oldStream;
        Stream newStream;
        
        public override System.IO.Stream ChainStream(System.IO.Stream stream)
        {
            oldStream = stream;
            newStream = new MemoryStream();
            return newStream;
        }

        public override void ProcessMessage(SoapMessage message)
        {
            switch (message.Stage)
            {
                case SoapMessageStage.BeforeDeserialize:
                    
                    log4net.ThreadContext.Properties["ip"] = HttpContext.Current.Request.UserHostAddress;
                    log4net.ThreadContext.Properties["action"] = message.Action;

                    WriteInput(message);
                    break;
                case SoapMessageStage.AfterDeserialize:
                    break;
                case SoapMessageStage.BeforeSerialize:
                    break;
                case SoapMessageStage.AfterSerialize:
                    WriteOutput(message);
                    break;
                default:
                    throw new Exception("Invalid Stage");
            }
        }

        public override object GetInitializer(Type serviceType)
        {
            return null;
        }

        public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attr)
        {
            return null;
        }

        public override void Initialize(object initializer)
        {
            //filename = (string)initializer;
        }

        public void WriteOutput(SoapMessage message)
        {
            string soapString = (message is SoapServerMessage) ? "SoapResponse" : "SoapRequest";
            string content = GetContent(newStream);
            // 为了Format XML,如果从性能考虑应该去掉此处的处理
            if (!string.IsNullOrEmpty(content))
            {
                XmlDocument xmlDoc = new XmlDocument();
                xmlDoc.LoadXml(content);
                using (StringWriter sw = new StringWriter())
                {
                    using (XmlTextWriter xtw = new XmlTextWriter(sw))
                    {
                        xtw.Formatting = Formatting.Indented;
                        xmlDoc.WriteTo(xtw);
                        content = sw.ToString();
                    }
                }
            }

            logger.Info(soapString + ":\n" + content);

            Copy(newStream, oldStream);
        }

        public void WriteInput(SoapMessage message)
        {
            Copy(oldStream, newStream);

            string soapString = (message is SoapServerMessage) ? "SoapRequest" : "SoapResponse";
            string content = GetContent(newStream);
            logger.Info(soapString + ":\n" + content);
        }

        void Copy(Stream from, Stream to)
        {
            TextReader reader = new StreamReader(from);
            TextWriter writer = new StreamWriter(to);
            writer.WriteLine(reader.ReadToEnd());
            writer.Flush();
        }

        string GetContent(Stream stream)
        {
            stream.Position = 0;
            TextReader reader = new StreamReader(stream);
            string content = reader.ReadToEnd();
            stream.Position = 0;
            return content;
        }

    }

}
TraceAttribute 实现如下:
using System;
using System.Web.Services.Protocols;

namespace WebService1.Common
{
    [AttributeUsage(AttributeTargets.Method)]
    public class TraceAttribute : SoapExtensionAttribute
    {
        private int priority = 0;
        public override Type ExtensionType
        {
            get { return typeof(TraceExtension); }
        }

        public override int Priority
        {
            get { return priority; }
            set { priority = value; }
        }
    }
}

其中 TraceExtension 利用 log4net 来记录调用 WebMethod 的Request 和 Response,还包括 ip 和 Action(Action其实对应的 WebMethod)
对应的 log4net 配置如下:
	
		
			
                        
			
			
			
			
			
			
				
			
		
		
			
			
		
	

那么 WebMethod 只要加上 [Trace] 特性,就可以开启日志记录功能。
        [WebMethod]
        [Trace]
        public PageResult QueryOrder(Query queryInfo)
        {
            OrderService service = new OrderService();
            return service.Query(queryInfo);
        }


输出日志如下:

2014-05-25 22:05:02,292 [8] INFO  [127.0.0.1] [http://tempuri.org/QueryOrder] - SoapRequest:

   
      
         
         
            1
            1
            
            
               
               ?
               
               ?
               
               ?
               
               ?
            
         
      
   


2014-05-25 22:05:02,357 [8] INFO  [127.0.0.1] [http://tempuri.org/QueryOrder] - SoapResponse:


  
    
      
        1
        1
        3
        1
        false
        
          
            1
            2014-05-25 22:05:02
            SHOP001
            PRD001
            1
            59
          
          ...
        
      
    
  

接下来利用 SoapHeader 实现最基本的 Basic Authentication 校验,当然你不想每一个 WebMethod 去做相同的Check,同样我们实现一个 Soap Extension。

Authentication (SoapHeader) 的定义:
using System;
using System.Web.Services.Protocols;

namespace WebService1.Common
{
    public class Authentication : SoapHeader
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }
}
AuthCheckExtension 的实现:在 SoapMessage AfterDeserialize 这个阶段,取出客户端传的 SoapHeader 验证 UserName 和 Password 在服务端是否存在。
如果不存在或者错误则抛出 no auth ! 的错误。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Web.Services.Protocols;
using WebService1.Config;

namespace WebService1.Common
{
    public class AuthCheckExtension : SoapExtension
    {
        public override void ProcessMessage(SoapMessage message)
        {
            if (message.Stage == SoapMessageStage.AfterDeserialize)
            {
                foreach (SoapHeader header in message.Headers)
                {
                    if (header is Authentication)
                    {
                        var authHeader = header as Authentication;
                        var isValidUser = true;
                        var users = AuthConfiguration.AuthSettings.Users;
                        if (users != null && users.Count > 0)
                        { 
                            isValidUser = users.Any(u => u.UserName == authHeader.UserName && u.Password == authHeader.Password);
                        }

                        if (!isValidUser)
                            throw new BizException("no auth !");
                    }
                }
            }
        }


        public override object GetInitializer(Type serviceType)
        {
            return null;
        }

        public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
        {
            return null;
        }

        public override void Initialize(object initializer)
        {
            // 初始化 AuthSettings 
            AuthConfiguration.Config();
        }
    }

}
然后给 WebMethod 加上 [SoapHeader("Authentication"), AuthCheck] 就OK了。
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Services;
using WebService1.Entity;
using WebService1.Service;
using System.Web.Services.Protocols;
using WebService1.Common;

namespace WebService1
{
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [System.ComponentModel.ToolboxItem(false)]
    public class Service1 : System.Web.Services.WebService
    {
        public Authentication Authentication { get; set; }

        [WebMethod]
        [Trace]
        [SoapHeader("Authentication"), AuthCheck]
        public PageResult QueryOrder(Query queryInfo)
        {
            OrderService service = new OrderService();
            return service.Query(queryInfo);
        }

    }
}

最后我们拿 SoapUI 来测试一下:

WebService 设计总结_第1张图片

WebService 设计总结_第2张图片

再来看看错误处理,如果故意输错 UserName:
WebService 设计总结_第3张图片

顺便要赞一下 SoapUI,真是 WebService 调试的利器,还可以生成 .NET / Java 代码,推荐大家使用。我们用 SoapUI 生成一下 Java 代码。
Java 客户端我决定用 CXF 来实现。所以要先配置一下 SoapUI:

WebService 设计总结_第4张图片


JAVA CXF Client 代码:

public static void main(String[] args) {
        try {

            Service1 service1 = new Service1();
            Service1Soap service1Soap = service1.getService1Soap();
            BindingProvider provider = (BindingProvider)service1Soap;

            List
headers = new ArrayList
(); Authentication authentication = new Authentication(); authentication.setUserName("fangxing"); authentication.setPassword("123456"); Header authHeader = new Header(ObjectFactory._Authentication_QNAME, authentication, new JAXBDataBinding(Authentication.class)); headers.add(authHeader); provider.getRequestContext().put(Header.HEADER_LIST, headers); QueryOfOrderCondition queryInfo = new QueryOfOrderCondition(); queryInfo.setPageNo(1); queryInfo.setPageSize(1000); OrderCondition condition = new OrderCondition(); condition.setShopId("SHOP001"); condition.setStartTime("2014-05-01 00:00:00"); condition.setEndTime("2014-05-10 23:59:59"); queryInfo.setCondition(condition); PageResultOfOrder result = service1Soap.queryOrder(queryInfo); System.out.println("get order size: " + result.getData().getOrder().size()); } catch (Exception e) { e.printStackTrace(); } }

示例代码下载,下载请阅 Readme.txt


你可能感兴趣的:([08],Web,Services)