WebSerivce学习笔记

Web Services是由企业发布的完成其特定商务需求的在线应用服务,其他公司或应用软件能够通过Internet来访问并使用这项在线服务。用简单点的话说,就是系统对外的接口它是一种构建应用程序的普遍模型,可以在任何支持网络通信的操作系统中实施运行;它是一种新的web应用程序分支,是自包含、自描述、模块化的应用,可以发布、定位、通过web调用。WebService是一个应用组件,它逻辑性的为其他应用程序提供数据与服务.各应用程序通过网络协议和规定的一些标准数据格式(Http,XML,Soap)来访问WebService,通过WebService内部执行得到所需结果。Web Service可以执行从简单的请求到复杂商务处理的任何功能。一旦部署以后,其他WebService应用程序可以发现并调用它部署的服务。可以说WebService就是一个可以远程调用的类,或者说是组件。

在构建和使用Web Service时,主要用到以下几个关键的技术和规则:

  1.XML:描述数据的标准方法.

  2.SOAP:表示信息交换的协议.

  3.WSDL:Web服务描述语言.

  4.UDDI(Universal Description, Discovery and Integration):通用描述、发现与集成,它是一种独立于平台的,基于XML语言的用于在互联网上描述商务的协议。

WebService的主要目标是跨平台的可互操作性。为了达到这一目标,WebService完全基于XML(可扩展标记语言)、XSD(XMLSchema)等独立于平台、独立于软件供应商的标准,是创建可互操作的、分布式应用程序的新平台。由此可以看出,在以下三种情况下,使用WebService会带来极大的好处。

长项一:跨防火墙的通信

  如果应用程序有成千上万的用户,而且分布在世界各地,那么客户端和服务器之间的通信将是一个棘手的问题。因为客户端和服务器之间通常会有防火墙或者代理服务器。在这种情况下,使用DCOM就不是那么简单,通常也不便于把客户端程序发布到数量如此庞大的每一个用户手中。传统的做法是,选择用浏览器作为客户端,写下一大堆ASP页面,把应用程序的中间层暴露给最终用户。这样做的结果是开发难度大,程序很难维护。

  从经验来看,在一个用户界面和中间层有较多交互的应用程序中,使用WebService这种结构,可以节省花在用户界面编程上20%的开发时间。另外,这样一个由WebService组成的中间层,完全可以在应用程序集成或其它场合下重用。最后,通过WebService把应用程序的逻辑和数据"暴露"出来,还可以让其它平台上的客户重用这些应用程序。

长项二:应用程序集成

  企业级的应用程序开发者都知道,企业里经常都要把用不同语言写成的、在不同平台上运行的各种程序集成起来,而这种集成将花费很大的开发力量。应用程序经常需要从运行在IBM主机上的程序中获取数据;或者把数据发送到主机或UNIX应用程序中去。即使在同一个平台上,不同软件厂商生产的各种软件也常常需要集成起来。通过WebService,应用程序可以用标准的方法把功能和数据"暴露"出来,供其它应用程序使用。

长项三:B2B的集成

  用WebService集成应用程序,可以使公司内部的商务处理更加自动化。但当交易跨越供应商和客户、突破公司的界限时会怎么样呢?跨公司的商务交易集成通常叫做B2B集成。WebService是B2B集成成功的关键。通过WebService,公司可以把关键的商务应用"暴露"给指定的供应商和客户。例如,把电子下单系统和电子发票系统"暴露"出来,客户就可以以电子的方式发送订单,供应商则可以以电子的方式发送原料采购发票。当然,这并不是一个新的概念,EDI(电子文档交换)早就是这样了。但是,WebService的实现要比EDI简单得多,而且WebService运行在Internet上,在世界任何地方都可轻易实现,其运行成本就相对较低。不过,WebService并不像EDI那样,是文档交换或B2B集成的完整解决方案。WebService只是B2B集成的一个关键部分,还需要许多其它的部分才能实现集成。

  用WebService来实现B2B集成的最大好处在于可以轻易实现互操作性。只要把商务逻辑"暴露"出来,成为WebService,就可以让任何指定的合作伙伴调用这些商务逻辑,而不管他们的系统在什么平台上运行,使用什么开发语言。这样就大大减少了花在B2B集成上的时间和成本,让许多原本无法承受EDI的中小企业也能实现B2B集成。

长项四:软件和数据重用

  软件重用是一个很大的主题,重用的形式很多,重用的程度有大有小。最基本的形式是源代码模块或者类一级的重用,另一种形式是二进制形式的组件重用。

  当前,像表格控件或用户界面控件这样的可重用软件组件,在市场上都占有很大的份额。但这类软件的重用有一个很大的限制,就是重用仅限于代码,数据不能重用。原因在于,发布组件甚至源代码都比较容易,但要发布数据就没那么容易,除非是不会经常变化的静态数据。

  WebService在允许重用代码的同时,可以重用代码背后的数据。使用WebService,再也不必像以前那样,要先从第三方购买、安装软件组件,再从应用程序中调用这些组件;只需要直接调用远端的WebService就可以了。举个例子,要在应用程序中确认用户输入的地址,只需把这个地址直接发送给相应的WebService,这个WebService就会帮你查阅街道地址、城市、省区和邮政编码等信息,确认这个地址是否在相应的邮政编码区域。WebService的提供商可以按时间或使用次数来对这项服务进行收费。这样的服务要通过组件重用来实现是不可能的,那样的话你必须下载并安装好包含街道地址、城市、省区和邮政编码等信息的数据库,而且这个数据库还是不能实时更新的。

  另一种软件重用的情况是,把好几个应用程序的功能集成起来。例如,要建立一个局域网上的门户站点应用,让用户既可以查询联邦快递包裹,查看股市行情,又可以管理自己的日程安排,还可以在线购买电影票。现在Web上有很多应用程序供应商,都在其应用中实现了这些功能。一旦他们把这些功能都通过WebService"暴露"出来,就可以非常容易地把所有这些功能都集成到你的门户站点中,为用户提供一个统一的、友好的界面。

  将来,许多应用程序都会利用WebService,把当前基于组件的应用程序结构扩展为组件/WebService的混合结构,可以在应用程序中使用第三方的WebService提供的功能,也可以把自己的应用程序功能通过WebService提供给别人。两种情况下,都可以重用代码和代码背后的数据。

  从以上论述可以看出,WebService在通过Web进行互操作或远程调用的时候是最有用的。不过,也有一些情况,WebService根本不能带来任何好处。

短处一:单机应用程序

  目前,企业和个人还使用着很多桌面应用程序。其中一些只需要与本机上的其它程序通信。在这种情况下,最好就不要用WebService,只要用本地的API就可以了。COM非常适合于在这种情况下工作,因为它既小又快。运行在同一台服务器上的服务器软件也是这样。最好直接用COM或其它本地的API来进行应用程序间的调用。当然WebService也能用在这些场合,但那样不仅消耗太大,而且不会带来任何好处。

短处二:局域网的同构应用程序

  在许多应用中,所有的程序都是用VB或VC开发的,都在Windows平台下使用COM,都运行在同一个局域网上。例如,有两个服务器应用程序需要相互通信,或者有一个Win32或WinForm的客户程序要连接局域网上另一个服务器的程序。在这些程序里,使用DCOM会比SOAP/HTTP有效得多。与此相类似,如果一个.NET程序要连接到局域网上的另一个.NET程序,应该使用.NETremoting。有趣的是,在.NETremoting中,也可以指定使用SOAP/HTTP来进行WebService调用。不过最好还是直接通过TCP进行RPC调用,那样会有效得多。

总之,只要从应用程序结构的角度看,有别的方法比WebService更有效、更可行,那就不要用WebService。

摘自百度百科:http://baike.baidu.com/view/837392.htm

Web Service是现在最适合实现SOA的技术,而Axis2是实现Web Service的一种技术框架(架构)。

Axis2 是下一代 Apache Axis。Axis2 虽然由 Axis 1.x 处理程序模型提供支持,但它具有更强的灵活性并可扩展到新的体系结构。Axis2 基于新的体系结构进行了全新编写,而且没有采用 Axis 1.x 的常用代码。支持开发 Axis2 的动力是探寻模块化更强、灵活性更高和更有效的体系结构,这种体系结构可以很容易地插入到其他相关 Web 服务标准和协议(如 WS-Security、WS-ReliableMessaging 等)的实现中。

Axis2 的特性包括:

  • 采用名为 AXIOM(AXIObject Model,Axis 对象模型)的新核心 XML 处理模型
  • 支持 In-Only 和 In-Out 消息交换模式 (MEP)
  • 阻塞和非阻塞客户端 API(应用程序编程接口)
  • 支持内置的 Web 服务寻址 (WS-Addressing)
  • 支持 XMLBeans 数据绑定
  • 新部署模型
  • 支持超文本传输协议 (HTTP)、简单邮件传输协议 (SMTP) 和传输控制协议 (TCP) 等传输协议

本系列文章以 Axis2 0.92 版本为基础。您可以在 Apache 网站获得 Axis2 的最新版本。

Axis 体系结构概述


图 1. Axis2 体系结构
 

Axis2 体系结构将逻辑与状态分离;这允许在并行线程中执行逻辑。服务和调用的静态状态和动态状态分别存储在 Description 和Context 类中。Axis2 体系结构是使用 7 个独立模块实现的。

我们将在本系列文章的第 2 部分中详细介绍上述各阶段。请求消息在通过 Inpipe 中配置的所有阶段后,到达 MessageReceiver,然后由 MessageReceiver 调用实际服务实现。服务器的 OutPipe 包含以下阶段:

  • Message initialization
  • Policy determination
  • User phases
  • MessageOut

用户配置的阶段位于这两个管道的用户阶段(User phases) 部分。如果在执行这些管道的过程中发生错误,则这些错误将通过 InFaultPipe 或 OutFaultPipe 管道。收到 Fault 消息后,在客户端调用 InFaultPipe;如果某个调用导致将错误发送到客户端,则在服务器端调用 OutFaultPipe。用户可以将处理程序添加到预先定义的阶段,并且按照这些处理程序运行的顺序进行配置。

  • 部署模块:此模块配置 Axis 引擎并部署服务和模块。axis2.xml(在 webapps/axis2/WEB-INF 中)包含 Axis2 引擎的全局配置,包括:
    • 全局模块 (Global modules)
    • 全局接收器 (Global receivers)
    • 传输 (Transports)
    • 用户阶段定义 (User phase definitions)

每个服务的配置都包含在服务存档的 services.xml 文件中。本文稍后将详细讨论此文件。

  • WSDL 和代码生成:此模块从 WSDL 文件中生成客户端存根和服务器框架代码。Axis2 代码生成器发出采用正确 XML 样式表的 XML 文件,以用所需语言生成代码。
  • 客户端 API:Axis2 客户端 API 调用遵循 WSDL 2.0 定义的 In-Only 和 In-Out 消息模式的操作。客户端 API 支持 In-Out 操作的阻塞和非阻塞调用。
  • 传输:此模块包含与传输层交互的处理程序。传输处理程序有两种类型:TransportListener 和 TransportSender。TransportListener 从传输层接收 SOAP 消息,然后将其传送到 InPipe 进行处理。TransportSender 发送通过指定传输从OutPipe 接收到的 SOAP 消息。Axis2 提供 HTTP、SMTP 和 TCP 的处理程序。对于 HTTP 传输,服务器端上的 AxisServlet和客户端上的一个简单的独立 HTTP 服务器(由 Axis2 提供)充当 TransportReceiver。

部署 Axis2

首先在 Axis2 二进制代码分发包的 webapps 目录下查找 Axis2 Web 应用程序 axis2.war。在 servlet 容器中部署此 war 文件。在 Tomcat 中,如果已在服务器配置中将 unpackWARs 设置为 True,则只需将 axis2.war 复制到 $TOMCAT_HOME/webapps 目录即可部署 Axis2。请立即启动 Tomcat 并访问 http://localhost:<port>/axis2。将显示 Axis2 欢迎页,单击此页上的 Validate 链接。您应到达 Axis2 Happiness page,不会出现任何错误。

开发 StockQuoteService

下面介绍如何使用 In-Only subscribe() In-Out getQuote() 这两个操作来开发 StockQuoteServicesubscribe() 操作将预订指定代号的每小时报价,而 getQuote() 将获得指定代号的当前报价。

下面的清单 StockQuoteService 的实现示例:


清单 1. StockQuoteService 实现

                

package stock;

import org.apache.axis2.om.OMAbstractFactory;

import org.apache.axis2.om.OMElement;

import org.apache.axis2.om.OMFactory;

import org.apache.axis2.om.OMNamespace;

        

public class StockQuoteService {

         


										public void subscribe(OMElement in){

    String symbol = in.getText();

    System.out.println("Subscription request for symbol ="+symbol);

    // put the actual subscribe code here...    

  }

          

          


										public OMElement getQuote(OMElement in){

          

    // Get the symbol from request message

    String symbol = in.getText();

            

    int quote = 0;

    if(symbol.equals("IBM")){

      quote = 100;

    }

    // Put more quotes here ...

            

    // Create response

    OMFactory fac = OMAbstractFactory.getOMFactory();

    OMNamespace omNs = fac.createOMNamespace(

      "http://www.developerworks.com/example", "example");

    OMElement resp = fac.createOMElement("getQuoteResponse", omNs);

    resp.setText(String.valueOf(quote));

    return resp;      

  }

}

 

对方法签名感到很疑惑?我们将讨论上述方法中的 OMElement

回页首

部署服务

部署描述符

Axis2 中,服务部署信息包含在 services.xml 文件(在 0.92 以前的版本中,此文件名为 service.xml)中。对于上述 StockQuoteService,服务部署描述符与下面的清单类似。


清单 2. Services.xml

                

<service name="StockQuoteService">

  <parameter name="ServiceClass" locked="xsd:false">


										stock.StockQuoteService
									

  </parameter>

        

  <operation name="getQuote">


										<messageReceiver 

      class="org.apache.axis2.receivers.RawXMLINOutMessageReceiver"/>
									

  </operation>

        

  <operation name="subscribe">


										<messageReceiver 

      class="org.apache.axis2.receivers.RawXMLINOnlyMessageReceiver"/>
									

  </operation>

</service>

 

服务的 name 属性定义服务的名称。Axis2 使用服务的名称创建服务的端点地址,例如 http://localhost:<port>/axis2/services/<nameofservice>。因此,对于 StockQuoteService,服务端点为 http://localhost:<port>/axis2/services/StockQuoteServiceServiceClass 参数指定服务实现类。

每个 <operation> 元素定义服务中一个操作的配置。<operation>  name 属性应设置为服务实现类中方法的名称。messageReceiver 元素定义用于处理此操作的消息接收器。Axis2 针对 In-Only In-Out 操作提供了两个无数据绑定的内置 MessageReceiversorg.apache.axis2.receivers.RawXMLINOnlyMessageReceiver 用于 In-Only 操作,而org.apache.axis2.receivers.RawXMLINOutMessageReceiver 用于 In-Out 操作。如果没有指定 messageReceiver,则 Axis2 将尝试使用 org.apache.axis2.receivers.RawXMLINOutMessageReceiver 作为缺省的 messageReceiver。上述 RAWXML 消息接收器将传入 SOAP 消息的 <Body> 的内容作为 OMElementOMElement XML 元素的 AXIOM 缩写)传递给服务实现。此操作应作为 OMElement 返回 SOAP 响应的 <Body> 元素包含的 XML 内容。这便解释了为何 subscribe() getQuote() 操作采用和返回 OMElement

services.xml 还可以包含分为 servicegroup 的多个服务。

打包

Axis 2 服务是作为 Axis Archive (.aar) 打包的。这是一个 JAR 文件(使用 jar zip 实用程序创建),在存档的 META-INF 目录中打包了 services.xml 文件。StockQuoteService 在打包成 StockQuoteService.aar 时将具有以下结构:

./stock/StockQuoteService.class

./META-INF/services.xml

 

预先打包的 StockQuoteService 存档可以在本文的下载部分中找到。

部署

Axis2 中部署服务相当简单,只需将 .aar 文件复制到 servlet 容器的 axis2 Web 应用程序中的 axis2/WEB-INF/services 目录下即可。对于 Tomcat,此位置为 $TOMCAT_HOME/webapps/axis2/WEB-INF/services

另一种部署服务的好方法是使用 Axis2 管理控制台中的 Upload Service 工具。请转到 http://localhost:<port>/axis2,然后选择Administration 链接。输入用户名和密码 admin/axis2,然后登录。(您可以在 axis2.xml 中配置用户名/密码。)在工具部分选择Upload Service 链接,再选择 .aar 文件,然后单击 Upload。就是这样简单!如果上传成功,系统将显示一条绿色成功消息。服务即被部署,而且可随时调用。如果要在远程 Axis2 服务器上部署服务,则此功能非常方便。

回页首

通过 Axis2 使用 Web 服务

Web 服务调用的特性由 MEP、传输协议以及客户端 API 的同步和/或异步行为决定。Axis2 当前支持 WSDL 2.0 定义的 In-Only In-Out MEPAxis2 客户端 API 支持服务的同步和异步调用。在调用 In-Out 操作时,在 API 级别和传输级别提供异步行为。API 级别异步是通过回滚获得的,它使用一个传输连接来同时传输请求和响应(例如,通过一个 HTTP 连接传输请求和响应)。在传输级别异步中,使用不同的传输连接分别发送请求和接收响应,例如使用 SMTP 进行传输时即如此。

下面是使用 Axis2 客户端 API 调用 In-Only In-Out 操作的详细信息。

调用 In-Only 操作

org.apache.axis2.clientapi.MessageSender 类用于调用 In-Only 操作(如下面的清单所示),而 In-Only 操作调用StockQuoteService  subscribe() 操作。


清单 3. 调用 In-Only 操作

                

try{

  EndpointReference targetEPR = new EndpointReference(

      "http://localhost:8080/axis2/services/StockQuoteService");

 

  // Make the request message

  OMFactory fac = OMAbstractFactory.getOMFactory();

  OMNamespace omNs = fac.createOMNamespace(

      "http://www.developerworks.com/example", "example");

  OMElement payload = fac.createOMElement("subscribe", omNs);

  payload.setText("IBM"); 

 

  // Send the request


										MessageSender msgSender = new MessageSender();

  msgSender.setTo(targetEPR);

  msgSender.setSenderTransport(Constants.TRANSPORT_HTTP);

  msgSender.send("subscribe", payload);
									

  }catch (AxisFault axisFault) {

      axisFault.printStackTrace();

  }

 

MessageSender.send() 发送请求消息并将其立即返回。要使用的传输由 MessageSender.setSenderTransport() 指定。此示例通过 HTTP 发送消息。

调用 In-Out 操作

使用 org.apache.axis2.clientapi.Call 类可以方便地调用 In-Out 操作。调用 In-Out 操作时,此 Call 类支持下列 4 种模式:



  1. 清单 4. 阻塞单传输模式

                        

try {

 

  EndpointReference targetEPR = new EndpointReference(

      "http://localhost:8080/axis2/services/StockQuoteService");

 

  // Create request message

  OMFactory fac = OMAbstractFactory.getOMFactory();

  OMNamespace omNs = fac.createOMNamespace(

      "http://www.developerworks.com/example", "example");

    OMElement payload = fac.createOMElement("getQuote",omNs);

  payload.setText("IBM");

 

  // Create the call


										Call call = new Call();

  call.setTo(targetEPR);

            

  call.setTransportInfo(Constants.TRANSPORT_HTTP,

    Constants.TRANSPORT_HTTP, false);
									

  // Invoke blocking


										OMElement result = call.invokeBlocking("getQuote", payload);
									

 

  System.out.println("Quote ="+result.getText());

}catch (AxisFault axisFault) {

    axisFault.printStackTrace();

}

 

代码的第一部分使用 AXIOM 创建请求消息。Call.setTransportInfo() 设置用于发送请求和获得响应的传输。Call.setTransportInfo() 操作的 Boolean 参数指出是否要使用不同的传输连接来分别发送请求和接收响应。在本例中,要求用一个 HTTP 连接发送请求和接收响应。



  1. 清单 5. 非阻塞单传输模式

                        

try {

  EndpointReference targetEPR = new EndpointReference(

      "http://localhost:8080/axis2/services/StockQuoteService");

 

  //Create the request

  OMFactory fac = OMAbstractFactory.getOMFactory();

  OMNamespace omNs = fac.createOMNamespace(

      "http://www.developerworks.com/example", "example");

  OMElement payload = fac.createOMElement("getQuote", omNs);

  payload.setText("IBM");

 

 


										// Create the call

    Call call = new Call();

    call.setTo(targetEPR);

 

    // Set the transport info.

    call.setTransportInfo(org.apache.axis2.Constants.TRANSPORT_HTTP,

      org.apache.axis2.Constants.TRANSPORT_HTTP, false);

 

  // Callback to handle the response

 

    Callback callback = new Callback() {

 

      public void onComplete(AsyncResult result) {

        System.out.println("Quote = "

          + result.getResponseEnvelope().getBody().getFirstElement()

            .getText());

      }

 

      public void reportError(Exception e) {

        e.printStackTrace();

      }

    };
									

 

 

  // Invoke non blocking

 


										call.invokeNonBlocking("getQuote", payload, callback);
									

 

  //Wait till the callback receives the response.

 


										while (!callback.isComplete()) {

      Thread.sleep(1000);

    }
									

 

 

  call.close();

} catch (AxisFault axisFault) {

  axisFault.printStackTrace();

} catch (Exception ex) {

  ex.printStackTrace();

}

 

Call.invokeNonBlocking() 方法立即返回而不阻塞。Call.invokeNonBlocking() 采用org.apache.axis2.clientapi.CallBack 的对象,如果响应来自服务,则将触发此对象。CallBack 有两个抽象方法onComplete(AsynchResult)  reportError(Exception),需要由具体的 CallBack 类实现这些方法。在服务调用正常完成后,Axis2 引擎调用 onComplete() 方法。在从服务器获得错误消息后,调用 Callback  reportError() 方法。Callback.isComplete() 将指出操作调用是否完成。

因为上面两个方法使用一个传输连接来发送和接收消息,所以这些方法不适合长时间运行的事务。原因是在响应可用之前,传输连接可能会超时。要解决此问题,可使用两个不同的连接来分别发送请求和接收响应。但因为使用了其他传输连接来获得响应,因此需要将请求和响应关联起来。Axis2 支持 WS-Addressing,后者通过使用 <wsa:MessageID> <wsa:RelatesTo> Header 可解决此问题。因此,如果使用两个传输,则支持对模块寻址,如下面两个模式所示。



  1. 清单 6. 阻塞双传输模式

                        

try{

  EndpointReference targetEPR = new EndpointReference(

      "http://localhost:8080/axis2/services/StockQuoteService");

 

  OMFactory fac = OMAbstractFactory.getOMFactory();

  OMNamespace omNs = fac.createOMNamespace(

      "http://www.developerworks.com/example", "example");

  OMElement payload = fac.createOMElement("getQuote",omNs);

  payload.setText("IBM");

 


										Call call = new Call();

    call.setTo(targetEPR);

 

    call.setTransportInfo(

      Constants.TRANSPORT_HTTP, Constants.TRANSPORT_HTTP, true);

 

    //Blocking Invocation

    OMElement result = call.invokeBlocking("getQuote", payload);
									

  System.out.println("Quote = "+result.getText());

    

}catch (AxisFault axisFault) {

    axisFault.printStackTrace();

}catch (Exception ex) {

    ex.printStackTrace();

}

 



  1. 清单 7. 非阻塞双传输模式

                        

try {

 

  EndpointReference targetEPR = new EndpointReference(

      "http://localhost:8080/axis2/services/StockQuoteService");

  OMFactory fac = OMAbstractFactory.getOMFactory();

  OMNamespace omNs = fac.createOMNamespace(

      "http://www.developerworks.com/example", "example");

  OMElement payload = fac.createOMElement("getQuote",omNs);

  payload.setText("IBM");

 

 


										Call call = new Call();

    call.setTo(targetEPR);

 

    call.setTransportInfo(

      Constants.TRANSPORT_HTTP, Constants.TRANSPORT_HTTP, true);
									

  // Callback to handle the response

 


										Callback callback = new Callback() {

    public void onComplete(AsyncResult result) {

      System.out.println("Quote = "

          + result.getResponseEnvelope().getBody().getFirstElement()

        .getText());

      }

 

    public void reportError(Exception e) {

        e.printStackTrace();

      }

    };
									

 

 

  // Non-Blocking Invocation

 


										call.invokeNonBlocking("getQuote", payload, callback);
									

  // Wait till the callback receives the response.

 


										while (!callback.isComplete()) {

      Thread.sleep(1000);

    }

    call.close();
									

}catch (AxisFault axisFault) {

      axisFault.printStackTrace();

}catch (Exception ex) {

      ex.printStackTrace();

}

 

摘自:https://www.ibm.com/developerworks/cn/webservices/ws-webaxis1/

原文地址:https://www.cnblogs.com/doit8791/p/2573770.html