虽然现在非常火的RPC技术以SpringCloud和Dubbo(x)为主流,但是如果做接口调用,还是逃不了要用一些较传统的技术。前几天在做接口调用时恰巧用到了WebService的相关技术(8,9两节是真实的开发),正好都在这里写一写。
----| RPC(Remote Procedure Call),远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。(来自百度百科)
----| RPC允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不需要显式编码这个远程调用的细节。(来自CSDN博客:https://blog.csdn.net/mindfloating/article/details/39473807)
从上面的概念中,能大概总结出RPC的基本特点如下:
在底层去看,RPC其实就是将流从一台计算机传输到另外一台计算机,无论是基于传输协议(http、tcp、udp等等)和网络IO(bio、nio)来实现。
参考资料:https://www.cnblogs.com/cainiao-Shun666/p/9181903.html
由于本文主要介绍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)
实际上一个WebService可以看做一个独立的功能,供外界使用。
其实,WebService通过http协议发送请求和接收结果时,发送的请求内容和结果内容都采用xml格式封装,并增加了一些特定的http消息头,以说明 http消息的内容格式,这些特定的http消息头和xml内容格式就是SOAP协议。
UDDI 的目的是为电子商务建立标准;UDDI是一套基于Web的、分布式的、为Web Service提供的、信息注册中心的实现标准规范,同时也包含一组使企业能将自身提供的Web Service注册,以使别的企业能够发现的访问协议的实现标准。
本文将使用原生的JWS和Apache的CXF框架来分别介绍如何使用WebService
为了与后面的CXF框架使用同一工程,这里直接用Maven搭建Provider-Demo工程
创建服务提供方的Maven工程,因为工程要使用Spring与CXF整合,所以打包方式为war
import javax.jws.WebService;
/**
* 基于JWS的WebService服务提供者
* @Title JWSProvider
* @author LinkedBear
*/
@WebService
public class JWSProvider {
}
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服务发布成功。。。");
}
}
浏览器输入http://localhost/getRandomCode?wsdl后,可以看到该服务的WSDL说明书,服务发布成功。
5.0.4.RELEASE
3.2.6
org.apache.cxf
cxf-core
${cxf.version}
org.apache.cxf
cxf-rt-frontend-jaxws
${cxf.version}
org.apache.cxf
cxf-rt-transports-http
${cxf.version}
org.apache.cxf
cxf-rt-transports-http-jetty
${cxf.version}
org.springframework
spring-core
${spring.version}
org.springframework
spring-beans
${spring.version}
org.springframework
spring-context
${spring.version}
org.springframework
spring-expression
${spring.version}
org.springframework
spring-context-support
${spring.version}
org.springframework
spring-webmvc
${spring.version}
org.springframework
spring-aspects
${spring.version}
cxf
org.apache.cxf.transport.servlet.CXFServlet
config-location
classpath:applicationContext-cxf.xml
cxf
/webservice/*
注意@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();
}
}
注意!如果Spring的依赖导不全,启动时会报错!
浏览器输入http://localhost:8080/WebService-Provider-Demo/webservice/nameHashCodeServer?wsdl后,可以看到该服务的WSDL说明书,服务发布成功。
先创建服务调用方的Maven工程。
由于调用方只负责使用服务,故打包方式为jar即可。
同样要导入CXF和Spring的依赖。
服务发布好后,下一步就是如何调用服务。
下面将分3部分介绍WebService服务的调用方式
在jdk安装目录下,找到bin目录,除了我们日常熟悉的javac,javap,javadoc等常用命令之外,还有一个wsimport命令:
使用该命令,可以将指定的WSDL转化为本地源码+编译后的字节码文件。
常用的命令参数如下:
最常用的命令行语句为:
wsimport –s .
此处的.指的是生成源码的位置为当前文件夹
我们将刚刚写好的基于JMS的WebService服务的WSDL取出,用wsimport命令生成本地源码,效果如下:
http://localhost/getRandomCode?wsdl
从生成的源码中可以看出,GetRandomCode.java为服务的调用类,要想使用服务,必定会使用到它。
但如果你想直接创建这个类的对象去调用,那你只会扑一场空:
这个类怎么没有对应的方法呢?
既然你觉得这个类是我们发布的服务名,那它是不是可以类比于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);
}
}
使用CXF框架,在生成本地源码时要使用CXF自己的方式生成。
CXF提供的解析命令为wsdl2java。
我们在一开始使用wsimport时,即便不进入jdk的目录也能正常使用,是因为我们配置了PATH的环境变量。
如果想在任意位置使用CXF的wsdl2java命令,也需要配置环境变量。但考虑到这个命令用的频率实在太低,故不配置,直接进入到CXF的目录下。
从官网下载CXF的框架包,解压之后,可以从bin目录下找到wsdl2java.bat。
wsdl2java命令的常用命令参数如下:
执行wsdl2java命令,可以将WSDL转换成.java(没有.class)
我们只需要唯一的那个接口就可以了,不需要全部的源文件。
但是删除其他源码后,接口会报错,原因是接口上有一个@XmlSeeAlso注解,需要传入ObjectFactory的class对象,这里并不需要这个Factory,直接删掉即可,只保留一对空的花括号。
之所以在服务调用方也用到了Spring,是因为CXF与Spring配合时,可以只留下接口文件,利用Spring的服务注入,来进行远程调用。
在src/main/resource目录下,创建applicationContext.xml文件,将CXF的服务注入到Spring的容器中(原理是Spring会创建这个接口的代理对象,通过代理对象去远程调用服务端的接口)。
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);
}
}
前面两种方式,最大的缺点是需要生成本地源码,费时费力不说,万一服务端改了接口的入参规则,你这边也要重新生成,再修改业务代码。
那我们就想:能不能有一种方式,可以不生成本地源码的前提下,也能调用WebService服务呢?
可以!使用HttpClient,配合SOAP协议,可以实现不生成本地源码的前提下,也能调用WebService服务。
我们说,WebService是基于SOAP协议的,我们使用本地源码发送的请求,其实也就是这些基于SOAP的POST请求,收到的响应也是基于SOAP的响应。
那么,如果我们自己构造基于SOAP协议的POST请求,是不是服务也就可以正常返回结果呢?当然是肯定的!
不过,唯一不太好的是:自行构造源码,获得响应后需要自行解析响应体。
使用HttpClient,就需要导入HttpClient的依赖(注意不要导错了)。
commons-httpclient
commons-httpclient
3.1
<[method] xmlns="[namaspace]">
<[args]>[text][args]>
[method]>
上面的格式中,方括号内的标识为具体WebService的请求。
举个简单的栗子吧:
url为http://localhost/getRandomCode?wsdl
里面的namespace为自行指定,不一定是请求路径的某一段。。。
LinkedBear
要想使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());
}
}
public class App {
public static void main(String[] args) throws Exception {
String url = "http://localhost/getRandomCode?wsdl";
StringBuilder sb = new StringBuilder();
sb.append("");
sb.append(" ");
sb.append(" ");
sb.append(" LinkedBear ");
sb.append(" ");
sb.append(" ");
sb.append(" ");
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);
}
}
请求结果(响应体真的没有换行符号,直接一行出来了。。。):
LinkedBear0.0681470650599495
我们完全可以使用Dom4j来提取响应体的数据,但是Dom4j只能一层一层的扒,太费劲。我推荐大家使用Jsoup进行xml转换和提取。
导入Jsoup的jar如下:
org.jsoup
jsoup
1.11.3
之后向刚才的源码追加如下内容,便可以只输出想要的返回结果。
Document document = Jsoup.parse(soapResponseData);
String text = document.getElementsByTag("return").text();
System.out.println(text);
//输出结果:
//LinkedBear0.0681470650599495
本来上面已经做完了,但是为了后期方便,就封装一套工具吧!这样以后就可以方便调用了。
设计的原则:尽可能的符合“开放-封闭原则”。
最基本的工具类,只需要将刚才上面列出来的SOAP协议请求体数据提取出一个Map,封装进即可。
初步封装如下:
public class WebServiceUtil {
public static void invokeWebService(Map map) throws Exception {
String url = (String) map.get("url");
StringBuilder sb = new StringBuilder();
sb.append("");
sb.append(" ");
//传入method和namespace
sb.append(" <" + map.get("method") + " xmlns=\"" + map.get("namespace") + "\">");
//动态构造参数和值
Map argsMap = (Map) map.get("argsMap");
for (Map.Entry entry : argsMap.entrySet()) {
sb.append(" <" + entry.getKey() + ">" + entry.getValue() +"" + entry.getKey() + ">");
}
sb.append(" " + map.get("method") + ">");
sb.append(" ");
sb.append(" ");
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 returnTagList = (List) map.get("returnTagList");
for (String returnTag : returnTagList) {
Elements tags = document.getElementsByTag(returnTag);
for (Element tag : tags) {
System.out.println(tag.text());
}
}
}
private WebServiceUtil() {
}
}
上面的封装有很多不妥之处:
针对以上问题,需要构造一个WebServiceSoapBean的数据结构,用来封装请求中出现的数据。
二度封装后的源码如下(为了更方便的做非空判定,加入了commons-lang):
public class WebServiceSoapBean {
private String url;
private String method;
private String namespace;
private Map argsMap;
private List 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("");
sb.append(" ");
//传入method和namespace
sb.append(" <" + bean.getMethod() + " xmlns=\"" + bean.getNamespace() + "\">");
//动态构造参数和值
Map argsMap = bean.getArgsMap();
for (Map.Entry entry : argsMap.entrySet()) {
sb.append(" <" + entry.getKey() + ">" + entry.getValue() +"" + entry.getKey() + ">");
}
sb.append(" " + bean.getMethod() + ">");
sb.append(" ");
sb.append(" ");
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 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() {
}
}
上面的封装,对于数据的处理没有什么太大的问题了,但是打印出来的响应数据却会比较乱。
接下来要将响应体的数据提取单独分离出一个方法,且提取返回类型应为Map或json。
继续添加fastjson的依赖(JSONObject比Map更强大,且有序,底层LinkedHashMap实现)。
三度封装后的源码如下(只有一个新加的方法):
//新加的方法:
private static List parseResponseXmlToJson(String xml, List 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 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;
}
目前的设计已经大体具备了一个通用工具类应该有的功能,这样就结束了吗?
需求:个性化定制WebService请求,包括额外的参数、额外的校验、额外的提取规则等等???
那通用工具类就远远不够了,需要进行纵向扩展。
在面向对象的思想中,纵向扩展的实现方案是:继承和多态。
那么接下来我们来分析:通用工具类和个性化定制的请求类,到底差别在哪里?如何分离共性?如何实现个性?
这就需要使用模板方法模式进行更深层次的通用抽取。
既然使用模板方法模式,就不能再做静态工具类了,而是纵向扩展的类继承体系。
最终封装的模板类如下(解释一个问题:这个类没有设计成抽象类,是因为即便没有特殊的需求下,普通WebService也可以直接通过这个类去实例化对象,调用服务接口,而不是每次都要创建一个匿名内部类):
public class WebServiceTemplateUtil {
public void invokeWebService(WebServiceSoapBean bean) throws Exception {
checkBeanIsComplete(bean);
String url = bean.getUrl();
StringBuilder sb = new StringBuilder();
sb.append("");
//模板方法切入点:有加入head部分的需求可以让子类重写此方法
doAddSoapHead(sb, bean);
sb.append(" ");
//传入method和namespace
sb.append(" <" + bean.getMethod() + " xmlns=\"" + bean.getNamespace() + "\">");
//动态构造参数和值
Map argsMap = bean.getArgsMap();
for (Map.Entry entry : argsMap.entrySet()) {
sb.append(" <" + entry.getKey() + ">" + entry.getValue() +"" + entry.getKey() + ">");
}
sb.append(" " + bean.getMethod() + ">");
sb.append(" ");
sb.append(" ");
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 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 parseResponseXmlToJson(String xml, List 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 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://dwz.pm/7v