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) 尽量避免将异常定义在返回值中,通过返回值定义错误那么不管服务端还是client都要写非常多if ... else 分支。
(3) 系统异常和业务异常要区分好,比方使用 SoapException 能够用 Code 来区分,比方:System.Error 表示系统错误,Bussiness.Error 表示业务错误。
(4) 补充:.net framework 假设没有包装那么默认有两种 fautCode: soap:Client 和 soap:Server。假设client传入BadRequest 基本就是 soap:Client 错误,其它 没有自己定义code的则就是 soap:Server 的错误。
三. 安全
不管何时都要保证系统的安全性,我认为安全也分系统安全和业务安全两种:
(1) 系统安全主要是指client的认证授权,调用次数(须要考虑会不会拖垮业务系统) 等
(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<Order> QueryOrder(Query<OrderCondition> queryInfo) { OrderService service = new OrderService(); return service.Query(queryInfo); } } }
PageResult<T>, Query<T> 将统一的业务部分抽取出来,这样定义其它的业务对象就能简化了。
using System; using System.Collections.Generic; namespace WebService1.Entity { [Serializable] public class PageResult<T> { 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<T> Data { get; set; } } }
using System; using System.Collections.Generic; namespace WebService1.Entity { [Serializable] public class Query<T> { public int PageNo { get; set; } public int PageSize { get; set; } public T Condition { get; set; } } }
跳过业务处理部分,来关注一下应用框架考虑的日志和安全拦截。能够利用 .NET framework 的 Soap Extensions (msdn) 非常easy地实现对 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 配置例如以下:
<log4net> <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender"> <param name="File" value="F:\Programming\VSProject2008\WebServiceSample\WebService1\WebService1\Logs\service.log"/> <param name="DatePattern" value=".yyyy-MM-dd'.log'" /> <param name="AppendToFile" value="true"/> <param name="MaxSizeRollBackups" value="10"/> <param name="MaximumFileSize" value="5MB"/> <param name="RollingStyle" value="Date"/> <param name="StaticLogFileName" value="false"/> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="%d [%t] %-5p [%property{ip}] [%property{action}] - %m%n"/> </layout> </appender> <root> <level value="DEBUG"/> <appender-ref ref="RollingFileAppender"/> </root> </log4net>
那么 WebMethod 仅仅要加上 [Trace] 特性,就能够开启日志记录功能。
[WebMethod] [Trace] public PageResult<Order> QueryOrder(Query<OrderCondition> 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: <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tem="http://tempuri.org/"> <soapenv:Body> <tem:QueryOrder> <!--Optional:--> <tem:queryInfo> <tem:PageNo>1</tem:PageNo> <tem:PageSize>1</tem:PageSize> <!--Optional:--> <tem:Condition> <!--Optional:--> <tem:StartTime>?</tem:StartTime> <!--Optional:--> <tem:EndTime>?</tem:EndTime> <!--Optional:--> <tem:ShopId>?</tem:ShopId> <!--Optional:--> <tem:ProductId>?</tem:ProductId> </tem:Condition> </tem:queryInfo> </tem:QueryOrder> </soapenv:Body> </soapenv:Envelope> 2014-05-25 22:05:02,357 [8] INFO [127.0.0.1] [http://tempuri.org/QueryOrder] - SoapResponse: <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <QueryOrderResponse xmlns="http://tempuri.org/"> <QueryOrderResult> <PageNo>1</PageNo> <PageSize>1</PageSize> <TotalCount>3</TotalCount> <PageCount>1</PageCount> <HasNextPage>false</HasNextPage> <Data> <Order> <Id>1</Id> <OrderDate>2014-05-25 22:05:02</OrderDate> <ShopId>SHOP001</ShopId> <ProductId>PRD001</ProductId> <Quantity>1</Quantity> <Price>59</Price> </Order> ... </Data> </QueryOrderResult> </QueryOrderResponse> </soap:Body> </soap:Envelope>
接下来利用 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 这个阶段,取出client传的 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<Order> QueryOrder(Query<OrderCondition> queryInfo) { OrderService service = new OrderService(); return service.Query(queryInfo); } } }
最后我们拿 SoapUI 来測试一下:
再来看看错误处理,假设有益输错 UserName:
顺便要赞一下 SoapUI,真是 WebService 调试的利器,还能够生成 .NET / Java 代码,推荐大家使用。我们用 SoapUI 生成一下 Java 代码。
Java client我决定用 CXF 来实现。所以要先配置一下 SoapUI:
JAVA CXF Client 代码:
public static void main(String[] args) { try { Service1 service1 = new Service1(); Service1Soap service1Soap = service1.getService1Soap(); BindingProvider provider = (BindingProvider)service1Soap; List<Header> headers = new ArrayList<Header>(); 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