SOAP协议和JAXWS

WebService可以理解为一种用于信息交换的网络接口标准,调用WebService接口其实就是通过标准化的通信协议在不同的应用之间进行交互。在JavaEE中,WebService通常指的是基于SOAP或RESTful风格的网络服务,它们广泛用于企业信息系统之间的分布式集成和协作。对于SOAP协议,JavaEE规范提供了JAX-WS标准用于构建基于SOAP的WebService。在服务端,开发者可以使用JAX-WS标准接口来定义、实现和发布WebService,在客户端则可以自动基于WSDL生成符合JAX-WS规范的客户端代码。

这篇笔记我们学习SOAP协议的基础知识,以及JavaEE中JAX-WS的使用。本章节基于当前最新的JDK8、JavaEE8版本和Wildfly14应用服务器,Wildfly14中JAX-WS的实现框架是CXF,后续章节我们也会介绍如何在Spring工程中集成CXF框架。

SOAP协议简介

SOAP(Simple Object Access Protocol)是一种用于在计算机网络上交换结构化信息的通信协议,最初由Microsoft、IBM和其他公司于1998年共同提出。SOAP是一种基于XML报文的协议,广泛用于在SOA分布式环境中进行通信。SOAP消息使用XML格式编码,这使得它在不同平台和编程语言之间进行通信时更具有灵活性和互操作性。SOAP协议的另一个特点是不依赖于特定的传输协议,它可以在多种传输协议上使用,如HTTP、SMTP等,不过实际开发中还是主要基于HTTP实现。

SOAP报文结构:SOAP报文通常由三个部分组成:envelope(信封)、header(头部)和body(主体)。envelope是消息的根元素,header包含一些可选的元数据,而body包含实际的消息内容,下面是一组请求报文和响应报文的例子。

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:ser="http://service.demows.demo.gacfox.com/">
    <soapenv:Header/>
    <soapenv:Body>
        <ser:queryWeather>
            <city>DemoCity</city>
        </ser:queryWeather>
    </soapenv:Body>
</soapenv:Envelope>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
        <ns1:queryWeatherResponse xmlns:ns1="http://service.demows.demo.gacfox.com/">
            <weather>
                <city>DemoCity</city>
                <date>2017-07-18T14:22:41.030+09:00</date>
                <temperature>17</temperature>
                <weather>晴</weather>
            </weather>
        </ns1:queryWeatherResponse>
    </soap:Body>
</soap:Envelope>

WSDL(Web Services Description Language):WSDL是SOAP接口的描述文档,它也是XML格式的。WSDL提供了服务的抽象定义,包括服务支持的操作和这些操作所需的报文格式。在实际开发中,WSDL文档通常由服务提供方生成并发布到Web服务器上,客户端通过访问WSDL文档来获取服务端提供的操作和报文格式信息,像CXF、Axis2等框架都支持自动生成WSDL文档,以及基于WSDL文档自动生成Java客户端代码,基于这些框架开发WebService的服务端和客户端非常方便。下面是一个WSDL文档的例子。

<wsdl:definitions name="WeatherService" targetNamespace="http://service.demows.demo.gacfox.com/">
    <wsdl:types>
        <xs:schema targetNamespace="http://service.demows.demo.gacfox.com/" version="1.0">
            <xs:complexType name="weather">
                <xs:sequence>
                    <xs:element minOccurs="0" name="city" type="xs:string"/>
                    <xs:element minOccurs="0" name="date" type="xs:dateTime"/>
                    <xs:element name="temperature" type="xs:int"/>
                    <xs:element minOccurs="0" name="weather" type="xs:string"/>
                </xs:sequence>
            </xs:complexType>
        </xs:schema>
    </wsdl:types>
    <wsdl:message name="queryWeatherResponse">
        <wsdl:part name="weather" type="tns:weather"></wsdl:part>
    </wsdl:message>
    <wsdl:message name="queryWeather">
        <wsdl:part name="city" type="xsd:string"></wsdl:part>
    </wsdl:message>
    <wsdl:portType name="WeatherServicePortType">
    </wsdl:portType>
    <wsdl:binding name="WeatherServiceSoapBinding" type="tns:WeatherServicePortType">
        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
        <wsdl:operation name="queryWeather">
            <soap:operation soapAction="" style="rpc"/>
            <wsdl:input name="queryWeather">
                <soap:body namespace="http://service.demows.demo.gacfox.com/" use="literal"/>
            </wsdl:input>
            <wsdl:output name="queryWeatherResponse">
                <soap:body namespace="http://service.demows.demo.gacfox.com/" use="literal"/>
            </wsdl:output>
        </wsdl:operation>
    </wsdl:binding>
    <wsdl:service name="WeatherService">
        <wsdl:port binding="tns:WeatherServiceSoapBinding" name="WeatherPort">
            <soap:address location="http://localhost:8080/demows/WeatherService"/>
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>
  • <wsdl:definitions>是文档的根元素,其中标识了文档名称和命名空间。
  • <wsdl:types>中定义了SOAP服务接口中用到的数据类型,可以理解为XML和Java实体类之间的绑定映射。
  • <wsdl:message>中描述了SOAP接口的输入输出参数。
  • <wsdl:portType><wsdl:binding>描述了SOAP接口的实现细节,<wsdl:operation>描述了具体的操作,<wsdl:input><wsdl:output>描述了具体的输入输出参数。
  • <wsdl:service><wsdl:port>定义了具体SOAP接口的相关信息,包括接口名和调用地址等。

WSDL实际上还是比较繁琐和复杂的,实际开发中WSDL文档通常是框架自动生成的,我们不需要也不应该手写。实际开发中,我们通常使用JAX-WS中的一系列注解属性来自动生成WSDL文档,不过注解中许多属性的命名也都有一定的技巧,合适的命名可以避免一些不必要的麻烦,具体可以看后面的代码。

开发WebService服务端

引入Maven依赖

这里我们基于Wildfly14应用服务器开发WebService,由于应用服务器已经内置了CXF,我们只需要引入JavaEE SDK即可。

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>8.0.1</version>
    <scope>provided</scope>
</dependency>

工程搭建

下面例子中,我们编写了一个天气查询接口,它接收一个城市名称作为输入,并返回该城市的天气信息,工程目录结构如下。注意一个WebService接口通常对应于一个Java类,不过实际开发中我们通常规定采用接口和实现分离的方式,接口类通常被称作SEI(Service Endpoint Interface),SEI和实现类分离这样代码具有更好的可读性和可维护性。

|_ src/main
    |_ java
        |_ model
            |_ Weather.java
        |_ service
            |_ WeatherService.java
            |_ impl
                |_ WeatherServiceImpl.java
    |_ webapp
        |_ WEB-INF
            |_ web.xml

定义SEI接口类和实现类

package com.gacfox.demo.demows.service;

import com.gacfox.demo.demows.model.Weather;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService(name = "WeatherServicePortType",
        targetNamespace = "http://service.demows.demo.gacfox.com/")
@SOAPBinding(style = SOAPBinding.Style.RPC, use = SOAPBinding.Use.LITERAL)
public interface WeatherService {
    @WebMethod(operationName = "queryWeather")
    @WebResult(name = "weather")
    Weather queryWeather(@WebParam(name = "city") String city);
}

接口中,我们首先使用@WebService注解定义该接口对应发布一个SOAP服务,name属性对应于WSDL中的<wsdl:portType>名字,它通常对应于自动生成的客户端代码中的接口类名,个人建议以XxxServicePortType命名,targetNamespace指定了WSDL命名空间,它是一个类似网址的形式,个人建议对应于当前接口类的包名。

@SOAPBinding接口指定了两个参数:style用于设置报文风格,这里我们使用RPC风格,use表示采用的编码格式,这两个值实际开发中通常没有理由选择其它选项。

@WebMethod@WebResult@WebParam这些注解就非常好理解了,分别用于设置WebService的方法名、返回值名和参数名,通常可以对应于Java代码中的方法名、返回值类型名和参数名,@WebResult@WebParam注解不是必须的,如果不手动指定,它们也会采用这些默认值。

package com.gacfox.demo.demows.service.impl;

import com.gacfox.demo.demows.model.Weather;
import com.gacfox.demo.demows.service.WeatherService;

import javax.jws.WebService;
import java.util.Date;

@WebService(endpointInterface = "com.gacfox.demo.demows.service.WeatherService",
        targetNamespace = "http://service.demows.demo.gacfox.com/",
        serviceName = "WeatherService",
        portName = "WeatherServicePort")
public class WeatherServiceImpl implements WeatherService {
    @Override
    public Weather queryWeather(String city) {
        // ... 具体业务逻辑
        return weather;
    }
}

实现类中,我们再次使用了@WebService注解,这是因为该注解中有些属性不能放置在SEI中,注解中我们首先指定了endpointInterface,它对应于SEI接口类名。之后我们再次指定了targetNamespace,这是因为它的默认值是当前类的包名,但我们的实现类通常放在一个impl包下,如果不手动指定可能发生一些奇怪的问题,所以这里建议手动指定。serviceName是WebService的服务名,建议取SEI接口类名。portName是WebService的端口名,建议以XxxServicePort形式命名。

访问WSDL文档

假设我们工程的ContextPath是demows,此时启动服务后,我们可以访问http://localhost:8080/demows/WeatherService?wsdl查看自动生成的WSDL文档。

开发WebService客户端

引入Maven依赖

对于WebService客户端,如果我们的工程不是一个JavaEE工程,那么需要额外引入CXF框架的依赖。

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-core</artifactId>
    <version>3.2.1</version>
</dependency>

注意:由于JDK11版本后从JDK中移除了JAX-WS、JAXB等JavaEE模块,因此我们使用这些功能时还需要在依赖中额外引入它们,并且要注意JavaEE和JakartaEE之间的版本区分。

使用wsdl2java工具生成客户端代码

CXF框架的发行包中包含了一个wsdl2java工具,它用于基于WSDL文档生成客户端代码,我们可以找到它并执行以下命令。

wsdl2java -d src/main/java -autoNameResolution -encoding UTF-8 http://localhost:8080/demows/WeatherService?wsdl
  • d:指定代码的输出目录
  • -autoNameResolution:自动解决命名冲突,一些较大的有很多历史遗留问题的工程可能存在这种情况,建议使用该选项
  • -encoding:指定编码格式,这里我们将其指定为UTF-8,该编码通常需要手动指定避免默认使用GBK等造成不必要的麻烦

执行完该命令后,我们就可以将客户端代码生成到工程中了,不过实际开发中,我们也可能采用将客户端代码封装为Jar包的形式。

调用WebService客户端

下面例子代码调用了wsdl2java自动生成的客户端代码。

package com.gacfox.demo;

import com.gacfox.demo.demows.service.Weather;
import com.gacfox.demo.demows.service.WeatherService;
import com.gacfox.demo.demows.service.WeatherServicePortType;

public class Main {
    public static void main(String[] args) {
        WeatherService weatherService = new WeatherService();
        WeatherServicePortType weatherServicePortType = weatherService.getWeatherServicePort();
        Weather weather = weatherServicePortType.queryWeather("DemoCity");

        System.out.println(weather.getTemperature());
    }
}

如果一切正常,运行上述代码时将看到执行成功。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap