原 荐 浅析RPC与WebService

栏目: 服务器 · 发布时间: 6年前

内容简介:虽然现在非常火的RPC技术以SpringCloud和Dubbo(x)为主流,但是如果做接口调用,还是逃不了要用一些较传统的技术。前几天在做接口调用时恰巧用到了WebService的相关技术(8,9两节是真实的开发),正好都在这里写一写。从上面的概念中,能大概总结出RPC的基本特点如下:在底层去看,RPC其实就是将流从一台计算机传输到另外一台计算机,无论是基于传输协议(http、tcp、udp等等)和网络IO(bio、nio)来实现。

虽然现在非常火的RPC技术以SpringCloud和Dubbo(x)为主流,但是如果做接口调用,还是逃不了要用一些较传统的技术。前几天在做接口调用时恰巧用到了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的,基于Socket的,SOA架构的分布式框架)
  • 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

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

原 荐 浅析RPC与WebService

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的服务

原 荐 浅析RPC与WebService

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

原 荐 浅析RPC与WebService

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说明书,服务发布成功。

原 荐 浅析RPC与WebService

8. 怎么调用WebService服务

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

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

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

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

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

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

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

原 荐 浅析RPC与WebService

使用该命令,可以将指定的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

原 荐 浅析RPC与WebService

原 荐 浅析RPC与WebService

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

原 荐 浅析RPC与WebService

8.1.3 使用JMS的方式调用服务

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

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

原 荐 浅析RPC与WebService

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

既然你觉得这个类是我们发布的服务名,那它是不是可以类比于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);
    }
}

原 荐 浅析RPC与WebService

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

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

8.2.1 wsdl2java使用方式

CXF提供的解析命令为wsdl2java。

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

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

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

原 荐 浅析RPC与WebService

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

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

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

原 荐 浅析RPC与WebService

原 荐 浅析RPC与WebService

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

原 荐 浅析RPC与WebService

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

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

原 荐 浅析RPC与WebService

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);
    }
}

原 荐 浅析RPC与WebService

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);
    }
}

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

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

(完)


以上所述就是小编给大家介绍的《原 荐 浅析RPC与WebService》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Beginning XSLT 2.0

Beginning XSLT 2.0

Jeni Tennison / Apress / 2005-07-22 / USD 49.99

This is an updated revision of Tennison's "Beginning XSLT", updated for the new revision of the XSLT standard. XSLT is a technology used to transform an XML document with one structure into another ......一起来看看 《Beginning XSLT 2.0》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

html转js在线工具
html转js在线工具

html转js在线工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换