添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
耍酷的红烧肉  ·  テスト85 #ExcelVBA - Qiita·  56 分钟前    · 
逆袭的打火机  ·  C# URL Encode (How It ...·  10 小时前    · 
刚分手的马铃薯  ·  WebUtility.UrlEncode(S ...·  10 小时前    · 
闯红灯的煎鸡蛋  ·  HttpUtility.UrlEncode ...·  10 小时前    · 
坏坏的红茶  ·  Improve your Psycopg2 ...·  15 小时前    · 
踏实的匕首  ·  Lyrics | Artists | ...·  3 月前    · 
绅士的创口贴  ·  ORA-29024: ...·  7 月前    · 

Spring Boot 2系列(五十八):集成CXF实现Web Service详解

前言:很早之前有接触和开发过Web Service 服务,但近些年在互联网行业几乎看不到 Web Service 服务了,互联网行业几乎都采用 HTTP + JSON 对外提供数据服务。

但并不意味着 Web Service 已消失(迟早的事),一些传统垂直行业的系统仍然使用 Web Service。

例如,医院的 HIS(医院信息系),10年前的系统大把的,大量外围业务系统和服务商依赖于它。

依然在使用 Web Service 技术,个人认为有以下两点:

  • 一是这类系统一经部署就很难更换,因为更换的成本和风险都非常高,高到难以接受,导致市场固化,新兴企业更好的技术更优的产品就难以打入该行业市场,同样意味着对于复杂的业务缺少打磨产品的市场环境。

  • 二是提供该类系统的服务商和甲方并不会主动也没有意愿去更换系统,只要能满足业务需要,更多的是在上面迭代新的功能。

    与开发新系统相比,收入固定的,但付出的成本是最低,也就没有内在的驱动力去研发新技术和新产品了。

    提供垂直行业信息系统的服务商实际是不多的,行业领域内可能就那么几家,采用的技术和提供的功能都可能是相似的(可能来自某一头部企业离职人员创业开发)。

    随着业务的发展,这类系统必然存在局限性的。在技术层面是没有跟上行业技术发展的,在业务层面是也难以满足新形态的业务需求。

    那更换该类系统的驱动可能需要来自顶层设计,例如,提出行业新的概念,制定准入规则;或大型IT企业切入该行业市场,对行业提出新的解读并提供整套更优的解决方案,从外部提供更多的赋加值,提供全方位的支持和补贴等。

    近期项目需要对接HIS,花了点时间重新研究了下 Web Service 的应用,在此做个记录

    Web Service

    Web Service 相关概念不做过多描述,可参考 百度百科-Web Service

    Web Service 体系架构有三个角色:

  • 服务提供者(Provide):发布服务,服务提供方,提供服务和服务描述,向UDDI注册中心注册;响应消费者的服务请求。
  • 服务消费者(Consumer):消费服务,服务的请求方,向UDDI注册中心查询服务,拿到返回的描述信息生成相应的 SOAP 消息,发送给服务提供者,实现服务调用。
  • 服务注册中心(Register):服务注册中心( UDDI )。
  • Axis2与CXF

    基于 Java 开发 Web Service 的框架主要有 Axis2 CXF 。如果需要多语言的支持,应该选择 Axis2 ;如果实现侧重于 Java 并希望与 Spring 集成,特别是把 WebService 嵌入到其他程序中,CXF 是更好的选择,CXF 官方为集成 Spring 提供了方便。

    Apache CXF 官网: http://cxf.apache.org/

    Axis2 与 CXF 区别:

    Axis2 Web Service 引擎 轻量级的SOA框架,可以作为ESB(企业服力总线) Sprign 集成 Web应用 服务的管控与治理

    Web Service 四个注解

    JDK 为 Web Service 提供了四个注解:**@WebService,@WebMethod,@WebParam,@WebResult**,位于 JDK 的 javax.jws 包下。

    @WebService

    作用在接口或实现类上

  • 作用在类上是声明一个 Web Service 服务实现。
  • 作用在类上是定义一个 Web Service 端点接口。
  • 原码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package javax.jws;

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    public @interface WebService {
    String name() default "";

    String targetNamespace() default "";

    String serviceName() default "";

    String portName() default "";

    String wsdlLocation() default "";

    String endpointInterface() default "";
    }
  • name :String,指定 Web Service 的名称,映射到 WSDL 1.1 的 wsdl:portType name ,默认值为 Java 类或接口的非限定名称。

    1
    <wsdl:portType name="HelloMessageServer">....</wsdl:portType>
  • serviceName : String,指定 Web Server 的服务名(对外发布的服务名),映射到 WSDL 1.1 的 wsdl:service name 。在端点接口上不允许有此成员值。

    默认值为端点接口 实现类的非限定类名 + Service 。例如, HelloServiceImplService。

    1
    <wsdl:service name="HelloServiceImplService">...</wsdl:service>
  • portName :String,指定 Web Service 的端口名,映射到 WSDL 1.1 的 wsdl:port name 。在端点接口上不允许有此成员值。默认值为 WebService.name+Port 。例如,HelloMessageServerPort。

    1
    2
    3
    4
    5
    <wsdl:service name="HelloServiceImplService">
    <wsdl:port binding="tns:HelloServiceImplServiceSoapBinding" name="HelloMessageServerPort">
    <soap:address location="http://localhost:8080/services/ws/hello"/>
    </wsdl:port>
    </wsdl:service>
  • targetNamespace :String,指定名称空间,默认是 http:// + 端点接口的包名倒序,映身到 wsdl 的 wsdl:definitions xs:schema 标签的 targetNamespace xmlns:tns

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <wsdl:definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://tempuri.org" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:ns1="http://schemas.xmlsoap.org/soap/http" name="HelloServiceImplService" targetNamespace="http://tempuri.org">
    <wsdl:types>
    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://tempuri.org" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://tempuri.org" version="1.0">
    ...
    </xs:schema>
    ...
    </wsdl:types>
    ...
    </wsdl:definitions>
  • endpointInterface : String,指定端点接口的全限定名。如果是没有接口,直接写实现类的,该属性不用配置。

    此属性允许开发人员将 接口 实现 分离。如果存在此属性,则 服务端点接口 将用于确定抽象 WSDL 约定(portType 和 bindings)。

    服务端点接口可以包括 JSR-181 注解 ,以定制从 Java 到 WSDL 的映射。

    服务实现 bean 可以实现服务端点接口,但不是必须。

    如果此成员值不存在,则从服务实现 bean 上的注释生成 Web Service 约定。如果目标环境需要服务端点接口,则将其生成到一个实现定义的包中,并具有一个实现定义的名称。

    在端点接口上不允许此成员值。

  • wsdlLocation :String,指定 Web Service 的 WSDL 描术文档的 Web 地址(URL),可以是相对路径或绝对路径。

    wsdlLocation 值的存在指示 服务实现 Bean 正在实现预定义的 WSDL 约定。 如果服务实现 bean 与此WSDL 中声明的 portType 和 bindings 不一致,则 JSR-181 工具必须提供反馈。

    请注意,单个 WSDL 文件可能包含多个 portType 和多个 bindings。 服务实现 bean 上的注释确定特定的portType 和与 Web Service 对应的 bindings。

    注意 :实现类上可以不添加 Webservice 注解。另 @WebService.targetNamespace 注解的使用官方还有如下说明,待深入理解:

  • 如果 @WebService.targetNamespace 注解作用在服务端点接口上,targetNamespace 被 wsdl:portType namespace 使用(并关联 XML 元素)。

  • 如果 @WebService.targetNamespace 注解作用在服务实现的 Bean 上,没有引用服务端点接口(通过 endpointInterface 属性),targetNamespace 被 wsdl:portType wsdl:service 使用(并关联 XML 元素)。

  • 如果 @WebService.targetNamespace 注解作用在服务实现的 Bean 上,引用了服务端点接口(通过 endpointInterface 属性),targetNamespace 只被 wsdl:service 使用(并关联 XML 元素)。

    @WebMethod

    作用在使用了 @WebService 注解的 接口 实现类 中的方法上。

    定义一个暴露为 Web Service 的操作方法。方法必须是 public,其参数返回值,异常必须遵循JAX-RPC 1.1 第5 节中定义的规则,方法不需要抛出 java.rmi.RemoteException 异常。

    原码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package javax.jws;

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    public @interface WebMethod {
    String operationName() default "";

    String action() default "";

    boolean exclude() default false;
    }
  • action :String,此操作的行为。

    对于 SOAP 绑定,这决定了 soapAction 的值。

    1
    <soap:operation soapAction="" style="document"/>
  • exclude :boolean,标记方法不暴露为 Web Service 的方法。默认值是 false,即不排除。

    用于停止继承的方法作为 Web Service 的一部分暴露公开。如果指定了此属性,则不能为 @WebMethod 指定其他元素。

    该成员属性不允许用在端口方法上。

  • operationName :String,匹配 wsdl:operation name ,默认为方法名。

    1
    2
    3
    4
    5
    6
    7
    <wsdl:portType name="HelloMessageServer">
    <wsdl:operation name="helloMessageServer">
    <wsdl:input message="tns:helloMessageServer" name="helloMessageServer"> </wsdl:input>
    <wsdl:output message="tns:helloMessageServerResponse" name="helloMessageServerResponse"> </wsdl:output>
    <wsdl:fault message="tns:Exception" name="Exception"> </wsdl:fault>
    </wsdl:operation>
    </wsdl:portType>
  • @WebResult

    定制返回值到 WSDL part 和 XML 元素的映射。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <wsdl:message name="Exception">
    <wsdl:part element="tns:Exception" name="Exception"> </wsdl:part>
    </wsdl:message>
    <wsdl:message name="helloMessageServerResponse">
    <wsdl:part element="tns:helloMessageServerResponse" name="parameters"> </wsdl:part>
    </wsdl:message>
    <wsdl:message name="helloMessageServer">
    <wsdl:part element="tns:helloMessageServer" name="parameters"> </wsdl:part>
    </wsdl:message>

    原码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package javax.jws;

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    public @interface WebResult {
    String name() default "";

    String partName() default "";

    String targetNamespace() default "";

    boolean header() default false;
    }
  • name :String,返回值的名称。

    如果 operation 是 rpc 风格,且 @WebResult.partName 未指定,此返回值则代表 wsdl:part name 值。

    如果 operation 是 document 风格,或者返回值映射到 header,则代表 XML 元素的 local name 值。

  • partName :String,此返回值表示的 wsdl:part name
    仅当 operation 为 rpc 风格,或 operation 为 document 风格且参数风格为 BARE 时才使用此选项。

  • targetNamespace :String,该返回值的 XML 名称空间(namespace)。

    仅当 operation 为 document 风格 或 返回值映射到 header 时。当 target namespace 设置为空字符串( "" ),该值表示为空名称空间(empty namespace)。

  • header :boolean,默认 false。如果为 true,则从消息头而不是消息正文中提取结果。

    @WebParam

    自定义 @WebMethod 注解作用的方法的一个参数到 Web Service message part 和 XML element 的映射。

    原码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package javax.jws;

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.PARAMETER})
    public @interface WebParam {
    String name() default "";

    String partName() default "";

    String targetNamespace() default "";

    WebParam.Mode mode() default WebParam.Mode.IN;

    boolean header() default false;

    public static enum Mode {
    IN,
    OUT,
    INOUT;

    private Mode() {
    }
    }
    }
  • header :boolean,默认 false。如果为 true,则从消息头而不是消息正文中提取结果。

  • mode :WebParam.Mode,参数的流动方向,枚举值有:IN,OUT,INOUT。

    只能为符合 Holder 类型定义的参数类型指定 OUT 和 INOUT(JAX-WS 2.0 [5], section 2.3.3) 。

    Holder 类型的参数必须指定为 OUT 或 INOUT。

  • name :String,参数名

    如果 operation 是 rpc 风格的,且未指定 @WebParam.partName ,则表示 wsdl:part name
    如果 operation 是 document 风格,或者参数映射到 header,则表示 XML element 的 local name。

    如果 operation 是 document 风格,参数风格是 BARE ,并且模式是 OUT 或 INOUT,则必须指定名称。

  • partName :String, wsdl:part name 代表此参数。

    仅当 operation rpc 风格 或 operation 为 document 风格,且参数风格为 BARE 时才使用此选项。

  • targetNamespace :String,参数的 XML 名称空间(namespace)。

    仅当 operation 是 document 风格,或参数映射到 header 时才使用。

    如果 target namespace 设置为 "" ,则表示空名称空间。

    Web Service 配置

    Bus 配置

    Apache CXF 核心架构是以 BUS 为核心,整合其他组件。为共享资源提供一个可配置的场所,作用类似于 Spring 的 ApplicationContext ,这些共享资源包括 WSDL管理器、绑定工厂等。通过对BUS进行扩展,可以方便地容纳自己的资源,或者替换现有的资源。

    默认 Bus 实现基于 Spring 架构,通过依赖注入,在运行时将组件串联起来。 BusFactory 负责 Bus 的创建。默认的BusFactory 是 SpringBusFactory ,对应于默认的 Bus 实现。在构造过程中,SpringBusFactory 会搜索 META-INF/cxf (包含在 CXF 的jar中)下的所有bean配置文件。根据这些配置文件构建一个 ApplicationContext 。开发者也可以提供自己的配置文件来定制 Bus。

    Bus 是 CXF 的主干。它管理扩展并充当拦截器提供者。Bus 的拦截器将被添加到在 Bus上(在其上下文中)创建的所有客户端和服务器端点的相应 入站 出站 消息和 错误 拦截器链中。

    默认情况下,它不会向这些拦截器链类型中的任何一个提供拦截器,但是可以通过 配置文件 或 Java 代码添加拦截器。

    CXF 基于 Spring Boot 的 starter (cxf-spring-boot-starter-jaxws)包的自动配置默认注册了 SpringBus Bean,所以手动注册 Bus 可以省略。

    自动配置SpringBus Bean

    1
    2
    3
    4
    5
    6
    @Configuration
    @ConditionalOnMissingBean(SpringBus.class)
    @ImportResource("classpath:META-INF/cxf/cxf.xml")
    protected static class SpringBusConfiguration {

    }

    classpath:META-INF/cxf/cxf.xml ,文件位于 org.apache.cxf:cxf-core:version/META-INF/cxf/cxf.xm

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- For Testing using the Spring commons processor, uncomment one of:-->
    <!--
    <bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor"/>
    <context:annotation-config/>
    -->
    <bean id="cxf" class="org.apache.cxf.bus.spring.SpringBus" destroy-method="shutdown"/>
    <bean id="org.apache.cxf.bus.spring.BusWiringBeanFactoryPostProcessor" class="org.apache.cxf.bus.spring.BusWiringBeanFactoryPostProcessor"/>
    <bean id="org.apache.cxf.bus.spring.Jsr250BeanPostProcessor" class="org.apache.cxf.bus.spring.Jsr250BeanPostProcessor"/>
    <bean id="org.apache.cxf.bus.spring.BusExtensionPostProcessor" class="org.apache.cxf.bus.spring.BusExtensionPostProcessor"/>
    </beans>

    注册 SpringBus Bean

    1
    2
    3
    4
    5
    // 可省略
    @Bean(name = Bus.DEFAULT_BUS_ID)
    public SpringBus springBus() {
    return new SpringBus();
    }

    Interceptor 配置

    Java 配置拦截器,如下

    CXF官方: https://cxf.apache.org/docs/interceptors.html

    拦截器是 CXF 内部的基本处理单元。调用服务时,会创建并调用 InterceptorChain。每个拦截器都有机会对消息做他们想做的事。这可以包括读取它、转换它、处理标头、验证消息等。

    拦截器可用于 CXF 客户端和 CXF 服务器。

  • 当 CXF 客户端调用 CXF 服务器时,有一个用于客户端的传出拦截器链和一个用于服务器的传入链。
  • 当服务器将响应发送回客户端时,服务器有一条传出链,客户端有一条传入链。此外,对于 SOAPFaults,CXF Web 服务将创建一个单独的出站错误处理链,而客户端将创建一个入站错误处理链。
  • CXF 中的一些拦截器示例包括:

  • SoapActionInterceptor:处理 SOAPAction header 并选择一个操作(如果已设置)。
  • StaxInInterceptor:从 transport 输入流创建一个 Stax XMLStreamReader。
  • Attachment(In/Out)Interceptor:将 multipart/related 消息转换为一系列附件。
  • 拦截器链分为阶段。每个拦截器运行的阶段在拦截器的构造函数中声明。每个阶段可能包含许多拦截器。

    InterceptorProviders

    CXF 内部的几个不同组件可以为 InterceptorChain 提供拦截器。 这些实现了 InterceptorProvider 接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface InterceptorProvider {

    List<Interceptor> getInInterceptors();

    List<Interceptor> getOutInterceptors();

    List<Interceptor> getOutFaultInterceptors();

    List<Interceptor> getInFaultInterceptors();
    }

    要将拦截器添加到拦截器链中,需要将其添加到拦截器提供者之一。

    1
    2
    MyInterceptor interceptor = new MyInterceptor();
    endpoint.getInInterceptors().add(interceptor);

    Endpoint 配置

    创建端点,通过端点发布服务。

    1
    2
    3
    4
    5
    6
    7
    8
    @Bean
    public Endpoint endpoint() {
    EndpointImpl endpoint = new EndpointImpl(springBus(), helloService);
    endpoint.publish("/ws/hello");
    // 添加拦截器
    endpoint.getInInterceptors().add(interceptor);
    return endpoint;
    }

    Endpoint 是个抽象类,表示一个 Web Service 端点。

    Endpoints 通过内部定义的静态方式来创建, 端点 始终与一个 Binding 和 一个 实现 绑定,两者在创建端点时设置。

    端点状态要么是 已发布 未发布 publish 方法可用于开始发布端点,此时端点开始接受传入的请求。

    可以在端点上设置 Executor ,以便更好地控制用于调度(dispatch)的传入请求的线程。例如,可以通过 ThreadPoolExecutor 并将其注册到端点来启用具有某些参数的线程池。

    可以通过端点获取 Binding 来设置处理链(handler chain)。

    端点可以有一个元数据文档列表,比如 WSDL 和 XMLSchema 文档,绑定到它。在发布时,JAX-WS 实现将尽可能多地重用元数据,而不是基于实现者上的注释生成新的元数据。

    EndpointImpl

    是 Endpoint 端点的默认实现,端点的很多属性都靠 EndpointImpl 来设置。

    注册CXF Servlet Bean

    CXF 基于 Spring Boot 的 starter (cxf-spring-boot-starter-jaxws)包的自动配置默认注册了 ServletRegistrationBean,所以手动注册可以省略。

    ServletRegistrationBean 作用

  • 用于在 Servlet 3.0+ 容器中注册一个 CXFServlet

  • 设置请求URL前缀映射,默认是 /services ,可以在 application.properties 配置文件中自定义

    1
    2
    #wsdl访问地址为 http://127.0.0.1:8080/services/xxxx?wsdl
    cxf.path=/services

    也可以关闭前缀映射,ServletRegistrationBean 构造方法提供了控制前缀开关的参数 alwaysMapUrl,默认为 true,即使用前缀映射;可以手动配置注册 ServletRegistrationBean Bean,设置 alwaysMapUrl=false 关闭前缀映射。

    1
    2
    3
    4
    5
    6
    7
    public ServletRegistrationBean(T servlet, boolean alwaysMapUrl, String... urlMappings) {
    Assert.notNull(servlet, "Servlet must not be null");
    Assert.notNull(urlMappings, "UrlMappings must not be null");
    this.servlet = servlet;
    this.alwaysMapUrl = alwaysMapUrl;
    this.urlMappings.addAll(Arrays.asList(urlMappings));
    }
  • 设置容器加载 Servlet 的优先顺序,必须在调用 onStartup 之前指定 Servlet。

    1
    cxf.servlet.load-on-startup=-1
  • ServletRegistrationBean 自动配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Bean
    @ConditionalOnMissingBean(name = "cxfServletRegistration")
    public ServletRegistrationBean<CXFServlet> cxfServletRegistration() {
    String path = this.properties.getPath();
    String urlMapping = path.endsWith("/") ? path + "*" : path + "/*";
    ServletRegistrationBean<CXFServlet> registration = new ServletRegistrationBean<>(
    new CXFServlet(), urlMapping);
    CxfProperties.Servlet servletProperties = this.properties.getServlet();
    registration.setLoadOnStartup(servletProperties.getLoadOnStartup());
    for (Map.Entry<String, String> entry : servletProperties.getInit().entrySet()) {
    registration.addInitParameter(entry.getKey(), entry.getValue());
    }
    return registration;
    }

    手动注册 ServletRegistrationBean:

    1
    2
    3
    4
    @Bean
    public ServletRegistrationBean dispatcherServlet() {
    return new ServletRegistrationBean(new CXFServlet(), "/soap/*");
    }

    默认前缀是 /services ,wsdl 访问地址为 http://127.0.0.1:8080/services/ws/api?wsdl,注意 /ws/api 是服务暴露的访问端点。

    上面改为手动注册后,wsdl 的访问地址为 http://127.0.0.1:8080/soap/ws/api?wsdl,列出服务列表:http://127.0.0.1:8080/soap/

    如果是自定义 Servlet 实现,需要在启用类添加注解:*@ServletComponentScan*。如果启动时出现错误:

    1
    not loaded because DispatcherServlet Registration found non dispatcher servlet dispatcherServlet

    可能是 springboot 与 cfx 版本不兼容。

    Web Service Server

    Web Service 服务端的本质是接收符合 SOAP 规范的 XML消息,解析 XML 进行业务处理,返回符合 SOAP 规范的 XML。

    基于 Spring Boot + CXF 实现 Web Service 服务提供者。

    添加依赖

    基于 Spring Boot,在 pom.xml 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    <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.apache.cxf</groupId>
    <artifactId>cxf-spring-boot-starter-jaxws</artifactId>
    <version>3.4.2</version>
    </dependency>
    <dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-transports-http</artifactId>
    <version>3.4.2</version>
    </dependency>
    <!--common utils start-->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.67</version>
    </dependency>
    <dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.8.0</version>
    </dependency>
    <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.10</version>
    </dependency>
    <dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.4</version>
    </dependency>
    <dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.5.1</version>
    </dependency>
    <!--common utils end-->

    服务接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * @desc 服务端点接口
    */
    @WebService(name = "HelloMessageServer", targetNamespace = "http://tempuri.org")
    public interface HelloService {
    @WebMethod
    Object helloMessageServer(@WebParam(name = "input1") String input1,
    @WebParam(name = "input2") String input2)
    throws Exception;
    }

    接口实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * @desc 服务实现
    */
    @Service
    @WebService(name = "HelloMessageServer", targetNamespace = "http://tempuri.org",
    endpointInterface = "com.webservice.service.HelloService")
    public class HelloServiceImpl implements HelloService {

    @Override
    public Object helloMessageServer(String input1, String input2) throws Exception {
    return input1 + "," + input2;
    }
    }

    发布服务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    package com.webservice.config;

    import com.webservice.service.HelloService;
    import org.apache.cxf.Bus;
    import org.apache.cxf.bus.spring.SpringBus;
    import org.apache.cxf.jaxws.EndpointImpl;
    import org.apache.cxf.transport.servlet.CXFServlet;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.web.servlet.ServletRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    import javax.servlet.Servlet;
    import javax.xml.ws.Binding;
    import javax.xml.ws.Endpoint;

    /**
    * @author gxing
    * @desc Web Service Config
    * @date 2021/2/26
    */
    @Configuration
    public class WebServiceConfig {

    // 服务端点
    @Autowired
    private HelloService helloService;

    /**
    * 自定义 Spring Bus, 可省略, 自动配置已注册
    * @return SpringBus
    */
    @Bean(name = Bus.DEFAULT_BUS_ID)
    public SpringBus springBus() {
    return new SpringBus();
    }

    /**
    * 自定义CXF Servlet,设置前缀, 默认是 /services,
    * 可省略,直接在 application.properties中设置 cxf.path=/services
    * @return ServletRegistrationBean
    */
    /* @Bean
    public ServletRegistrationBean<Servlet> dispatcherServlet() {
    return new ServletRegistrationBean<>(new CXFServlet(), "/soap/*");
    }*/

    @Bean
    public Endpoint endpoint() {
    EndpointImpl endpoint = new EndpointImpl(springBus(), helloService);
    // 发布服务
    endpoint.publish("/ws/hello");;
    return endpoint;
    }
    }

    查看 wsdl

    GET 请求: http://localhost:8080/services/ws/hello?wsdl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    <wsdl:definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://tempuri.org" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:ns1="http://schemas.xmlsoap.org/soap/http" name="HelloServiceImplService" targetNamespace="http://tempuri.org">
    <wsdl:types>
    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://tempuri.org" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://tempuri.org" version="1.0">
    <xs:element name="helloMessageServer" type="tns:helloMessageServer"/>
    <xs:element name="helloMessageServerResponse" type="tns:helloMessageServerResponse"/>
    <xs:complexType name="helloMessageServer">
    <xs:sequence>
    <xs:element minOccurs="0" name="input1" type="xs:string"/>
    <xs:element minOccurs="0" name="input2" type="xs:string"/>
    </xs:sequence>
    </xs:complexType>
    <xs:complexType name="helloMessageServerResponse">
    <xs:sequence>
    <xs:element minOccurs="0" name="return" type="xs:anyType"/>
    </xs:sequence>
    </xs:complexType>
    <xs:element name="Exception" type="tns:Exception"/>
    <xs:complexType name="Exception">
    <xs:sequence>
    <xs:element minOccurs="0" name="message" type="xs:string"/>
    </xs:sequence>
    </xs:complexType>
    </xs:schema>
    </wsdl:types>
    <wsdl:message name="Exception">
    <wsdl:part element="tns:Exception" name="Exception"> </wsdl:part>
    </wsdl:message>
    <wsdl:message name="helloMessageServerResponse">
    <wsdl:part element="tns:helloMessageServerResponse" name="parameters"> </wsdl:part>
    </wsdl:message>
    <wsdl:message name="helloMessageServer">
    <wsdl:part element="tns:helloMessageServer" name="parameters"> </wsdl:part>
    </wsdl:message>
    <wsdl:portType name="HelloMessageServer">
    <wsdl:operation name="helloMessageServer">
    <wsdl:input message="tns:helloMessageServer" name="helloMessageServer"> </wsdl:input>
    <wsdl:output message="tns:helloMessageServerResponse" name="helloMessageServerResponse"> </wsdl:output>
    <wsdl:fault message="tns:Exception" name="Exception"> </wsdl:fault>
    </wsdl:operation>
    </wsdl:portType>
    <wsdl:binding name="HelloServiceImplServiceSoapBinding" type="tns:HelloMessageServer">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <wsdl:operation name="helloMessageServer">
    <soap:operation soapAction="" style="document"/>
    <wsdl:input name="helloMessageServer">
    <soap:body use="literal"/>
    </wsdl:input>
    <wsdl:output name="helloMessageServerResponse">
    <soap:body use="literal"/>
    </wsdl:output>
    <wsdl:fault name="Exception">
    <soap:fault name="Exception" use="literal"/>
    </wsdl:fault>
    </wsdl:operation>
    </wsdl:binding>
    <wsdl:service name="HelloServiceImplService">
    <wsdl:port binding="tns:HelloServiceImplServiceSoapBinding" name="HelloMessageServerPort">
    <soap:address location="http://localhost:8080/services/ws/hello"/>
    </wsdl:port>
    </wsdl:service>
    </wsdl:definitions>

    elementFormDefault

    WSDL 的 schema 中的 elementFormDefault 有两种值,默认是 unqualified 的,表示入参报文是不受限的。

    elementFormDefault="qualified" 时,表示入参的 XML 报文是受限的,即入参子标签是需要带前缀。

    如下,入参 input 标签 tem 就是前缀。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <SOAP-ENV:Envelope
    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:tem="http://tempuri.org">
    <SOAP-ENV:Body>
    <tem:helloMessageServer>
    <tem:input2>Hello</tem:input2>
    <tem:input1>World</tem:input1>
    </tem:helloMessageServer>
    </SOAP-ENV:Body>
    </SOAP-ENV:Envelope>

    Spring Boot 集成的 Web Service 服务,要将 WSDL 中的 elementFormDefault 设置为 qualified 的实现方式是比较另类的。

    暴露端点接口 的包中创建一个普通文件,文件名必须是 package-info.java ,注意不是 java 文件,然后编辑文件内容如下:

    1
    2
    3
    4
    @javax.xml.bind.annotation.XmlSchema(namespace = "http://tempuri.org",
    attributeFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED,
    elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)
    package com.webservice.service;

    attributeFormDefault 设置好像不起作用。

    请求接口

    使用 postman 发送请求 Web Service 暴露的端口接口:

    请求方式: POST ;请求体 Body:选择 raw 类型, XML 格式。

    URL: http://localhost:8080/services/ws/hello

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <SOAP-ENV:Envelope
    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:tem="http://tempuri.org">
    <SOAP-ENV:Body>
    <tem:helloMessageServer>
    <tem:input2>Hello</tem:input2>
    <tem:input1>World</tem:input1>
    </tem:helloMessageServer>
    </SOAP-ENV:Body>
    </SOAP-ENV:Envelope>

    响应结果:

    1
    2
    3
    4
    5
    6
    7
    <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
    <helloMessageServerResponse xmlns="http://tempuri.org">
    <return xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Hello,World</return>
    </helloMessageServerResponse>
    </soap:Body>
    </soap:Envelope>

    Web Service Client

    添加依赖

    与 Web Service Server 中的依赖一致。

    入参实体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    /**
    * @desc Service 接口请求体
    */
    @Data
    public class ServiceRequest {

    /**
    * service 接口地址
    */
    @NotEmpty(message = "wsdUrl 不能为空!")
    private String wsdUrl;
    /**
    * 接口方法名
    */
    @NotEmpty(message = "operationName 不能为空!")
    private String operationName;
    /**
    * XML 格式入参
    */
    private HashMap<String, String> paramsMap;
    /**
    * 方法入参
    * 注意:入参顺序与 service 接口的参数顺序必须一致
    */
    private List<Object> paramList;

    /**
    * 名称空间uri
    */
    private String namespaceURI = "http://tempuri.org";

    /**
    * 名称空间前缀
    * 示例:<tem:helloMessageServer> tem 就是 prefix
    */
    private String prefix = "tem";

    /**
    * 子标签是否有前缀
    */
    private boolean qualified = true;
    }

    调用实现

    Web Service 服务端的本质是接收符合 SOAP 规范的 XML消息,解析 XML 进行业务处理,返回符合 SOAP 规范的 XML。

    Java 端调用 Web Servcie 可以有多种方式:

  • 基于 CXF 的客户端调用

    CXF 客户端供了调用 Web Service 的多个 invoke 方法。

    1
    Object[] invoke(QName operationName, Object... params) throws Exception;

    注意: invoke 的参数入参是数组,是不含参数名的,所以数组入参的顺序必须与服务端的接口入参一致。

  • 基于原生的 SOAP 调用 【推荐此方式调用】

    其步骤主要有:获取连接工厂,通过工厂创建连接;获取报文工厂,通过工厂创建报文消息,在消息报文里添加设置报文头,正文,组装XML节点等;最后使用连接(入参消息报文和目标wsdUrl)调用远程服务。返回结果的数据是 SOAP 原生的 XML 格式数据。

    SOAP 调用的底层是组装 xml 请求报文发送 POST 请求。与上面 Web Service Server 章节的【请求接口】处理一致。

  • 使用代理类工厂的方式

    该方式需要 Web Service 服务端提供接口的 jar 包供客户端引入,客户端通过代理工厂对目标接口创建一个代理实现,通过代理来接口。

    不建议此方式,Web Service 服务端的实现可能是跨语言跨平台的,非 Java 语言就不支持,或服务商并不会提供这样的 jar 包。

    Controller 接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    /**
    * @desc 接口入口
    */
    @RestController
    public class WebServiceController {

    @Autowired
    private IService service;

    /**
    * CXF客户端动态调用
    *
    * @param serviceRequest
    * @return
    */
    @PostMapping("/cxfInvoke")
    public Object service(@Validated @RequestBody ServiceRequest serviceRequest) {
    return service.cxfInvoke(serviceRequest);
    }

    /**
    * SOAP连接调Web Service接口
    *
    * @param serviceRequest
    * @return
    */
    @PostMapping("/soapInvoke")
    public Object soapInvoke(@Validated @RequestBody ServiceRequest serviceRequest) {
    return service.soapInvoke(serviceRequest);
    }
    }

    业务接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * @desc 业务接口
    */
    public interface IService {

    /**
    * CXF客户端动态调用
    *
    * @param serviceRequest
    * @return
    */
    Object cxfInvoke(ServiceRequest serviceRequest);

    /**
    * SOAP连接调Web Service接口
    *
    * @param serviceRequest
    * @return
    */
    Object soapInvoke(ServiceRequest serviceRequest);
    }

    业务接口实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    package com.webservice.service.impl;

    import com.alibaba.fastjson.JSON;
    import com.webservice.common.utils.MapXmlUtil;
    import com.webservice.common.utils.WebServiceExecutor;
    import com.webservice.entity.ServiceRequest;
    import com.webservice.service.IService;
    import org.apache.commons.io.output.ByteArrayOutputStream;
    import org.apache.cxf.endpoint.Client;
    import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import org.springframework.stereotype.Service;
    import org.springframework.util.CollectionUtils;
    import org.springframework.util.ObjectUtils;

    import javax.xml.namespace.QName;
    import javax.xml.soap.*;
    import java.io.IOException;
    import java.nio.charset.Charset;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;

    /**
    * @desc 调用 WebService
    */
    @Service
    public class ServiceImpl implements IService {
    private static final Logger logger = LogManager.getLogger(ServiceImpl.class);

    /**
    * CXF客户端动态调用
    *
    * @param serviceRequest
    * @return
    */
    public Object cxfInvoke(ServiceRequest serviceRequest) {
    try {
    List<Object> paramList = serviceRequest.getParamList();
    Object[] params = CollectionUtils.isEmpty(paramList) ? new Object[0] : paramList.toArray();

    String wsdUrl = serviceRequest.getWsdUrl();
    String operationName = serviceRequest.getOperationName();
    //请求
    logger.info("Web Service Request, wsdUrl:{}, operationName:{}, params:{}", wsdUrl, operationName, JSON.toJSONString(params));
    String result = WebServiceExecutor.invokeRemoteMethod(wsdUrl, operationName, params)[0].toString();
    logger.info("Web Service Response:{}", result);
    return result;
    } catch (Exception e) {
    e.printStackTrace();
    throw new RuntimeException(e);
    }
    }

    /**
    * SOAP连接调Web Service接口
    *
    * @param serviceRequest
    */
    public Object soapInvoke(ServiceRequest serviceRequest) {
    String wsdUrl = serviceRequest.getWsdUrl();
    String namespaceURI = serviceRequest.getNamespaceURI();
    String operationName = serviceRequest.getOperationName();
    String prefix = serviceRequest.getPrefix();
    HashMap<String, String> paramsMap = serviceRequest.getParamsMap();

    ByteArrayOutputStream outputStream = null;
    try {
    SOAPConnectionFactory factory = SOAPConnectionFactory.newInstance();
    SOAPConnection connection = factory.createConnection();
    MessageFactory messageFactory = MessageFactory.newInstance();
    //消息体
    SOAPMessage message = messageFactory.createMessage();
    SOAPPart part = message.getSOAPPart();
    SOAPEnvelope envelope = part.getEnvelope();
    envelope.addNamespaceDeclaration(prefix, namespaceURI);

    SOAPHeader header = message.getSOAPHeader();
    header.detachNode();
    SOAPBody body = message.getSOAPBody();

    QName bodyName = new QName(namespaceURI, operationName, prefix);
    SOAPBodyElement element = body.addBodyElement(bodyName);

    // 组装请求参数
    for (Map.Entry<String, String> entry : paramsMap.entrySet()) {
    QName sub = null;
    if (serviceRequest.isQualified()) {
    sub = new QName(namespaceURI, entry.getKey(), prefix);
    } else {
    sub = new QName(entry.getKey());
    }
    SOAPElement childElement = element.addChildElement(sub);
    childElement.addTextNode(entry.getValue());
    }

    //请求体
    outputStream = new ByteArrayOutputStream();
    message.writeTo(outputStream);
    String requestBody = outputStream.toString(Charset.defaultCharset());
    logger.info("Web Service Request Body:{}", requestBody);

    //执行请求
    SOAPMessage result = connection.call(message, wsdUrl);
    logger.info("Request Web Service End.");
    //响应体
    outputStream.reset();
    result.writeTo(outputStream);
    String responseBody = outputStream.toString(Charset.defaultCharset());
    logger.info("Web Service Response:{}", responseBody);

    //响应文本
    org.w3c.dom.Node firstChild = result.getSOAPBody().getFirstChild();
    org.w3c.dom.Node subChild = firstChild.getFirstChild();
    String textContent = subChild.getTextContent();
    logger.info("Web Service Response textContent:{}", textContent);
    return textContent;
    } catch (SOAPException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    if (!ObjectUtils.isEmpty(outputStream)) {
    try {
    outputStream.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    throw new RuntimeException("请求 Web Service 异常");
    }

    // /**
    // * 代理类工厂的方式
    // * Web Service 服务端需要提供接口的jar包供客户端引入
    // * 需要拿到对方的接口地址, 同时需要引入接口
    // */
    // public Object proxyFactoryInvoke(ServiceRequest serviceRequest) {
    // try {
    // // 接口地址
    // String address = serviceRequest.getWsdUrl();
    // // 代理工厂
    // JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean();
    // // 设置代理地址
    // jaxWsProxyFactoryBean.setAddress(address);
    // // 设置接口类型
    // jaxWsProxyFactoryBean.setServiceClass(HelloService.class);
    // // 创建一个代理接口实现
    // HelloService us = (HelloService) jaxWsProxyFactoryBean.create();
    // // 调用代理接口的方法调用并返回结果
    // String result = (String) us.helloMessageServer("Hello", "World");
    // System.out.println(result);
    // return result;
    // } catch (Exception e) {
    // e.printStackTrace();
    // }
    // throw new RuntimeException("请求 Web Service 异常");
    // }

    /**
    * CXF客户端动态调用
    */
    public Object cxfClientInvoke() {
    // 创建动态客户端
    JaxWsDynamicClientFactory clientFactory = JaxWsDynamicClientFactory.newInstance();
    Client client = clientFactory.createClient("http://localhost:8080/services/ws/hello?wsdl");

    // 需要密码的情况需要加上用户名和密码
    // client.getOutInterceptors().add(new ClientLoginInterceptor(USER_NAME, PASS_WORD));
    Object[] objects = new Object[0];

    List<Object> paramList = new ArrayList<>();
    paramList.add("Hello");
    paramList.add("World");

    Object[] params = paramList.toArray();

    try {
    // invoke("方法名",参数1,参数2,参数3....);
    //这里注意如果是复杂参数的话,要保证复杂参数可以序列化
    objects = client.invoke("helloMessageServer", params);
    System.out.println("Service Response:" + JSON.toJSONString(objects[0]));
    } catch (Exception e) {
    e.printStackTrace();
    }
    return objects[0];
    }

    }

    客户端请求

  •