WebService 设计总结
接触过很多电商的WebService,有种一看就蛋疼的设计,今天要从这个反例说一说 WebService 的设计。 [WebMethod] public string QueryOrderDetail(string xml) { ... } 如上代码输入是一个XML,输出也是一个XML,方法内部自己在做序列化和反序列化。放着成熟的SOAP标准不用,自己再实现一套数据标准。 反而XML成为一个黑盒,调用双方不得不依赖于接口文档,真是吃力不讨好。 因此好的WebService接口,应该从下面几个方面仔细考虑: 一. 参数 (1) 参数应该直接使用简单的数据类型(POCO、POJO),甚至时间类型都可以考虑用string,只要双方约束好时间字符串的格式。 (2) 如果参数个数超过3个,那就需要考虑设计一个Class了,避免参数列表过长,当然这没有硬性规定。 (3) 设计统一的参数规则。比如对外提供的查询接口就要考虑分页相关的数据。保证类似的接口都有统一的参数定义,形成习惯是提升效率最好方式。 ? ? ? 业务参数和非业务参数应该分开,比如分页的数据就可以抽象出基类。 二. 异常 三. 安全
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) ?很容易地实现对 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:ProgrammingVSProject2008WebServiceSampleWebService1WebService1Logsservice.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 这个阶段,取出客户端传的 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 客户端我决定用 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 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |