前言:很早之前有接触和开发过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
CXF
简述
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" > <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
配置文件中自定义
也可以关闭前缀映射,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 > <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 >
服务接口 1 2 3 4 5 6 7 8 9 10 @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 @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;@Configuration public class WebServiceConfig { @Autowired private HelloService helloService; @Bean(name = Bus.DEFAULT_BUS_ID) public SpringBus springBus () { return new SpringBus(); } @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 >
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 @Data public class ServiceRequest { @NotEmpty(message = "wsdUrl 不能为空!") private String wsdUrl; @NotEmpty(message = "operationName 不能为空!") private String operationName; private HashMap<String, String> paramsMap; private List<Object> paramList; private String namespaceURI = "http://tempuri.org" ; 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 @RestController public class WebServiceController { @Autowired private IService service; @PostMapping("/cxfInvoke") public Object service (@Validated @RequestBody ServiceRequest serviceRequest) { return service.cxfInvoke(serviceRequest); } @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 public interface IService { Object cxfInvoke (ServiceRequest serviceRequest) ; 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;@Service public class ServiceImpl implements IService { private static final Logger logger = LogManager.getLogger(ServiceImpl.class); 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); } } 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 异常" ); } public Object cxfClientInvoke () { JaxWsDynamicClientFactory clientFactory = JaxWsDynamicClientFactory.newInstance(); Client client = clientFactory.createClient("http://localhost:8080/services/ws/hello?wsdl" ); Object[] objects = new Object[0 ]; List<Object> paramList = new ArrayList<>(); paramList.add("Hello" ); paramList.add("World" ); Object[] params = paramList.toArray(); try { objects = client.invoke("helloMessageServer" , params); System.out.println("Service Response:" + JSON.toJSONString(objects[0 ])); } catch (Exception e) { e.printStackTrace(); } return objects[0 ]; } }
客户端请求
CXF 客户端请求,使用 postman 工具发送 POST 请求,请求体是 JSON:
请求地址:http://localhost:8090/cxfInvoke
1 2 3 4 5 { "wsdUrl" : "http://localhost:8080/services/ws/hello?wsdl" , "operationName" : "helloMessageServer" , "paramList" : ["Hello" ,"World" ] }
SOAP 原生调用,使用 postman 工具发送 POST 请求,请求体是 JSON:
请求地址:http://localhost:8090/soapInvoke
1 2 3 4 5 6 7 8 9 10 11 { "wsdUrl" : "http://localhost:8080/services/ws/hello?wsdl" , "operationName" : "helloMessageServer" , "prefix" :"tem" , "qualified" :true , "namespaceURI" :"http://tempuri.org" , "paramsMap" : { "input1" : "Hello" , "input2" : "World" } }
响应结果都为:Hello,World
WebServiceExecutor工具类 在 CXF 客户端调用中用到
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 import com.webservice.common.constants.SysConstants;import org.apache.commons.lang3.StringUtils;import org.apache.cxf.endpoint.Endpoint;import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory;import org.apache.cxf.service.model.BindingInfo;import org.apache.cxf.service.model.BindingOperationInfo;import org.apache.cxf.transport.http.HTTPConduit;import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;import org.springframework.util.CollectionUtils;import org.w3c.dom.Document;import org.w3c.dom.Element;import javax.xml.namespace.QName;import javax.xml.parsers.DocumentBuilder;import javax.xml.parsers.DocumentBuilderFactory;import javax.xml.transform.OutputKeys;import javax.xml.transform.Transformer;import javax.xml.transform.TransformerException;import javax.xml.transform.TransformerFactory;import javax.xml.transform.dom.DOMSource;import javax.xml.transform.stream.StreamResult;import java.io.ByteArrayOutputStream;import java.io.StringWriter;import java.util.HashMap;import java.util.List;public class WebServiceExecutor { public static String requestWebService (String operationName, String wsdUrl, HashMap<String, String> xmlMap, List<Object> list) throws Exception { if (StringUtils.isBlank(operationName)) { throw new Exception("异常:operationName必填参数为null!" ); } if (StringUtils.isBlank(wsdUrl)) { throw new Exception("异常:wsdUrl必填参数为null!" ); } String result = "" ; String xmlParam = createParamList(xmlMap); list.add(xmlParam); Object[] params = list.toArray(); result = invokeRemoteMethod(wsdUrl, operationName, params)[0 ].toString(); return result; } public static String createParamList (HashMap<String, String> paramMap) throws Exception { String result = "" ; if (!CollectionUtils.isEmpty(paramMap)) { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder db = factory.newDocumentBuilder(); Document document = db.newDocument(); String xmlRoot = paramMap.get(SysConstants.XML_ROOT); Element xml = document.createElement(xmlRoot); for (String key : paramMap.keySet()) { if (!key.equals(SysConstants.XML_ROOT)) { Element paramElement = document.createElement(key); paramElement.setTextContent(paramMap.get(key)); xml.appendChild(paramElement); } } document.appendChild(xml); result = createXmlToString(document); } return result; } private static String createParamList (String[] params) { String result = "" ; try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder db = factory.newDocumentBuilder(); Document document = db.newDocument(); Element request = document.createElement("request" ); for (String param : params) { Element paramElement = document.createElement(param); paramElement.setTextContent("?" ); request.appendChild(paramElement); } document.appendChild(request); TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer transformer = transformerFactory.newTransformer(); StringWriter writer = new StringWriter(); transformer.transform(new DOMSource(document), new StreamResult(writer)); result = createXmlToString(document); result = result.substring(result.indexOf("<request>" ), result.length()); } catch (Exception e) { } return result; } private static String createXmlToString (Document document) { String xmlString = null ; try { TransformerFactory transFactory = TransformerFactory.newInstance(); Transformer transformer = transFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8" ); transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "" ); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount" , "4" ); transformer.setOutputProperty(OutputKeys.INDENT, "yes" ); DOMSource domSource = new DOMSource(document); ByteArrayOutputStream bos = new ByteArrayOutputStream(); transformer.transform(domSource, new StreamResult(bos)); xmlString = bos.toString(); } catch (TransformerException e) { e.printStackTrace(); } return xmlString; } public static Object[] invokeRemoteMethod(String url, String operationName, Object[] parameters) throws Exception { JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance(); if (!url.endsWith("wsdl" )) { url += "?wsdl" ; } org.apache.cxf.endpoint.Client client = dcf.createClient(url); HTTPConduit conduit = (HTTPConduit) client.getConduit(); HTTPClientPolicy policy = new HTTPClientPolicy(); policy.setConnectionTimeout(10 * 1000 ); policy.setReceiveTimeout(12 * 1000 ); conduit.setClient(policy); Endpoint endpoint = client.getEndpoint(); QName opName = new QName(endpoint.getService().getName().getNamespaceURI(), operationName); BindingInfo bindingInfo = endpoint.getEndpointInfo().getBinding(); if (bindingInfo.getOperation(opName) == null ) { for (BindingOperationInfo operationInfo : bindingInfo.getOperations()) { if (operationName.equals(operationInfo.getName().getLocalPart())) { opName = operationInfo.getName(); break ; } } } Object[] res = null ; res = client.invoke(opName, parameters); return res; } }
【Demo 源码 > https://gitee.com/gxing19/spring-boot-web-service】
Web Service WSDL 相关参考
CXF 用户指南
Web Service工作原理及实例
W3school > WSDL 教程
Spring boot webService使用