zoukankan      html  css  js  c++  java
  • 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 + ":
    " + 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 + ":
    " + 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 这个阶段,取出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


  • 相关阅读:
    MySQL -- 表联结
    Unittest方法 -- 项目实现自动发送邮件
    Linux工作中常用命令
    Requests方法 -- Token获取操作
    Requests方法 -- 关联用例执行
    Requests方法 -- Blog流程类进行关联
    Requests方法 -- 参数化
    defer和async的区别
    js函数收藏:获取cookie值
    XSHELL使用技巧总结
  • 原文地址:https://www.cnblogs.com/blfshiye/p/3803800.html
Copyright © 2011-2022 走看看