Web Service 入门
一、什么是web service
1.1 概念
-
它是一种跨编程语言和跨操作系统平台的远程调用技术即跨平台远程调用技术。将调用方称为客户端,将被调方称为服务端,客户端通过网络通信协议访问服务端提供的接口。
-
xml是web service的跨语言、跨平台的基础,XML主要的优点在于它既与平台无关,又与厂商无关。
-
web service采用标准SOAP(Simple Object Access Protocol) 协议传输,soap属于w3c标准。Soap协议是基于http的应用层协议,soap协议传输是xml数据。
-
XSD,W3C为webservice制定了一套传输数据类型,使用xml进行描述,即XSD(XML Schema Datatypes),任何编程语言写的web service接口在发送数据时都要转换成web service标准的XSD发送。
-
WSDL 是基于 XML 的用于描述Web Service及其函数、参数和返回值。通俗理解Wsdl是web service的使用说明书。
1.2应用场景
因为web service使用xml作为传输数据格式,xml是可跨平台跨语言的。因此对于要支持不同语言的系统交互时可以使用web service作为接口使用。web service表面上是一个应用程序,外界不用关心里面的具体实现,只需要获得它的API描述文档,传递对应的参数就可以返回需要的数据。在使用上解决了客户与服务的独立、
二、我的第一个WebService应用
2.1 准备工作
我这里一开始常见项目的时候用的是SpringBoot模块,其实是用普通的maven项目即可,但是要自己配置tomcat。在SpringBoot项目的基础下,maven依赖如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
2.1 创建服务
服务接口
@WebService
public interface WeatherService {
public String query(String city);
}
接口实现
@WebService
public class WeatherServiceImpl implements WeatherService {
@Override
public String query(String city) {
return city + "大晴天";
}
}
2.2 发布接口
@SpringBootApplication
public class WebserviceApplication {
public static void main(String[] args) {
SpringApplication.run(WebserviceApplication.class, args);
Endpoint.publish("http://localhost:8081/weatherservice",new WeatherServiceImpl());
}
}
我这里直接在SpringBoot中的主函数中发布接口,端口会与SpringBoot中的默认端口冲突,所以发布的地址中使用的接口是8081。
2.3 查看发布的xml接口文档
整个springboot项目启动之后,在浏览器中输入http://localhost:8081/weatherservice/wsdl会得到下面的xml信息
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<!-- Published by JAX-WS RI (http://jax-ws.java.net). RI's version is JAX-WS RI 2.2.9-b130926.1035 svn-revision#5f6196f2b90e9460065a4c2f4e30e065b245e51e. -->
<!-- Generated by JAX-WS RI (http://jax-ws.java.net). RI's version is JAX-WS RI 2.2.9-b130926.1035 svn-revision#5f6196f2b90e9460065a4c2f4e30e065b245e51e. -->
<definitions xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsp="http://www.w3.org/ns/ws-policy" xmlns:wsp1_2="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://WebServiceImpl.achao.com/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://WebServiceImpl.achao.com/" name="WeatherServiceImplService">
<types>
<xsd:schema>
<xsd:import namespace="http://WebServiceImpl.achao.com/" schemaLocation="http://localhost:8081/weatherservice?xsd=1"/>
</xsd:schema>
</types>
<message name="query">
<part name="parameters" element="tns:query"/>
</message>
<message name="queryResponse">
<part name="parameters" element="tns:queryResponse"/>
</message>
<portType name="WeatherServiceImpl">
<operation name="query">
<input wsam:Action="http://WebServiceImpl.achao.com/WeatherServiceImpl/queryRequest" message="tns:query"/>
<output wsam:Action="http://WebServiceImpl.achao.com/WeatherServiceImpl/queryResponse" message="tns:queryResponse"/>
</operation>
</portType>
<binding name="WeatherServiceImplPortBinding" type="tns:WeatherServiceImpl">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
<operation name="query">
<soap:operation soapAction=""/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
</binding>
<service name="WeatherServiceImplService">
<port name="WeatherServiceImplPort" binding="tns:WeatherServiceImplPortBinding">
<soap:address location="http://localhost:8081/weatherservice"/>
</port>
</service>
</definitions>
2.4 创建客户端调用服务端接口
- 新建一个项目(模块),idea中可以直接选择webserviceclient,然后输入这个接口文档的地址(期间保持服务端在开启状态),之后idea会自动根据这个接口文档中的信息生成相关的类。
- 测试接口
这里有两种方法调用目标接口:
- 实例化接口服务类,获得接口实例对象。
WeatherServiceImplService service = new WeatherServiceImplService();
System.out.println(service.getPort(WeatherServiceImpl.class).query("广工"));
- 通过服务类创建接口服务类,获得接口实例对象
QName qName = new QName("http://WebServiceImpl.achao.com/", "WeatherServiceImplService");
Service service = Service.create(new URL("http://localhost:8081/weatherservice?wsdl"), qName);
WeatherServiceImpl port = service.getPort(WeatherServiceImpl.class);
System.out.println(port.query("深圳"));
这两种方法本质上是一样的,因为接口服务类本身是继承自Service的,里面已经封装好了Qname 和 对应的Url并传递到了Service中。
- 调用后的输出效果:
三、 访问网络中提供的webservice服务(手机号查询)
3.1 准备工作
-
网上的web服务,找一个手机归属地查询的复位接口文档地址 http://ws.webxml.com.cn/WebServices/MobileCodeWS.asmx?WSDL
-
创建一个客户端(与上面的方法一样),对接口文档地址绑定生成相关的类。
3.2 运行接口方法
- 学会看wsdl文档,懂得各个接口的作用,使用上面的方法调用这个客户端的接口(这里的接口是MobileCodeWSSoap)
- 使用目标接口的方法,查的手机号的归属地信息
public class HelloWorldClient {
public static void main(String[] argv) {
MobileCodeWS mobileCodeWS = new MobileCodeWS();
MobileCodeWSSoap port = mobileCodeWS.getPort(MobileCodeWSSoap.class);
System.out.println(port.getMobileCodeInfo("15907537109",""));
}
}
- 运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6HyVUmaz-1605096790361)(C:\Users\achao\AppData\Roaming\Typora\typora-user-images\image-20200909234409557.png)]
四、访问网络中提供的webservice服务(天气预报)
4.1 解决调用接口文档错误
webservice接口文档地址:http://ws.webxml.com.cn/WebServices/WeatherWS.asmx?wsdl
客户端调用这个接口文档的时候会出现一个错误:
这是Java调用.NET生成的WebService 时会出现的错误。因为这个服务时使用.NET开发的。
解决方案:
我们只需要将这个wsdl描述文件保存到本地,然后做一些更改:
- 将wsdl文件中的:<s:element ref=“s:schema”/><s:any/> 修改为<s:any minOccurs=“2” maxOccurs="2 "/>即可
- 之后使用绝对路径的方式来生成对应的客户端代码即可
4.2 这里wsdl文件一直报错;搁置中。。。
五、通过注解规范化发布接口
之所以要使用注解规范化开发接口是应为在不用的环境下传递过来的参数需要映射的问题,因为参数要让人看懂就必须规范化命名,参数规范化命名,那么你后台加上注解去规范化命名参数。实际地开发中经常可以遇到,所以这个规范化开发不能忽视。
5.1 @WebService注解
在要实现对外发布的接口上使用这个注解,这个注解里面的属性及相关的作用如下所示:
-
name
- wsdl:portType(接口类型) 的名称。缺省值为 Java 类或接口的非限定名称。(字符串)
-
targetNamespace
- 指定从 Web Service 生成的 WSDL 和 XML 元素的 XML 名称空间。缺省值为从包含该 Web Service 的包名映射的名称空间。(字符串)
-
serviceName
- 指定 Web Service 的服务名称:wsdl:service。缺省值为 Java 类的简单名称 + Service。(字符串)
-
endpointInterface
- 指定用于定义服务的抽象 Web Service 约定的服务端点接口的限定名。如果指定了此限定名,那么会使用该服务端点接口来确定抽象 WSDL 约定。(字符串)
-
portName
- wsdl:portName。缺省值为 WebService.name+Port。(字符串)
-
wsdlLocation
- 指定用于定义 Web Service 的 WSDL 文档的 Web 地址。Web 地址可以是相对路径或绝对路径。(字符串)
5.2 @WebMethod
在使用了@WebService注解的类里面的方法上使用,表示该方法对外发布。里面的属性及相关的作用如下所示:
- operationName
- 指定此方法相匹配的wsdl:operation的名称。缺省值为该Java方法的名称(字符串)
- action
- 定义此操作的行为。对于SOAP绑定,此值将确定SOAPAction头的值,缺省值为Java方法的名称(字符串)
- exclude
- 指定是否从WebService中排除某一方法,即对不对外发布这个方法。缺省值为false(布尔值)
5.3@WebParam
这个注解使用在对外发布的方法的括号里面,后面紧跟着映射的参数。对在生成的xml文件中对发布方法中的参数起到一个解释说明的作用。主要的就是设置name属性值,用于在xml中显示属性的名称。
5.4 @WebResult
注解使用在对外发布的方法上,主要用来设置里面的name属性,对该方法中的返回值做一个说明。
六、CXF的使用
对于CXF的简单理解,它就是为了简化WebService开发的一种框架。就像spring一样,简化了我们日常的Java开发。
6.1 准备工作
这里我是用的是SpringBoot项目,因为SpringBoot会自动使用内嵌的tomcat所以比较方便。使用CXF需要的maven如下:
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-core</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<version>3.4.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.cxf/cxf-rt-transports-http -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http</artifactId>
<version>3.4.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.cxf/cxf-rt-transports-http-jetty -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId> </artifactId>
<version>3.4.0</version>
</dependency>
接口代码:
// 接口类
@WebService
public interface HellowInterface {
@WebMethod
public String hellow_ws(@WebParam(name = "role") String str);
}
// 实现类(与使用endpoint发布不同之处,使用CXF的时候,接口实现类上不需要使用@Webservice注解
public class Hellow implements HellowInterface {
public String hellow_ws(String str) {
System.out.println("你好" + str);
return "hellow" + str;
}
}
6.2 发布服务
服务端代码:
public class Service {
public static void main(String[] args) {
Hellow hellow = new Hellow(); // 初数化一个接口实例
JaxWsServerFactoryBean factoryBean = new JaxWsServerFactoryBean();
factoryBean.setServiceClass(HellowInterface.class); // 设置web服务的类型
factoryBean.setAddress("http://127.0.0.1:8081/hellow"); // 设置发布的地址
factoryBean.setServiceBean(hellow); // 设置接口实例
factoryBean.create(); // 创建发布
}
}
6.3 使用服务
通过上面2.4的方法,将wsdl文件解析到客户端生成对应的class。在客户端中使用下面的代码完成为webservice接口的调用;
public class Client {
public static void main(String[] args) {
JaxWsProxyFactoryBean factoryBean = new JaxWsProxyFactoryBean();
factoryBean.setServiceClass(HellowInterface.class);
factoryBean.setAddress("http://127.0.0.1:8081/hellow");
HellowInterface hellow = (HellowInterface) factoryBean.create();
String achao = hellow.hellowWs("achao");
System.out.println(achao);
}
}
这里留下一个疑问,怎么才能在控制台输出发布接口中的"System.out.println(“你好” + str)"删输出语句
希望看到我这篇博客的大佬能告知一下。
6.4 CXF中的基本数据类型传输
通过上面客户端的实现,我们可以知道,web服务中是可以传输String的数据类型的,其实它都可以传输基本地数据类型,我们需要考虑的是,它支不支持实体类和集合的传输呢?直接动手!
传输实体类对象
// 新建一个实体类people
public class People {
private String name;
private int age;
public People() {
}
public People(String name, int age) {
this.name = name;
this.age = age;
}
}
// 其他方法省略
// 在接口上添加这个获取people的方法(注意要在接口类上添加注解)
public People getPeople(){
return new People("achao", 18);
}
// 重新解析一下wsdl文件,在客户端使用web服务
public class Client {
public static void main(String[] args) {
JaxWsProxyFactoryBean factoryBean = new JaxWsProxyFactoryBean();
factoryBean.setServiceClass(HellowInterface.class);
factoryBean.setAddress("http://127.0.0.1:8081/hellow");
HellowInterface hellow = (HellowInterface) factoryBean.create();
// String achao = hellow.hellowWs("achao");
// System.out.println(achao);
People people = hellow.getPeople();
System.out.println(people.toString());
}
}
// 输出结果;People{age=18, name='achao'}
结论: 当我们在要发布的接口中添加了这个获取people实体类的方法时,对应解析后生成的类中也将people这个类生成了下来,也就是说wsdl文件中包含了这个实体类的信息,最后得出的结论就是:CXF中可以传输实体类!
传输集合类对象
单纯得去传输简单的集合类对象是可行的,但是当两者嵌套般的进行传输的时候,会发生错误:
// 接口实现类中添加方法
public Map<String, List<People>> getMap() {
List<People> list = new ArrayList<>();
list.add(new People("achao",18));
Map<String, List<People>> map = new HashMap<>();
map.put("achao", list);
return map;
}
// 这里使用的返回值是Map<String, List<People>>类型的,当我们的服务端想对外发布的时候,就报了个错误:
// java.util.List是接口, 而 JAXB 无法处理接口。
在我的测试中,使用的上面的依赖版本时,当单独使用Map<String, People>的时候,是可以传输过去的,但是对应生成的结构有了些变化(返回的不是Map类,但是可以获取里面的数据);当使用Map<String, ArrayList>的时候,也是可以传输过去的,但是对应生成的结构无法获取ArrayList中的数据,原因是解析生成的ArrayList类中没有对应的属性,因而造成数据丢失。
当控制台报出“java.util.List是接口, 而 JAXB 无法处理接口。”错误的时候,原因是jaxb无法处理接口为List的作为value怎么转化成为对应的实体类。但是单纯的Map还是可以转化的。这里面应该涉及它源码设计的问题,以上都只是猜测。我觉得学习有时候就是这样,推测能够合理化就是有利的。让我想起了高中物理老师的精辟言论:“你和你爸爸在人群中走散了,你记得穿黄衣服的就是你爸爸,虽然不都是你爸爸,但是总是有利于你找到你爸爸的。”
处理传输Map<String, List>类型的方法:
- 在相应的接口函数上添加注解@XmlJavaTypeAdapter:
// MapAdapter.class是一个处理Map<String, List<People>>的adapter
@WebMethod
@XmlJavaTypeAdapter(value = MapAdapter.class)
public Map<String, List<People>> getMap();
- 编写adapter;
// 编写adapter需要继承XmlAdapter类,里面是要转换的类型
public class MapAdapter extends XmlAdapter<MapReflect[], Map<String, List<People>>> {
/*
* 将 MapReflect[]转换成为 Map<String, List<People>>方法
* */
@Override
public Map<String, List<People>> unmarshal(MapReflect[] v) throws Exception {
Map<String, List<People>> retmap = new HashMap<>();
for (int i = 0; i < v.length; i++) {
MapReflect mapReflect = v[i];
retmap.put(mapReflect.getKey(),mapReflect.getValue());
}
return retmap;
}
/*
* 将 Map<String, List<People>>转换成为 MapReflect[]的方法
* */
@Override
public MapReflect[] marshal(Map<String, List<People>> v) throws Exception {
MapReflect[] mapReflects = new MapReflect[v.size()];
Set<Map.Entry<String, List<People>>> entries = v.entrySet();
int i=0;
for (Map.Entry<String, List<People>> entry : entries) {
MapReflect reflect = new MapReflect();
reflect.setKey(entry.getKey());
reflect.setValue(entry.getValue());
mapReflects[i] = reflect;
i++;
}
return mapReflects;
}
}
客户端测试;
public class Client {
public static void main(String[] args) {
JaxWsProxyFactoryBean factoryBean = new JaxWsProxyFactoryBean();
factoryBean.setServiceClass(HellowInterface.class);
factoryBean.setAddress("http://127.0.0.1:8081/hellow");
HellowInterface hellow = (HellowInterface) factoryBean.create();
// 拿到对应map中的key值
String key = hellow.getMap().getItem().get(0).getKey();
System.out.println(key);
// 拿到对应map中的value值
List<People> value = hellow.getMap().getItem().get(0).getValue();
System.out.println(value.get(0).toString());
}
}
// 运行结果:
// achao
// People{age=18, name='achao'}
七、CXF添加拦截器
7.1 默认的日志拦截器
CXF自带了日志拦截器,可以在客户端和服务端使用,作用是可以在控制栏看到soap请求的数据。使用方法如下(客户端同理)
Hellow hellow = new Hellow();
JaxWsServerFactoryBean factoryBean = new JaxWsServerFactoryBean();
factoryBean.setServiceClass(HellowInterface.class);
factoryBean.setAddress("http://127.0.0.1:8081/hellow");
factoryBean.setServiceBean(hellow);
factoryBean.getInInterceptors().add(new LoggingInInterceptor()); // 添加进入拦截器
factoryBean.getOutInterceptors().add(new LoggingOutInterceptor()); // 添加出去拦截器
factoryBean.create();
7.2 自定义拦截器
自定义的拦截器往往是为了访问控制,比如在服务端设置了如下的进入拦截器:
// 使用这种继承的方式来实现自定义的拦截器
public class MyInterceptor extends AbstractPhaseInterceptor<SoapMessage>{
public MyInterceptor(String phase){
super(Phase.PRE_INCOKE); // 在调用方法之前调用自定义的拦截器
}
// 这是要实现的方法,说白了这个方法就是对soap报文的操作
public void handleMessage(SoapMessage message) throws Fault{
// 获取报文的头部信息
List<header> headers = message.getHeaders();
// 对没有头部的报文进行拦截
if(headers == ull || headers.size == 0){
throw new Fault(new IllegalArfumentException("没有Header, 拦截器实施拦截"))
}
// 其实头部只有一个
Header firstHeader = headers.get(0);
// 获取头部元素对象
Element ele = (Element) firstHeader.getObject();
// 获取头部中结点为username和password的结点
NodeList uList = ele.getElementsByTagName("userName");
NodeList pList = ele.getElementByTagName)("password");
if(uList.getLength != 1){
throw new Fault(new IllegalArfumentException("用户名格式不对"))
}
if(pList.getLength != 1){
throw new Fault(new IllegalArfumentException("密码格式不对"))
}
// 通过这个节点来取得这个节点里面的元素的值
String username = uList.get(0).getTestContent();
String password = pList.get(0).getTextContent();
// 接下来可以对这对用户名和密码进行检验的操作....
}
}
看完了上面的代码,我们可以清晰地认识到其实这个拦截器的作用就是对报文的解析,在上面的自定义拦截器中,是将服务端发送过来的soap报文中的header中包含的账号密码的结点进行取值,当然之前还做了结点的合法性检测,那么在客户端的拦截器中就要对出去的报文进行拦截器的设置了:
public class AddSoapHeader extends AbstractSoapInterceptor {
public void handleMessage(SoapMessage message) throws Fault {
List<Header> headerList = message.getHeaders();
Document doc = DOMUtils.createDocument();
// 这是创建了一个元素(节点)
Element ele = doc.creareElement("authHeader");
// 创建username节点
Element uElement = doc.createElement("userName");
// 节点赋值
uElement.setTextContent("userName");
// 创建password节点
Element pElement = doc.createElement("password");
// 节点赋值
pElement.setTextContent("password");
// 作为author的子节点
ele.appendChild(uElement)
ele.appendChild(pElement);
// 添加到头部当中去
headerList.add(new Header(new QName("header", ele)));
}
}
八、总结:
从WebService的发布开始,配合使用相应的注解,这些注解在生成内容为xml格式的网页的时候,将一些标签更友好化的展现出来;从这方面的原理上来看,使用webservice接口的调用人员只需要了解那些标签的含义,然后使用对应的方法的时候,将方法需要的参数封装成soap报文中的节点,这样自己构造soap报文发送给客户端进行请求使用接口时,服务端也能解析传过来的参数。
经常说WebService是一种规范,刚开始接触的时候并不知道这个规范是什么,通过这一轮的学习之后,上面的这段话就是我总结出来的WebService遵循的规范,这便是一种由深到浅的过程吧!
后面紧接着学习使用了CXF框架,相对于WebService规范,我相信大部分的人对框架理解还是有的,框架就是一套简化开发的工具,它有自己一套独立的使用步骤,但是底层还是没有变的。所以根据框架的特性,CXF框架无非是在获取到WebService接口对应的class文件的时候,在使用的过程中,直接使用这些接口类的方法,直接传递这些实体类的参数,获取到的数据更是直接封装好了,这个过程就是cxf把那些烦人的构造soap报文发送请求的过程,给你封装在了底层,让你更加直观、更加方便地调用WebService服务。