浅析RPC与WebService

2023-10-27

虽然现在非常火的RPC技术以SpringCloud和Dubbo为主流,但是如果做接口调用,还是逃不了要用一些较传统的技术。前几天在做接口调用时恰巧用到了WebService的相关技术(8,9两节是真实的开发),正好都在这里写一写。

文章涉及到的主要源码可在文章结尾的链接获取,有需要的小伙伴可以前往下载。

1. RPC相关基础

1.1 什么是RPC

----| RPC(Remote Procedure Call),远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。(来自百度百科)

----| RPC允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不需要显式编码这个远程调用的细节。(来自CSDN博客:https://blog.csdn.net/mindfloating/article/details/39473807)

1.2 RPC的特点

从上面的概念中,能大概总结出RPC的基本特点如下:

  • 通过网络传输的
  • 跨终端、跨平台的
  • 基于请求-响应的
  • 只调用过程,不需关注细节

1.3 RPC的基本原理

在底层去看,RPC其实就是将流从一台计算机传输到另外一台计算机,无论是基于传输协议(http、tcp、udp等等)和网络IO(bio、nio)来实现。

1.4 常见的RPC的调用技术

  • SpringCloud(Spring的,基于REST的,SOA架构的分布式框架,严格意义上讲SpringCloud不是一个RPC框架,它是REST框架)
  • Dubbo(x)(阿里巴巴的,基于Socket的,SOA架构的分布式框架)
  • WebService(跨语言的,基于SOAP协议,走xml数据或json数据)
  • Hessian(跨语言的,基于Binary-RPC协议,走二进制数据)
  • HttpClient(通常用于RESTful风格的调用,跨语言,基于http和json)
  • jdk原生(HttpURLConnection)

参考资料:https://www.cnblogs.com/cainiao-Shun666/p/9181903.html

由于本文主要介绍WebService,对其他的调用技术不展开介绍,有需要的小伙伴可以在博客平台上获取相关博客和资料。

 

2. WebService是什么

Web service是一个平台独立的,低耦合的,自包含的、基于可编程的web的应用程序,可使用开放的XML(标准通用标记语言下的一个子集)标准来描述、发布、发现、协调和配置这些应用程序,用于开发分布式的互操作的应用程序。(来自百度百科)

简单的来讲:WebService是一种跨语言和跨操作系统的远程调用技术。

有篇博客里讲的一段话不错,摘抄下来供小伙伴阅读:

其实可以从多个角度来理解 WebService,从表面上看,WebService就是一个应用程序向外界暴露出一个能通过Web进行调用的API,也就是说能用编程的方法通过Web来调用这个应用程序。我们把调用这个WebService的应用程序叫做客户端,而把提供这个WebService的应用程序叫做服务端。从深层次看,WebService是建立可互操作的分布式应用程序的新平台,是一个平台,是一套标准。它定义了应用程序如何在Web上实现互操作性,你可以用任何你喜欢的语言,在任何你喜欢的平台上写WebService,只要我们可以通过WebService标准对这些服务进行查询和访问。(来自博客:https://www.cnblogs.com/xdp-gacl/p/4048937.html

————| 2018.08.22补充:

评论里一位老哥提出了对SOAP、WebService、Socket的一些见解和指正,特此感谢!以下是评论原文:

SOAP是WebService的一种,REST也是WebService的一种。

所谓WebService,就是一种符合国际标准格式的HttpService,包括SOAP,REST等标准,协议为HTTP;与之对应的是Socket。而grpc,thrift基本都是定制化的Socket,这是因为Socket本身无标准数据格式,你可以称thrift为TCP界的REST,我是这样来理解的;而新来的WebSocket则是介于TCP,HTTP之间的一种满足浏览器某些方面的特殊访问,它是随浏览器和W3C标准的进化而产生的。

3. 为什么要用WebService

  • 平台语言无关性——WebService基于SOAP协议,且发布的所有服务都有对应的xml(wsdl),可以实现跨平台、跨语言的支持
  • 安全通信——WebService走http请求,不受防火墙限制
  • 功能复用——通过使用WebService,可以暴露共用服务,实现功能复用

实际上一个WebService可以看做一个独立的功能,供外界使用。

4. WebService的核心

4.1 SOAP

  • SOAP(Simple Object Access Protocal),即简单对象访问协议,它用来描述传递信息的格式。
  • SOAP是一种简单的基于xml的协议和规范,它使应用程序通过http来交换信息。
  • SOAP可以简单理解为http + xml。

其实,WebService通过http协议发送请求和接收结果时,发送的请求内容和结果内容都采用xml格式封装,并增加了一些特定的http消息头,以说明 http消息的内容格式,这些特定的http消息头和xml内容格式就是SOAP协议。

4.2 WSDL

  • WSDL(Web Services Description Language),即网络服务描述语言,可描述WebService服务。
  • 一句话概括:WSDL是基于xml的用于描述WebService及其方法、参数和返回值的“说明书”。
  • WSDL设计成xml是有章可循的:xml可以很容易被DOM解析,标签及属性也容易被人阅读,进而看懂相应服务的使用方式。
  • 每一个WebService服务必定会有它对应的WSDL,通过这个WSDL,可以在不同语言平台上解析成对应语言的源码,进而被开发者使用。

4.3 UDDI

UDDI 的目的是为电子商务建立标准;UDDI是一套基于Web的、分布式的、为Web Service提供的、信息注册中心的实现标准规范,同时也包含一组使企业能将自身提供的Web Service注册,以使别的企业能够发现的访问协议的实现标准。

5. 什么时候可以用WebService

  • 提供统一服务访问接口(没有语言限制)
  • 系统间服务发布与调用
  • 分布式(可以用但不方便,当然现在用的更多的是SpringCloud和Dubbo(x))
  • 对性能要求不是很高的服务

6. 在Java中有什么方式使用WebService

  • jdk原生的WebService方式(基础)
  • 使用Apache的CXF框架(常用)
  • 使用Apache的axis框架(多语言,重量级)
  • 使用XFire框架(已没落)

本文将使用原生的JWS和Apache的CXF框架来分别介绍如何使用WebService

7. 怎么发布WebService服务

为了与后面的CXF框架使用同一工程,这里直接用Maven搭建Provider-Demo工程

7.1 创建服务提供方的Maven工程

创建服务提供方的Maven工程,因为工程要使用Spring与CXF整合,所以打包方式为war

ddd9c0ad2c4a733b48057639c6f774f97d7.jpg

7.2 创建基于JWS的WebService服务

7.2.1 创建JWSProvider类,并加上@WebService注解

import javax.jws.WebService;

/**
 * 基于JWS的WebService服务提供者
 * @Title JWSProvider
 * @author LinkedBear
 */
@WebService
public class JWSProvider {
   
}

7.2.2 编写服务源码

import javax.jws.WebService;
import javax.xml.ws.Endpoint;

/**
 * 基于JWS的WebService服务提供者
 * @Title JWSProvider
 * @author LinkedBear
 */
@WebService
public class JWSProvider {
    /**
     * 服务功能效果:在传入的名后追加一个随机数
     * @param name
     * @return
     */
    public String getRandomCode(String name) {
        System.out.println("基于JWS的WebService服务:getRandomCode被调用了。。。");
        return name + Math.random();
    }
    
    public static void main(String[] args) throws Exception {
        System.out.println("开始发布WebService服务。。。");
        Endpoint.publish("http://localhost/getRandomCode", new JWSProvider());
        System.out.println("WebService服务发布成功。。。");
    }
}

7.2.3 执行main方法,控制台打印成功提示启动基于JWS的服务

8605274abac0ac7164e44e1055e52fe58b2.jpg

浏览器输入http://localhost/getRandomCode?wsdl后,可以看到该服务的WSDL说明书,服务发布成功。

33598372eaafa2a1a4f0b9ef2920bf4027d.jpg

7.3 创建基于CXF的WebService服务

7.3.1 在pom.xml中加入CXF的依赖

<properties>
	<spring.version>5.0.4.RELEASE</spring.version>
	<cxf.version>3.2.6</cxf.version>
</properties>

<dependencies>
    <!-- 导入CXF的依赖 -->
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-core</artifactId>
		<version>${cxf.version}</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-frontend-jaxws</artifactId>
		<version>${cxf.version}</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-transports-http</artifactId>
		<version>${cxf.version}</version>
	</dependency>
	<dependency>
		<groupId>org.apache.cxf</groupId>
		<artifactId>cxf-rt-transports-http-jetty</artifactId>
		<version>${cxf.version}</version>
	</dependency>

	<!--导入Spring的依赖 -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-core</artifactId>
		<version>${spring.version}</version>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-beans</artifactId>
		<version>${spring.version}</version>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-context</artifactId>
		<version>${spring.version}</version>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-expression</artifactId>
		<version>${spring.version}</version>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-context-support</artifactId>
		<version>${spring.version}</version>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-webmvc</artifactId>
		<version>${spring.version}</version>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-aspects</artifactId>
		<version>${spring.version}</version>
	</dependency>
</dependencies>

7.3.2 在web.xml中配置CXF的Servlet

<!-- CXF的Servlet -->
<servlet>
    <servlet-name>cxf</servlet-name>
    <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
    <init-param>
        <param-name>config-location</param-name>
        <param-value>classpath:applicationContext-cxf.xml</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>cxf</servlet-name>
    <url-pattern>/webservice/*</url-pattern>
</servlet-mapping>

7.3.3 在src/main-resource目录下创建applicationContext-cxf.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:jaxws="http://cxf.apache.org/jaxws"
	xmlns:soap="http://cxf.apache.org/bindings/soap"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://cxf.apache.org/bindings/soap 
                        http://cxf.apache.org/schemas/configuration/soap.xsd
                        http://cxf.apache.org/jaxws 
                        http://cxf.apache.org/schemas/jaxws.xsd">
	<!-- 导入jar包中的cxf配置文件 -->
	<import resource="classpath:META-INF/cxf/cxf.xml" />
	<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

</beans>

7.3.4 创建服务提供者接口和实现类

注意@WebService注解要加到接口上!

import javax.jws.WebService;

/**
 * 基于CXF的服务提供者接口
 * @Title CXFProvider
 * @author LinkedBear
 */
@WebService
public interface CXFProvider {
    String getNameHashCode(String name);
}

实现类:

/**
 * 基于CXF的服务提供者
 * @Title CXFProvider
 * @author LinkedBear
 */
public class CXFProviderImpl implements CXFProvider {
    /**
     * 服务功能效果:在传入的name后追加hashCode
     * @param name
     * @return
     */
    @Override
    public String getNameHashCode(String name) {
        System.out.println("基于CXF的WebService服务:getNameHashCode被调用了。。。");
        return name + name.hashCode();
    }
}

7.3.5 在applicationContext-cxf中注册服务

<!-- 注册服务的对象 -->
<bean id="cxfProvider" class="com.linkedbear.webservice.cxf.CXFProviderImpl"/>
<!-- 注册服务 -->
<jaxws:server id="nameHashCodeServer" address="/nameHashCodeServer">
    <jaxws:serviceBean>
        <ref bean="cxfProvider"/>
    </jaxws:serviceBean>
</jaxws:server>

7.3.6 将工程部署到Tomcat,启动服务器

注意!如果Spring的依赖导不全,启动时会报错!

浏览器输入http://localhost:8080/WebService-Provider-Demo/webservice/nameHashCodeServer?wsdl后,可以看到该服务的WSDL说明书,服务发布成功。

d954ee35c0440b7007554c4e2032a14b9e9.jpg

8. 怎么调用WebService服务

先创建服务调用方的Maven工程。

由于调用方只负责使用服务,故打包方式为jar即可。

同样要导入CXF和Spring的依赖。

 

服务发布好后,下一步就是如何调用服务。

下面将分3部分介绍WebService服务的调用方式

8.1 使用jdk自带的wsimport命令,生成本地源码,基于JWS调用

在jdk安装目录下,找到bin目录,除了我们日常熟悉的javac,javap,javadoc等常用命令之外,还有一个wsimport命令:

b6cf0b04b13a081473d082c35f0807730b0.jpg

使用该命令,可以将指定的WSDL转化为本地源码+编译后的字节码文件。

8.1.1 wsimport使用方式

常用的命令参数如下:

  • -d <directory>  在指定的目录生成class文件
  • -clientjar <jarfile>  在当前目录生成jar文件,结合-d <directory>可以在指定的目录生成jar文件
  • -s <directory>  在指定的目录生成java源文件
  • -p <pkg>  指定生成文件的包结构
  • -keep  在生成class文件,或者jar包时,同时保留java源文件

最常用的命令行语句为:

wsimport –s . <wsdl>

此处的.指的是生成源码的位置为当前文件夹

 

我们将刚刚写好的基于JMS的WebService服务的WSDL取出,用wsimport命令生成本地源码,效果如下:

http://localhost/getRandomCode?wsdl

9f7240b3d6f20a6829f0a2c5daff7c25e4b.jpg

1bf59bf0a5eef2aed8cbec88024d9f85a5e.jpg

8.1.2 将解析的.java复制到工程中

49e2c053357c574c4e2681d70393f96a040.jpg

8.1.3 使用JMS的方式调用服务

从生成的源码中可以看出,GetRandomCode.java为服务的调用类,要想使用服务,必定会使用到它。

但如果你想直接创建这个类的对象去调用,那你只会扑一场空:

2bed4cc8cb3a1d67f653c3bfbe74d53a18b.jpg

这个类怎么没有对应的方法呢?

既然你觉得这个类是我们发布的服务名,那它是不是可以类比于Java反射中的Method类呢?

那既然Method类是表示一个方法的,那它肯定要有执行对象才可以调用该方法吧!

 

正确调用方式:

public class JWSConsumer {
    public static void main(String[] args) throws Exception {
        //先创建WebService服务对象
        JWSProviderService webservice = new JWSProviderService();
        //再创建该服务的提供者对象
        JWSProvider provider = webservice.getJWSProviderPort();
        //之后,才能调用服务
        String code = provider.getRandomCode("LinkedBear");
        System.out.println(code);
    }
}

cc210f763479e3b5879bb826f23004d86c7.jpg

8.2 使用CXF框架的方式调用服务

使用CXF框架,在生成本地源码时要使用CXF自己的方式生成。

8.2.1 wsdl2java使用方式

CXF提供的解析命令为wsdl2java。

我们在一开始使用wsimport时,即便不进入jdk的目录也能正常使用,是因为我们配置了PATH的环境变量。

如果想在任意位置使用CXF的wsdl2java命令,也需要配置环境变量。但考虑到这个命令用的频率实在太低,故不配置,直接进入到CXF的目录下。

从官网下载CXF的框架包,解压之后,可以从bin目录下找到wsdl2java.bat。

deea73d0aafa13739c00691146e231d76ea.jpg

wsdl2java命令的常用命令参数如下:

  • -p  指定生成的客户端包名
  • -d  指定生成的客户端生成目录
  • --compile  指定需要进行编译
  • -c  指定编译生成的目录
  • -client  指定生成客户端调用类, 即包含main方法调用客户端方法的类

 

执行wsdl2java命令,可以将WSDL转换成.java(没有.class)

4af9f53c4e2c5bbcb4a71ee0ff948d919ab.jpg

e17dff9baecdf4e4cff7b4ffdb82d1a2624.jpg

8.2.2 将解析的.java复制到工程中

4eb201431da39b88366f7e5ccbe29f658a1.jpg

我们只需要唯一的那个接口就可以了,不需要全部的源文件。

但是删除其他源码后,接口会报错,原因是接口上有一个@XmlSeeAlso注解,需要传入ObjectFactory的class对象,这里并不需要这个Factory,直接删掉即可,只保留一对空的花括号。

5ef929271576ba315da8c1c43ea69439d36.jpg

8.2.3 将服务注入到本地

之所以在服务调用方也用到了Spring,是因为CXF与Spring配合时,可以只留下接口文件,利用Spring的服务注入,来进行远程调用。

在src/main/resource目录下,创建applicationContext.xml文件,将CXF的服务注入到Spring的容器中(原理是Spring会创建这个接口的代理对象,通过代理对象去远程调用服务端的接口)。

<jaxws:client id="consumer" 
                address="http://localhost:8080/WebService-Provider-Demo/webservice/nameHashCodeServer" 
                serviceClass="com.linkedbear.webservice.cxf.CXFProvider"/>

8.2.4 使用CXF的方式调用服务

public class App {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        CXFProvider provider = (CXFProvider) ctx.getBean("consumer");
        String nameHashCode = provider.getNameHashCode("LinkedBear");
        System.out.println(nameHashCode);
    }
}

99badc567049f011d3c2016c67200907ce5.jpg

8.3 不生成本地源码的方式调用服务

前面两种方式,最大的缺点是需要生成本地源码,费时费力不说,万一服务端改了接口的入参规则,你这边也要重新生成,再修改业务代码。

那我们就想:能不能有一种方式,可以不生成本地源码的前提下,也能调用WebService服务呢?

可以!使用HttpClient,配合SOAP协议,可以实现不生成本地源码的前提下,也能调用WebService服务。

8.3.1 为什么使用HttpClient也可以成功调用服务呢?

我们说,WebService是基于SOAP协议的,我们使用本地源码发送的请求,其实也就是这些基于SOAP的POST请求,收到的响应也是基于SOAP的响应。

那么,如果我们自己构造基于SOAP协议的POST请求,是不是服务也就可以正常返回结果呢?当然是肯定的!

不过,唯一不太好的是:自行构造源码,获得响应后需要自行解析响应体。

8.3.2 导入HttpClient的依赖

使用HttpClient,就需要导入HttpClient的依赖(注意不要导错了)。

<dependency>
	<groupId>commons-httpclient</groupId>
	<artifactId>commons-httpclient</artifactId>
	<version>3.1</version>
</dependency>

8.3.3 SOAP协议的请求体xml格式(精简)

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
        <[method] xmlns="[namaspace]">
            <[args]>[text]</[args]>
        </[method]>
    </soap:Body>
</soap:Envelope>

上面的格式中,方括号内的标识为具体WebService的请求。

举个简单的栗子吧:

url为http://localhost/getRandomCode?wsdl

里面的namespace为自行指定,不一定是请求路径的某一段。。。

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
        <getRandomCode xmlns="http://webservice.linkedbear.com/">
            <name>LinkedBear</name>
        </getRandomCode>
    </soap:Body>
</soap:Envelope>

8.3.4 修改服务提供者源码

要想使WebService支持SOAP的POST请求访问,就需要在每个参数上加上注解标识,代表可以被SOAP的请求使用。

修改后的源码如下(注意看参数上面的注解):

@WebService
public class JWSProvider {
    public String getRandomCode(
                @WebParam(name="name",targetNamespace="http://webservice.linkedbear.com/") String name
            ) {
        System.out.println("基于JWS的WebService服务:getRandomCode被调用了。。。");
        return name + Math.random();
    }
    
    public static void main(String[] args) throws Exception {
        Endpoint.publish("http://localhost/getRandomCode", new JWSProvider());
    }
}

8.3.5 使用HttpClient发送POST请求,调用WebService服务

public class App {
    public static void main(String[] args) throws Exception {
        String url = "http://localhost/getRandomCode?wsdl";
        StringBuilder sb = new StringBuilder();
        sb.append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">");
        sb.append("  <soap:Body>");
        sb.append("    <getRandomCode xmlns=\"http://webservice.linkedbear.com/\">");
        sb.append("      <name>LinkedBear</name>");
        sb.append("    </getRandomCode>");
        sb.append("  </soap:Body>");
        sb.append("</soap:Envelope>");
        PostMethod postMethod = new PostMethod(url);
        byte[] bytes = sb.toString().getBytes("utf-8");
        InputStream inputStream = new ByteArrayInputStream(bytes, 0, bytes.length);
        RequestEntity requestEntity = new InputStreamRequestEntity(inputStream, bytes.length, "text/xml;charset=UTF-8");
        postMethod.setRequestEntity(requestEntity);
        
        HttpClient httpClient = new HttpClient();
        httpClient.executeMethod(postMethod);
        String soapResponseData = postMethod.getResponseBodyAsString();
        System.out.println(soapResponseData);
    }
}

请求结果(响应体真的没有换行符号,直接一行出来了。。。):

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:getRandomCodeResponse xmlns:ns2="http://jws.webservice.linkedbear.com/"><return>LinkedBear0.0681470650599495</return></ns2:getRandomCodeResponse></soap:Body></soap:Envelope>

8.3.6 提取响应数据

我们完全可以使用Dom4j来提取响应体的数据,但是Dom4j只能一层一层的扒,太费劲。我推荐大家使用Jsoup进行xml转换和提取。

导入Jsoup的jar如下:

<dependency>
	<groupId>org.jsoup</groupId>
	<artifactId>jsoup</artifactId>
	<version>1.11.3</version>
</dependency>

之后向刚才的源码追加如下内容,便可以只输出想要的返回结果。

Document document = Jsoup.parse(soapResponseData);
String text = document.getElementsByTag("return").text();
System.out.println(text);
//输出结果:
//LinkedBear0.0681470650599495

9. 【附加】制造通用的基于SOAP的WebService请求工具

本来上面已经做完了,但是为了后期方便,就封装一套工具吧!这样以后就可以方便调用了。

设计的原则:尽可能的符合“开放-封闭原则”。

9.1 设计基本请求工具类

最基本的工具类,只需要将刚才上面列出来的SOAP协议请求体数据提取出一个Map,封装进即可。

初步封装如下:

public class WebServiceUtil {
    public static void invokeWebService(Map<String, Object> map) throws Exception {
        String url = (String) map.get("url");
        StringBuilder sb = new StringBuilder();
        sb.append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">");
        sb.append("  <soap:Body>");
        //传入method和namespace
        sb.append("    <" + map.get("method") + " xmlns=\"" + map.get("namespace") + "\">");
        //动态构造参数和值
        Map<String, String> argsMap = (Map<String, String>) map.get("argsMap");
        for (Map.Entry<String, String> entry : argsMap.entrySet()) {
            sb.append("      <" + entry.getKey() + ">" + entry.getValue() +"</" + entry.getKey() + ">");
        }
        sb.append("    </" + map.get("method") + ">");
        sb.append("  </soap:Body>");
        sb.append("</soap:Envelope>");
        PostMethod postMethod = new PostMethod(url);
        byte[] bytes = sb.toString().getBytes("utf-8");
        InputStream inputStream = new ByteArrayInputStream(bytes, 0, bytes.length);
        RequestEntity requestEntity = new InputStreamRequestEntity(inputStream, bytes.length, "text/xml;charset=UTF-8");
        postMethod.setRequestEntity(requestEntity);
        
        HttpClient httpClient = new HttpClient();
        httpClient.executeMethod(postMethod);
        String soapResponseData = postMethod.getResponseBodyAsString();
        
        //提取响应体中的指定标签内的数据
        Document document = Jsoup.parse(soapResponseData);
        List<String> returnTagList = (List<String>) map.get("returnTagList");
        for (String returnTag : returnTagList) {
            Elements tags = document.getElementsByTag(returnTag);
            for (Element tag : tags) {
                System.out.println(tag.text());
            }
        }
    }
    
    private WebServiceUtil() {
    }
}

9.2 二度封装:参数封装为数据结构,并加入空值校验

上面的封装有很多不妥之处:

  1. 参数的key值属于魔法值,随时都有可能修改;
  2. 每次从map中get值时可能取到空值;
  3. 从map中取出的集合内元素也有可能为null。

针对以上问题,需要构造一个WebServiceSoapBean的数据结构,用来封装请求中出现的数据。

二度封装后的源码如下(为了更方便的做非空判定,加入了commons-lang):

public class WebServiceSoapBean {
    private String url;
    private String method;
    private String namespace;
    private Map<String, String> argsMap;
    private List<String> returnTagList;
    //getter, setter
}

public class WebServiceUtil {
    public static void invokeWebService(WebServiceSoapBean bean) throws Exception {
        checkBeanIsComplete(bean);
        
        String url = bean.getUrl();
        StringBuilder sb = new StringBuilder();
        sb.append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">");
        sb.append("  <soap:Body>");
        //传入method和namespace
        sb.append("    <" + bean.getMethod() + " xmlns=\"" + bean.getNamespace() + "\">");
        //动态构造参数和值
        Map<String, String> argsMap = bean.getArgsMap();
        for (Map.Entry<String, String> entry : argsMap.entrySet()) {
            sb.append("      <" + entry.getKey() + ">" + entry.getValue() +"</" + entry.getKey() + ">");
        }
        sb.append("    </" + bean.getMethod() + ">");
        sb.append("  </soap:Body>");
        sb.append("</soap:Envelope>");
        PostMethod postMethod = new PostMethod(url);
        byte[] bytes = sb.toString().getBytes("utf-8");
        InputStream inputStream = new ByteArrayInputStream(bytes, 0, bytes.length);
        RequestEntity requestEntity = new InputStreamRequestEntity(inputStream, bytes.length, "text/xml;charset=UTF-8");
        postMethod.setRequestEntity(requestEntity);
        
        HttpClient httpClient = new HttpClient();
        httpClient.executeMethod(postMethod);
        String soapResponseData = postMethod.getResponseBodyAsString();
        
        //提取响应体中的指定标签内的数据
        Document document = Jsoup.parse(soapResponseData);
        List<String> returnTagList = bean.getReturnTagList();
        for (String returnTag : returnTagList) {
            Elements tags = document.getElementsByTag(returnTag);
            for (Element tag : tags) {
                System.out.println(tag.text());
            }
        }
    }
    
    private static void checkBeanIsComplete(WebServiceSoapBean bean) {
        if (StringUtils.isBlank(bean.getUrl())) {
            throw new NullPointerException("对不起,url为空!");
        }
        if (StringUtils.isBlank(bean.getMethod())) {
            throw new NullPointerException("对不起,method为空!");
        }
        if (StringUtils.isBlank(bean.getNamespace())) {
            throw new NullPointerException("对不起,namespace为空!");
        }
        if (Objects.isNull(bean.getArgsMap())) {
            throw new NullPointerException("对不起,参数列表为null!");
        }
        if (Objects.isNull(bean.getReturnTagList())) {
            throw new NullPointerException("对不起,响应体标签列表为null!");
        }
    }

    private WebServiceUtil2() {
    }
}

9.3 三度封装:优化响应体数据打印

上面的封装,对于数据的处理没有什么太大的问题了,但是打印出来的响应数据却会比较乱。

接下来要将响应体的数据提取单独分离出一个方法,且提取返回类型应为Map或json。

继续添加fastjson的依赖(JSONObject比Map更强大,且有序,底层LinkedHashMap实现)。

三度封装后的源码如下(只有一个新加的方法):

//新加的方法:
private static List<JSONObject> parseResponseXmlToJson(String xml, List<String> returnTagList) {
    //转换之前先校验:如果压根就没有这个标签,直接抛空指针
    if (returnTagList.isEmpty()) {
        throw new NullPointerException("对不起,响应体标签集合为空!");
    }
    for (String tag : returnTagList) {
        //不能只校验标签,还要加上尖括号,防止出现响应数据中出现tag而撞车
        if (!xml.contains("<" + tag + ">")) {
            throw new NullPointerException("对不起,响应体中没有这个标签:" + tag);
        }
    }
    
    Document document = Jsoup.parse(xml);
    //前面校验过了,所以这里肯定能取出来
    Element parent = document.getElementsByTag(returnTagList.get(0)).get(0).parent();
    //取同级,每一个同级就是一个json
    String tagName = parent.tagName();
    //再退一级,找这个tagName,就可以获取所有数据结点了
    Elements elements = parent.parent().getElementsByTag(tagName);
    List<JSONObject> list = new ArrayList<>(elements.size());
    for (Element element : elements) {
        JSONObject json = new JSONObject();
        for (String returnTag : returnTagList) {
            json.put(returnTag, element.getElementsByTag(returnTag).get(0).text());
        }
    }
    
    return list;
}

9.4 四度封装:模板方法模式

目前的设计已经大体具备了一个通用工具类应该有的功能,这样就结束了吗?

需求:个性化定制WebService请求,包括额外的参数、额外的校验、额外的提取规则等等???

那通用工具类就远远不够了,需要进行纵向扩展。

在面向对象的思想中,纵向扩展的实现方案是:继承和多态。

 

那么接下来我们来分析:通用工具类和个性化定制的请求类,到底差别在哪里?如何分离共性?如何实现个性?

这就需要使用模板方法模式进行更深层次的通用抽取。

既然使用模板方法模式,就不能再做静态工具类了,而是纵向扩展的类继承体系。

最终封装的模板类如下(解释一个问题:这个类没有设计成抽象类,是因为即便没有特殊的需求下,普通WebService也可以直接通过这个类去实例化对象,调用服务接口,而不是每次都要创建一个匿名内部类):

public class WebServiceTemplateUtil {
    public void invokeWebService(WebServiceSoapBean bean) throws Exception {
        checkBeanIsComplete(bean);
        
        String url = bean.getUrl();
        StringBuilder sb = new StringBuilder();
        sb.append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">");
        //模板方法切入点:有加入head部分的需求可以让子类重写此方法
        doAddSoapHead(sb, bean);
        sb.append("  <soap:Body>");
        //传入method和namespace
        sb.append("    <" + bean.getMethod() + " xmlns=\"" + bean.getNamespace() + "\">");
        //动态构造参数和值
        Map<String, String> argsMap = bean.getArgsMap();
        for (Map.Entry<String, String> entry : argsMap.entrySet()) {
            sb.append("      <" + entry.getKey() + ">" + entry.getValue() +"</" + entry.getKey() + ">");
        }
        sb.append("    </" + bean.getMethod() + ">");
        sb.append("  </soap:Body>");
        sb.append("</soap:Envelope>");
        PostMethod postMethod = new PostMethod(url);
        byte[] bytes = sb.toString().getBytes("utf-8");
        InputStream inputStream = new ByteArrayInputStream(bytes, 0, bytes.length);
        RequestEntity requestEntity = new InputStreamRequestEntity(inputStream, bytes.length, "text/xml;charset=UTF-8");
        postMethod.setRequestEntity(requestEntity);
        
        HttpClient httpClient = new HttpClient();
        httpClient.executeMethod(postMethod);
        String soapResponseData = postMethod.getResponseBodyAsString();
        
        //提取响应体中的指定标签内的数据
        List<JSONObject> jsons = parseResponseXmlToJson(soapResponseData, bean.getReturnTagList());
        for (JSONObject json : jsons) {
            System.out.println(json.toJSONString());
        }
    }
    
    //如果有加入head部分的需求可以重写此方法
    protected void doAddSoapHead(StringBuilder sb, WebServiceSoapBean bean) {
    }
    //如果有额外的数据校验的需求可以重写此方法
    protected void doExtraRequestCheck(WebServiceSoapBean bean) {
    }
    
    private List<JSONObject> parseResponseXmlToJson(String xml, List<String> returnTagList) {
        //转换之前先校验:如果压根就没有这个标签,直接抛空指针
        if (returnTagList.isEmpty()) {
            throw new NullPointerException("对不起,响应体标签集合为空!");
        }
        for (String tag : returnTagList) {
            //不能只校验标签,还要加上尖括号,防止出现响应数据中出现tag而撞车
            if (!xml.contains("<" + tag + ">")) {
                throw new NullPointerException("对不起,响应体中没有这个标签:" + tag);
            }
        }
        
        Document document = Jsoup.parse(xml);
        //前面校验过了,所以这里肯定能取出来
        Element parent = document.getElementsByTag(returnTagList.get(0)).get(0).parent();
        //取同级,每一个同级就是一个json
        String tagName = parent.tagName();
        //再退一级,找这个tagName,就可以获取所有数据结点了
        Elements elements = parent.parent().getElementsByTag(tagName);
        List<JSONObject> list = new ArrayList<>(elements.size());
        for (Element element : elements) {
            JSONObject json = new JSONObject();
            for (String returnTag : returnTagList) {
                json.put(returnTag, element.getElementsByTag(returnTag).get(0).text());
            }
        }
        
        return list;
    }
    
    private void checkBeanIsComplete(WebServiceSoapBean bean) {
        if (StringUtils.isBlank(bean.getUrl())) {
            throw new NullPointerException("对不起,url为空!");
        }
        if (StringUtils.isBlank(bean.getMethod())) {
            throw new NullPointerException("对不起,method为空!");
        }
        if (StringUtils.isBlank(bean.getNamespace())) {
            throw new NullPointerException("对不起,namespace为空!");
        }
        if (Objects.isNull(bean.getArgsMap())) {
            throw new NullPointerException("对不起,参数列表为null!");
        }
        if (Objects.isNull(bean.getReturnTagList())) {
            throw new NullPointerException("对不起,响应体标签列表为null!");
        }
        
        //模板方法模式切入点:有额外的数据校验的需求可以让子类重写此方法
        doExtraRequestCheck(bean);
    }
}

如果子类还需要额外的方法,可以重写模板方法达到目的。

这样就满足了“开放-封闭原则”——“对扩展开放,对修改关闭”。

(完)

附:源码下载

服务提供者:https://gitee.com/linkedbear/WebService-Provider-Demo

服务调用者:https://gitee.com/linkedbear/WebService-Consumer-Demo

转载于:https://my.oschina.net/LinkedBear/blog/1928400

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

浅析RPC与WebService 的相关文章

随机推荐

  • CUDA Samples: Long Vector Add

    以下CUDA sample是分别用C 和CUDA实现的两个非常大的向量相加操作 并对其中使用到的CUDA函数进行了解说 各个文件内容如下 common hpp ifndef FBC CUDA TEST COMMON HPP define F
  • mybatis的一些特殊符号标识(大于,小于,等于,不等于)

    特殊字符 替代符号 红色基本为常用的 amp lt lt gt gt quot apos 小于等于 a lt b a lt b a b 大于等于 a gt b a gt b a b 不等于 a ba b a
  • 【allegro 17.4软件操作保姆级教程十一】表贴器件封装制作

    个人主页 highman110 作者简介 一名硬件工程师 持续学习 不断记录 保持思考 输出干货内容 目录 封装组成元素 焊盘类型 表贴器件封装制作 环境设置 计算坐标 放置pin脚 绘制丝印线 放置位号和value 放置1脚标识 放置其他
  • Linux QT GUI 界面程序打包 linuxdeployqt

    说明 1 主要依赖linuxdeployqt打包工具 2 打包Linux上的QT界面软件 使其不依赖QT开发环境 linuxdeployqt安装过程 不能下载官方编译好的 appimage文件 会报告gblic的问题 下载官方源码 修改ma
  • 【深度学习】真正的即插即用!盘点11种CNN网络设计中精巧通用的“小”插件...

    作者丨皮特潘 编辑丨极市平台 导读 所谓 插件 就是要能锦上添花 又容易植入 落地 即真正的即插即用 本文盘点的 插件 能够提升CNN平移 旋转 scale等变性能力或多尺度特征提取 感受野等能力 在很多SOTA网络中都会看到它们的影子 前
  • java基础题系列(1 - 10)

    说在前面 马上就要到秋招了 也从网上获取了一些java的基础面试题 总共有100多道 为了保持状态 每天回顾10道左右 如果result有不对的地方 希望各位大佬可以指正 谢谢 20200706 by 1z 请你说说java和php的区别
  • [开发

    使用Jackson库解析JSON 在Maven项目中 在pom xml中添加以下依赖
  • Mac连接网线能够接收微信消息,浏览器不能访问网页

    1 问题 Mac连接网线能够接收微信消息 浏览器不能访问网页 访问网页 有提示 如下信息 找不到IP地址 2 解决方法 和其他人的配置进行比较 发现自己的网络设置中 DNS服务器显示的是8 8 8 8 其他人的是灰色的DNS服务器 点击高级
  • git blame命令详解

    文章目录 1 git blame用法 2 举例 1 git blame用法 git blame用来追溯一个指定文件的历史修改记录 git blame用来追溯一个指定文件的历史修改记录 它能显示任何文件中每行最后一次修改的提交记录 所以 如果
  • mybatis---设置typeAliasesPackage支持通配符匹配

    设置typeAliasesPackage支持 通配符匹配 mybatis的typeAliasesPackage属性的作用是 搜索指定包别名 配置了以后xml文件中的resultType和parameterType就不需要指定全类名com e
  • linux服务器网站安全狗安装教程

    1 下载服务器安全狗和服务器网站安全狗 选择好版本 http download safedog cn safedog linux64 tar gz 这个是网站安全狗的下载地址 2 进行下载 命令是 wget http down safedo
  • CTF实战30 CTF题目练习和讲解五

    该培训中提及的技术只适用于合法CTF比赛和有合法授权的渗透测试 请勿用于其他非法用途 如用作其他非法用途与本文作者无关 这一阶段我们将会接触PWN类的题目 PWN等于REVERSE 因为一开始可能很多逆向的基础都不太好 毕竟我们重点不是PW
  • 安卓学习之自定义View类和控件(Button去掉边框)

    在很多情况下 我们需要自定义属于自己的layout 毕竟很多时候我们需要把 一些经常用到的布局保存下来 在以后继续使用 避免重复代码 标题栏实例 xml代码如下 style android attr borderlessButtonStyl
  • 时空预测

    目录 线性时空预测 图时空预测 线性时空预测 这篇文章在时空预测领域 搭建了一个简单高效的线性模型 且使用了channel independence的方式进行建模 模型的整体结构如下图所示 是一个级联的结构 输入分为三个部分 tempora
  • pythonflaskmock数据_基于 Flask 的简易 Mock 平台

    Mock Server 基于Flask实现的一个简易Mock平台 使用标准json结构体编写Mock Api https github com yinquanwang MockServer Key Features 遵循Http协议 支持G
  • JavaScript(1)-JS变量、关键字、命名规范

    文章目录 前言 一 JS变量 关键字 命名规范 1 关键字 已经被JS内部使用了的 2 保留字 虽然暂时还未被使用 但将来可能会被JS内部使用 变量的命名规范 JS数据类型 JS运算符的使用 算术运算符 总结 前言 JavaScript是一
  • 【数据结构】多项式相加

    问题描述 编写一个程序用单链表存储多项式 并实现两个一元多项式A与B相加的函数 A B刚开始是无序的 A与B之和按降序排列 例如 多项式A 1 2X 0 2 5X 1 3 2X 3 2 5X 5 多项式B 1 2X 0 2 5X 1 3 2
  • 电话号码对应英文单词 (python)

    在电话号码输入数字 输出他所有的单词组合 解法 1 循环法 这里假设电话号码只有3位 那么可以使用3个for循环来进行输出 c ABC DEF GHI JKL MNO PQRS TUV WXYZ 分别代表着0 1 2 3 4 5 6 7 8
  • 如何在C++中删除文件

    include
  • 浅析RPC与WebService

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 虽然现在非常火的RPC技术以SpringCloud和Dubbo为主流 但是如果做接口调用 还是逃不了要用一些较传统的技术 前几天在做接口调用时恰巧用到了WebService