目录
Apache CXF 概述
1、Java 语言原生的 JWS 可用来开发和发布 WebService 服务,不需要任何第三方库,为 Java 开发者提供便捷发布和调用 WebService 服务的途径。
2、借助一些 WebService 框架可以很轻松地把自己的业务对象发布成 WebService 服务,实现更复杂的功能,Java 语言常用的第三方 WebService 框架有:axis,xfire,cxf 等。
3、CXF(Celtix + XFire) 是 Apache 旗下重磅的 SOA (Service Oriented Architecture) 面向服务的框架,它实现了 ESB(企业服务总线),能很好的和 Spring 进行集成。
4、CXF 官网地址:Apache CXF -- Index
5、CXF 不但是一个优秀的 Web Services / SOAP / WSDL 引擎,也是一个不错的 ESB 总线,为 SOA 的实施提供了一种选择方案。虽然 Axis2 出现的时间较早,但 CXF 的追赶速度快。
6、Java JDK 自带的 JWS 开发 webService 时,方法的参数与返回值支持 Java 8 种基本类型,支持 Lsit、Set 等集合类型,Array 类型,自定义的 POJO 对象,但是不支持 Map 类型。而 CXF 可以支持 Map 类型。
Spring boot WebService 使用快速入门
1、环境:Java JDK 1.8 + Spring boot 2.0.3 + cxf-spring-boot-starter-jaxws 3.3.0。
服务端
1、pom.xml 导入 apache cxf 依赖:
<!--webservice + CXF .../start.......-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.cxf/cxf-spring-boot-starter-jaxws -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
<version>3.3.0</version>
</dependency>
<!--webservice + CXF .../end.......-->
pom.xml · 汪少棠/web_app - Gitee.com。
2、 提供 webservice 服务接口及其实现。
package com.wmx.web_app.webService;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
/**
* 单位信息 webService 远程接口
* 1、@javax.jws.WebService 定义接口为 webservice 服务。
*
* @author wangMaoXiong
* @version 1.0
* @date 2023/10/8 9:26
*/
@WebService
public interface IAgencyService {
/**
* 查询单位信息
* 1、@WebMethod 定义方法为 webservice 方法。写或不写,服务端都会将 @WebService 中的所有方法提供给客户端调用。建议全部写上。
* * operationName:操作名称,默认为方法名称。
* 2、@WebParam 定义方法参数,用于定义wsdl中的参数映射名称,方便查看与调用,否则会是 arg0、arg1 ...。建议写上。
* 3、@WebResult 定义 wsdl 文件中,服务端方法返回值名称,默认为 result,方便查看与调用。建议写上。
* @param paramJson :Json 对象字符串,复杂对象统一转成 json 对象字符串进行传输,双方接收后再转成 Json 对象
* @return :Json 对象字符串,复杂对象统一转成 json 对象字符串进行传输,双方接收后再转成 Json 对象 或者自己需要的实体对象。
*/
@WebMethod(operationName = "queryAgencyStatInfo")
@WebResult(name = "queryResult")
String queryAgencyStatInfo(@WebParam(name = "paramBody", targetNamespace = "http://webService.web_app.wmx.com/") String paramJson);
/**
* 更正单位信息
* @param paramJson :Json 对象字符串,复杂对象统一转成 json 对象字符串进行传输,双方接收后再转成 Json 对象
* @return :Json 对象字符串,复杂对象统一转成 json 对象字符串进行传输,双方接收后再转成 Json 对象 或者自己需要的实体对象。
*/
@WebMethod(operationName = "syncAgencyStatInfo")
@WebResult(name = "syncResult")
String syncAgencyStatInfo(@WebParam(name = "paramBody", targetNamespace = "http://webService.web_app.wmx.com/") String paramJson);
}
src/main/java/com/wmx/web_app/webService/IAgencyService.java · 汪少棠/web_app - Gitee.com。
package com.wmx.web_app.webService.impl;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.wmx.web_app.webService.IAgencyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jws.WebService;
/**
* 单位信息 webService 远程接口实现
* 1、@WebService 定义接口为 webservice 服务。
* * serviceName:对应生成的 WSDL 文件中的 definitions 元素中的 name 属性值,默认以服务类名称+"Service"为值。
* * targetNamespace:目标名称空间,对应 WSDL 文档的根元素 definition 元素的 targetNameSpace 属性。默认为 "http://" 加 "包名的反写"。
* * endpointInterface:终端接口,定义服务抽象 WebService 协定的服务端点接口的完整名称,通常设置为服务接口的全路径。
*
* @author wangMaoXiong
* @version 1.0
* @date 2023/10/8 9:26
*/
@WebService(serviceName = "agencyService",
targetNamespace = "http://webService.web_app.wmx.com/",
endpointInterface = "com.wmx.web_app.webService.IAgencyService")
public class AgencyService implements IAgencyService {
private static final Logger log = LoggerFactory.getLogger(AgencyService.class);
/**
* 查询单位信息
* 1、@WebMethod 定义方法为 webservice 方法
* 2、@WebParam 定义方法参数,用于定义wsdl中的参数映射
*
* @param paramJson :Json 对象字符串,复杂对象统一转成 json 对象字符串进行传输,双方接收后再转成 Json 对象。
* 如 {"user_id":"4783748PL898878","token":"37483748YU23","user_name":"蚩尤后裔"}
* @return :Json 对象字符串,复杂对象统一转成 json 对象字符串进行传输,双方接收后再转成 Json 对象 或者自己需要的实体对象
*/
@Override
public String queryAgencyStatInfo(String paramJson) {
String returnValue;
log.info("查询单位信息-开始-请求参数:\n{}", JSONUtil.toJsonPrettyStr(paramJson));
System.out.println("根据参数查询数据...");
returnValue = "{\"reason\":\"加载成功\",\"status_code\":\"1000\",\"data\":{\"msg\":\"查询成功\",\"subSystemName\":\"基础库管理\",\"subSystemCode\":\"bgt-basic-server\",\"tableList\":[{\"tableName\":\"BAS_AGENCY_EXT 单位扩展信息表\",\"tableSize\":560},{\"tableName\":\"BAS_AGENCY_INFO 单位基本信息表\",\"tableSize\":560}],\"sendStatus\":1,\"id\":\"5337a4bc06634e5491588c4fb9019787\"}}";
JSONObject jsonObject = JSONUtil.parseObj(returnValue);
return jsonObject.toString();
}
/**
* 更正单位信息
* 1、@WebMethod 定义方法为 webservice 方法
* 2、@WebParam 定义方法参数,用于定义wsdl中的参数映射
*
* @param paramJson :Json 对象字符串,复杂对象统一转成 json 对象字符串进行传输,双方接收后再转成 Json 对象。
* {"user_id":"5543748KH988878","token":"25483748YU98","user_name":"蚩尤后裔"}
* @return :Json 对象字符串,复杂对象统一转成 json 对象字符串进行传输,双方接收后再转成 Json 对象 或者自己需要的实体对象
*/
@Override
public String syncAgencyStatInfo(String paramJson) {
String returnValue;
log.info("更正单位信息-开始-请求参数:\n{}", JSONUtil.toJsonPrettyStr(paramJson));
System.out.println("根据参数更正数据...");
returnValue = "{\"reason\":\"操作成功\",\"status_code\":\"1000\",\"data\":{\"msg\":\"更正成功\",\"subSystemName\":\"基础库管理\",\"subSystemCode\":\"bgt-basic-server\",\"tableList\":[{\"tableName\":\"BAS_AGENCY_EXT 单位扩展信息表\",\"tableSize\":145},{\"tableName\":\"BAS_AGENCY_INFO 单位基本信息表\",\"tableSize\":146}],\"sendStatus\":1,\"id\":\"5337a4bc06634e5491588c4fb9019787\"}}";
JSONObject jsonObject = JSONUtil.parseObj(returnValue);
return jsonObject.toString();
}
}
src/main/java/com/wmx/web_app/webService/impl/AgencyService.java · 汪少棠/web_app - Gitee.com。
3、提供 webservice 配置类。
package com.wmx.web_app.webService;
import com.wmx.web_app.webService.impl.AgencyService;
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.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.xml.ws.Endpoint;
/**
* WebService 配置类
*
* @author wangMaoXiong
* @version 1.0
* @date 2023/10/8 9:45
*/
@Configuration
public class WebServiceConfig {
/**
* 注入 CXF servlet bean
* 1、bean 名称不能叫 dispatcherServlet,否则会覆盖MVC原本的 dispatcherServlet。
* 2、指定 webservice 服务访问的路径。所有的服务接口都会在路径下。
*
* @return
*/
@Bean(name = "cxfServlet")
public ServletRegistrationBean<CXFServlet> cxfServlet() {
// 注册servlet 拦截 /webservice 开头的请求,不设置时默认为 /services/*
return new ServletRegistrationBean<>(new CXFServlet(), "/webservice/*");
}
/**
* 业务处理类,也可以直接在实现类上标注 @Service 注解。
* @return
*/
@Bean
public IAgencyService agencyService() {
return new AgencyService();
}
@Bean(name = Bus.DEFAULT_BUS_ID)
public SpringBus springBus() {
return new SpringBus();
}
/**
* 发布 Web 服务访问端点
* 1、本方法可以提供多个,用于发布多个访问断点。
* 2、publish 方法用于开始发布端点,此时端点开始接受传入请求。
* 3、stop 方法可以用于停止接受传入请求并关闭端点。一旦停止,就无法再次发布终结点。
*
* @return
*/
@Bean
public Endpoint agencyServiceEndpoint() {
// 参数二是SEI实现类对象
EndpointImpl endpoint = new EndpointImpl(this.springBus(), this.agencyService());
// 发布服务
endpoint.publish("/agencyService");
return endpoint;
}
}
src/main/java/com/wmx/web_app/webService/WebServiceConfig.java · 汪少棠/web_app - Gitee.com。
4、启动服务可以从浏览器访问接口 http://localhost:8080/webservice/ 看到整个服务列表,同时客户端可以调用服务,比如客户端代码,或者是第三方工具(如 SoapUI)。
客户端
1、可以使用第三方工具(如 SoapUI)先测试服务端接口,接着可以自己写客户端代码。特别注意:SoapUI 工具能正常调用,并不代表服务端就一定没问题,有时候工具访问正常,但是客户端代码访问就报错,原因还是服务端配置问题。
2、客户端我使用另一个服务,环境:Java 1.8 + Spring boot 2.3.5。
3、客户端有以下常见的调用方式。
4、温馨提醒:如果使用浏览器在线访问 wsdl 地址,复制内容粘贴到本地创建 xxx.wsdl 文件时,建议使用 google Chrome 浏览器,某次使用的 firefox 复制的内容少了一些东西,导致文件内容不完整而报错。
调用方式 | 描述 |
---|---|
原生Java JDK 方式 | 先使用插件根据 wsdl 文件生成客户端代码,然后使用原生 JDK API 调用。 优点是操作简单,不用额外导入依赖,缺点是无法做复杂操作。 |
Apache cxf 代理方式 | 1、需要使用插件(Java JDK wsimport.exe、Apache cxf wsdl2java )根据服务端的 WSDL 文件生成代码并放入到客户端中。 2、优点是客户端调用时如同本地调用一样,写起来更丝滑,缺点是必须先生成客户端代码。 |
Apache cxf 动态工厂方式 | 1、直接根据服务端提供的 http://ip:port/webservice/xxx?wsdl 请求。 2、优点是调用简单,根据 wsdl 地址就可以调用,缺点是类似反射一样,写起来不直观,如。 Object[] invoke("方法名",参数1,参数2,参数3....); |
第三方工具 | 如 Hutool Soap客户端-SoapClient。 |
原生 Java JDK 方式
1、先使用工具或者插件根据服务端 wsdl 文件生成客户端代码,然后全部导入到项目中,然后编写代码调用。
/**
* 原生 Java JDK 服务工厂方式调用。
* 1、客户端不用导入任何与webservice相关的依赖包(如apache的cxf),也和 Spring boot 无关。
* 2、http://localhost:8081/wsClient/queryAgencyStatInfo1
*
* @param bodyMap : {"user_id":"15424JK411","token":"741541212","user_name":"蚩尤后裔"}
* @return
* @throws Exception
*/
@PostMapping("/wsClient/queryAgencyStatInfo1")
public Map<String, Object> queryAgencyStatInfo1(@RequestBody Map<String, Object> bodyMap) throws Exception {
// url:webservice 服务端提供的服务地址,结尾必须加 ?wsdl
URL url = new URL("http://localhost:8080/webservice/agencyService?wsdl");
/**
* QName 表示 XML 规范中定义的限定名称,QName 的值包含名称空间 URI、本地部分和前缀。
* QName(final String namespaceURI, final String localPart):指定名称空间 URI 和本地部分的 QName 构造方法。
* 1、两个参数值都可以在 wsdl 文件中的第一行中找到,如下所示,就是其中的两个属性。
* * * <wsdl:definitions name="agencyService" targetNamespace="http://webService.web_app.wmx.com/">
* 2、或者在生成的接口的实现类中也能找到,如下所示,同样是 name 和 targetNamespace 属性。
* @WebServiceClient(name = "agencyService", targetNamespace = "http://webService.web_app.wmx.com/", wsdlLocation = "http://localhost:8080/webservice/agencyService?wsdl")
*/
QName qName = new QName("http://webService.web_app.wmx.com/", "agencyService");
/**
* Service 对象提供 Web 服务的客户端视图
* Service 作为以下内容的工厂:
* * 1、目标服务端点的代理,
* * 2、用于远程操作的动态面向消息调用的 javax.xml.ws.Dispatch 实例。
* create(java.net.URL wsdlDocumentLocation,QName serviceName):创建 Service 实例。
* wsdlDocumentLocation : 服务 WSDL 文档位置的 URL
* serviceName : 服务的 QName
*/
Service service = Service.create(url, qName);
/**
* 使用 getPorts 方法可以对服务上可用的端口/接口进行枚举
* getPort(Class<T> serviceEndpointInterface):获取支持指定服务端点接口的对象实例
*/
IAgencyService agencyService = service.getPort(IAgencyService.class);
String bodyParams = JSONUtil.toJsonStr(bodyMap);
String agencyStatInfo = agencyService.queryAgencyStatInfo(bodyParams);
return JSONUtil.toBean(agencyStatInfo, Map.class);
}
/**
* 原生 Java JDK 面向接口方式调用。
* 1、客户端不用导入任何与webservice相关的依赖包(如apache的cxf),也和 Spring boot 无关。
* 2、将工具生成的客户端代码全部引入,直接调用本地接口与实现类来调用远程服务接口。连地址都可以省略不写。
* 4、这种方式最简单,最方便,但是无法做复杂操作,比如认证等等。
* 5、http://localhost:8081/wsClient/queryAgencyStatInfo2
*
* @param bodyMap : {"user_id":"254578451PL","token":"6451215","user_name":"蚩尤后裔"}
* @return
* @throws Exception
*/
@PostMapping("/wsClient/queryAgencyStatInfo2")
public Map<String, Object> queryAgencyStatInfo2(@RequestBody Map<String, Object> bodyMap) throws Exception {
// 直接创建接口实现类的对象
AgencyService agencyService = new AgencyService();
// 然后调用getXxxPort方法得到目标接口
IAgencyService agencyServicePort = agencyService.getAgencyServicePort();
// 最后直接调用接口方法
String agencyStatInfo = agencyServicePort.queryAgencyStatInfo(JSONUtil.toJsonStr(bodyMap));
return JSONUtil.toBean(agencyStatInfo, Map.class);
}
src/main/java/org/example/webservice/WebServiceClient.java · 汪少棠/java-se - Gitee.com。
Apache cxf 动态工厂方式
项目 pom.xml 引入 apache cxf 依赖:
<!-- https://mvnrepository.com/artifact/org.apache.cxf/cxf-spring-boot-starter-jaxws -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
<version>3.4.0</version>
</dependency>
pom.xml · 汪少棠/java-se - Gitee.com。
1、直接根据服务端提供的 http://ip:port/webservice/xxx?wsdl 请求。
2、优点是调用简单,根据 wsdl 地址就可以调用,缺点是类似反射一样,写起来不直观,如。
Object[] invoke("方法名",参数1,参数2,参数3....);
@PostMapping("/wsClient/queryAgencyStatInfo")
public Map<String, Object> queryAgencyStatInfo(@RequestBody Map<String, Object> bodyMap) throws Exception {
// 服务端 wsdl 地址
String wsdlUrl = "http://localhost:8080/webservice/agencyService?wsdl";
// 代理工厂
JaxWsDynamicClientFactory wsDynamicClientFactory = JaxWsDynamicClientFactory.newInstance();
// 动态客户端
Client client = wsDynamicClientFactory.createClient(wsdlUrl);
String bodyParams = JSONUtil.toJsonStr(bodyMap);
// 调用指定的方法 invoke("方法名",参数1,参数2,参数3....);
Object[] invoke = client.invoke("queryAgencyStatInfo", bodyParams);
// 获取返回值
JSONObject jsonObject = JSONUtil.parseObj(invoke[0]);
Map map = JSONUtil.toBean(jsonObject, Map.class);
return map;
}
src/main/java/org/example/webservice/WebServiceClient.java · 汪少棠/java-se - Gitee.com。
Apache cxf 代理方式
1、项目 pom.xml 引入 apache cxf 依赖,参考上一节。
2、使用工具或者插件根据服务端 wsdl 文件生成客户端代码,然后全部导入到项目中,然后编写代码调用。
/**
* Apache cxf 代理方式
* 需要引用 Apache cxf 依赖。
* http://localhost:8081/wsClient/syncAgencyStatInfo1
*
* @param bodyMap : {"user_id":"5414512HY745","token":"T45124512","user_name":"蚩尤后裔"}
* @return
* @throws Exception
*/
@PostMapping("/wsClient/syncAgencyStatInfo1")
public Map<String, Object> syncAgencyStatInfo1(@RequestBody Map<String, Object> bodyMap) throws Exception {
// 服务端 wsdl 地址
String wsdlUrl = "http://localhost:8080/webservice/agencyService?wsdl";
// 代理工厂
JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean();
// 设置代理地址
jaxWsProxyFactoryBean.setAddress(wsdlUrl);
// 指定代理实现的SEI(service endpoint interface)的类
jaxWsProxyFactoryBean.setServiceClass(IAgencyService.class);
// 可以在此处配置出拦截器
// jaxWsProxyFactoryBean.getOutInterceptors().add(new ClientLoginInterceptor("root", "admin"));
// 创建一个可用于进行远程调用的JAX-WS代理。必须将返回的对象强制转换为适当的类
IAgencyService agencyService = (IAgencyService) jaxWsProxyFactoryBean.create();
String agencyStatInfo = agencyService.syncAgencyStatInfo(JSONUtil.toJsonStr(bodyMap));
return JSONUtil.toBean(agencyStatInfo, Map.class);
}
Apache CXF 拦截器
1、拦截器的目的是为了在 webService 请求过程中能动态操作请求和响应数据。Java JDK 原生的 JWS 是没有拦截器的。
2、拦截器分类:
1)按所处的位置分:服务器端拦截器、客户端拦截器 |
2)按消息的方向分:入拦截器、出拦截器 |
3)按定义者分:系统拦截器、自定义拦截器(服务端与客户端拦截器是无关的,没有联系的,各自自由设置拦截器) |
3、拦截器主要 API:
org.apache.cxf.interceptor.Interceptor | 整个拦截器体系的超类(接口) |
org.apache.cxf.phase.AbstractPhaseInterceptor | 阶段拦截器,自定义拦截器可以实现/继承它 |
org.apache.cxf.interceptor.LoggingInInterceptor | 系统日志入拦截器类 |
org.apache.cxf.interceptor.LoggingOutInterceptor | 系统日志出拦截器类 |
4、在发布服务时添加拦截器即可。
5、服务端和客户端都可以独立的添加拦截器,互不影响。服务端是先进再出,客户端是先出再进。
日志拦截器
1、使用日志拦截器,实现日志记录,这些日志打印在控制台上,可以清晰的看到请求与返回的 saop 消息。
org.apache.cxf.interceptor.LoggingInInterceptor :系统日志入拦截器类
org.apache.cxf.interceptor.LoggingOutInterceptor :系统日志出拦截器类
服务端
1、传统发布方式代码:
import org.apache.cxf.interceptor.Interceptor;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.message.Message;
import javax.xml.ws.Endpoint;
import java.util.List;
import java.util.logging.Logger;
/**
* Created by Administrator on 2019/1/25 0025.
* webServer 启动类
*/
public class Web_Service {
//日志记录器
public static final Logger logger = Logger.getGlobal();
public static void main(String[] args) {
/**webService服务器提供给客户端访问的地址
* 192.168.1.20 为服务器 ip、3333为指定的端口号、web_server 为应用名称、student 为标识
* 这个地址符合 http 地址即可,为了看起来更像是 web访问,所以才写了应用名,即使是 http://192.168.1.20:3333/xxx 也是可以的
*/
String wsUrl = "http://192.168.1.20:3333/web_server/student";
/**
* javax.xml.ws.Endpoint 表示一个 web service 终端,这是一个抽象类,其中提供了静态方法可以直接调用
* Endpoint publish(String address, Object implementor)
* address:传输协议的 url 地址;
* implementor(实现者):web service 终端的实现类,因为一个 ws 接口可能会有多个实现类
*/
Endpoint endpoint = Endpoint.publish(wsUrl, new StudentServiceImpl());
/**
* javax.xml.ws.Endpoint 是一个抽象类,因为导入了 CXF 的 jar 包,所以 endpoint 的类实质是-
* org.apache.cxf.jaxws.EndpointImpl 类型,这里进行强转,因为拦截器 JWS 没有,CXF 才有。
*/
EndpointImpl endpointImpl = (EndpointImpl) endpoint;
/**
* 添加系统日志出拦截器类
* getOutInterceptors 获取 "出拦截器集合",使用 add 方法将需要添加的拦截器进行添加,这样就会起作用
* org.apache.cxf.interceptor.LoggingOutInterceptor :这是 cxf 提供好的日志出拦截器
*/
List<Interceptor<? extends Message>> outInterceptorList = endpointImpl.getOutInterceptors();
outInterceptorList.add(new LoggingOutInterceptor());
/**
* 添加系统日志入拦截器类
* getInInterceptors 获取 "入拦截器集合",使用 add 方法将需要添加的拦截器进行添加,这样就会起作用
* org.apache.cxf.interceptor.LoggingInInterceptor : cxf 提供好的日志入拦截器
*/
List<Interceptor<? extends Message>> inInterceptorList = endpointImpl.getInInterceptors();
inInterceptorList.add(new LoggingInInterceptor());
logger.info("webService 服务启动,等待客户端请求...");
}
}
2、Spring boot 项目发布方式代码:
@Bean
public Endpoint agencyServiceEndpoint() {
// 参数二是SEI实现类对象
EndpointImpl endpoint = new EndpointImpl(this.springBus(), this.agencyService());
// 添加 apache cxf 日志拦截器:系统日志入拦截器、系统日志出拦截器
// 这些日志会被打印在控制台上,可以清晰的看到请求与返回的 saop 消息.
// 为哪个服务断点添加拦截器,哪个服务断点就生效,其余断点不会受影响。
List<Interceptor<? extends Message>> inInterceptors = endpoint.getInInterceptors();
List<Interceptor<? extends Message>> outInterceptors = endpoint.getOutInterceptors();
inInterceptors.add(new LoggingInInterceptor());
outInterceptors.add(new LoggingOutInterceptor());
// 发布服务。
// 建议与@WebService中的serviceName属性值一致即可。
endpoint.publish("/agencyService");
return endpoint;
}
src/main/java/com/wmx/web_app/webService/WebServiceConfig.java · 汪少棠/web_app - Gitee.com。
3、服务端点被请求时,服务端日志拦截器效果如下:
2023-10-29 08:58:21.695 INFO 7456 --- [nio-8080-exec-1] o.a.c.s.a.A.IAgencyService : Inbound Message
----------------------------
ID: 4
Address: http://localhost:8080/webservice/agencyService
Encoding: UTF-8
Http-Method: POST
Content-Type: text/xml;charset=UTF-8
Headers: {accept-encoding=[gzip,deflate], connection=[Keep-Alive], Content-Length=[343], content-type=[text/xml;charset=UTF-8], host=[localhost:8080], SOAPAction=[""], user-agent=[Apache-HttpClient/4.1.1 (java 1.5)]}
Payload: <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:web="http://webService.web_app.wmx.com/">
<soapenv:Header/>
<soapenv:Body>
<web:queryAgencyStatInfo>
<!--Optional:-->
<web:paramBody>{}</web:paramBody>
</web:queryAgencyStatInfo>
</soapenv:Body>
</soapenv:Envelope>
--------------------------------------
2023-10-29 08:58:21.711 INFO 7456 --- [nio-8080-exec-1] o.a.c.s.a.A.IAgencyService : Outbound Message
---------------------------
ID: 4
Response-Code: 200
Encoding: UTF-8
Content-Type: text/xml
Headers: {}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:queryAgencyStatInfoResponse xmlns:ns2="http://webService.web_app.wmx.com/"><queryResult>{"reason":"加载成功","status_code":"1000","data":{"msg":"查询成功","subSystemName":"基础库管理","subSystemCode":"bgt-basic-server","tableList":[{"tableName":"BAS_AGENCY_EXT 单位扩展信息表","tableSize":560},{"tableName":"BAS_AGENCY_INFO 单位基本信息表","tableSize":560}],"sendStatus":1,"id":"5337a4bc06634e5491588c4fb9019787"}}</queryResult></ns2:queryAgencyStatInfoResponse></soap:Body></soap:Envelope>
--------------------------------------
客户端
1、客户端与服务端拦截器无关,代码基本与服务端一致,控制台输出的效果也一致,代码演示:。
@PostMapping("/wsClient/queryAgencyStatInfo1")
public Map<String, Object> queryAgencyStatInfo1(@RequestBody Map<String, Object> bodyMap) throws Exception {
/**
* url:webservice 服务端提供的服务地址,结尾必须加 ?wsdl
* 1、可以直接访问在线地址,也可以通过浏览器访问后,然后将内容复制粘贴保存到本地项目中。
*/
// URL url = new URL("http://localhost:8080/webservice/agencyService?wsdl");
URL url = WebServiceClient.class.getClassLoader().getResource("wsdl/agencyService.wsdl");
/**
* QName 表示 XML 规范中定义的限定名称,QName 的值包含名称空间 URI、本地部分和前缀。
* QName(final String namespaceURI, final String localPart):指定名称空间 URI 和本地部分的 QName 构造方法。
* 1、两个参数值都可以在 wsdl 文件中的第一行中找到,如下所示,就是其中的两个属性。
* * * <wsdl:definitions name="agencyService" targetNamespace="http://webService.web_app.wmx.com/">
* 2、或者在生成的接口的实现类中也能找到,如下所示,同样是 name 和 targetNamespace 属性。
* @WebServiceClient(name = "agencyService", targetNamespace = "http://webService.web_app.wmx.com/", wsdlLocation = "http://localhost:8080/webservice/agencyService?wsdl")
*/
QName qName = new QName("http://webService.web_app.wmx.com/", "agencyService");
/**
* Service 对象提供 Web 服务的客户端视图
* Service 作为以下内容的工厂:
* * 1、目标服务端点的代理,
* * 2、用于远程操作的动态面向消息调用的 javax.xml.ws.Dispatch 实例。
* create(java.net.URL wsdlDocumentLocation,QName serviceName):创建 Service 实例。
* wsdlDocumentLocation : 服务 WSDL 文档位置的 URL
* serviceName : 服务的 QName
*/
Service service = Service.create(url, qName);
/**
* 使用 getPorts 方法可以对服务上可用的端口/接口进行枚举
* getPort(Class<T> serviceEndpointInterface):获取支持指定服务端点接口的对象实例
*/
IAgencyService agencyService = service.getPort(IAgencyService.class);
/**
* Java JDK 原生的 JWS 是没有拦截器的。以下代码为 Apache CXF 代码。
* 添加拦截器代码如同服务端一样,都是先获取拦截器列表,然后添加需要使用的拦截器
* org.apache.cxf.frontend.ClientProxy :apache cxf 客户端代理
* Client getClient(Object o) 返回发送请求的客户端对象,参数传入服务端接口对象
* 服务端是先进再出,客户端是先出再进。
*/
Client client = ClientProxy.getClient(agencyService);
client.getOutInterceptors().add(new LoggingOutInterceptor());
client.getInInterceptors().add(new LoggingInInterceptor());
String bodyParams = JSONUtil.toJsonStr(bodyMap);
String agencyStatInfo = agencyService.queryAgencyStatInfo(bodyParams);
return JSONUtil.toBean(agencyStatInfo, Map.class);
}
src/main/java/org/example/webservice/WebServiceClient.java · 汪少棠/java-se - Gitee.com。
自定义拦截器
1、自定义拦截器可以继承 org.apache.cxf.phase.AbstractPhaseInterceptor 抽象类,然后实现其中的 handleMessage 方法。
2、使用方式与日志拦截器完全一致,只需要将日志拦截器换成自定义的拦截器即可。
3、本示例演示使用自定义拦截器,实现用户名与密码的检验,因为实际中一些 webService 对外提供服务,客户端调用时需要提供正确的账号与密码才能调用成功。
1)客户端需要设置 out(出) 拦截器,在 soap 消息中携带账号与密码过去
2)服务端需要设置 in(入) 拦截器,解析 soap 消息中的账号与密码
服务端
1、自定义拦截器:
import com.wmx.web_app.webService.IUserService;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.headers.Header;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import javax.xml.namespace.QName;
/**
* {@link IUserService} 服务端点拦截器——校验账户与密码
* <p>
* 1、通过在 <soap:Header> 下自定义标签元素进行验证。
* 2、客户端必须必须按着服服务端的约定标签格式进行传输,比如标签名称等,否则服务端永远无法解析到值。
*
* @author wangMaoXiong
* @version 1.0
* @date 2023/10/29 9:31
*/
public class WsUserInInterceptor extends AbstractPhaseInterceptor<SoapMessage> {
private static final Logger log = LoggerFactory.getLogger(WsUserInInterceptor.class);
private static final String userName = "admin";
private static final String password = "123456";
/**
* 构造器中必须使用 super 设置拦截器发生的时刻/阶段
* org.apache.cxf.phase.Phase 中提供了大量的常量表示拦截器发生的时刻
*/
public WsUserInInterceptor() {
// 协议前进行拦截
super(Phase.PRE_PROTOCOL);
}
/**
* 注意:客户端必须必须按着服服务端的约定格式进行传输,比如标签名称等,否则服务端永远无法解析到值。
* 客户端传来的 soap 消息先进入拦截器这里进行处理,客户端的账号与密码消息放在 soap 的消息头<head></head>中,类似如下。
* <pre>
* <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
* <soap:Header>
* <userInfo>
* <userName>admin</userName>
* <password>123456</password>
* </userInfo>
* </soap:Header>
* <soap:Body>
* <ns2:getStudentById xmlns:ns2="http://web_service.lct.com/">
* <sId>11</sId>
* </ns2:getStudentById>
* </soap:Body>
* </soap:Envelope>
* </pre>
* 现在只需要解析其中的 <head></head>标签,如果解析验证成功,则放行,否则这里直接抛出异常,服务端不会再往后运行,客户端也会跟着抛出此异常,得不到正确结果
*
* @param message
* @throws Fault
*/
@Override
public void handleMessage(SoapMessage message) throws Fault {
// 客户端需要正确的请求格式
String itemFormat = "<Header><userInfo><userName>xxx</userName><password>xxx</password></userInfo></Header>";
// org.apache.cxf.headers.Header
// QName :xml 限定名称,客户端设置头信息时,必须与服务器保持一致,否则这里返回的 header 为null,则永远通不过.
Header header = message.getHeader(new QName("userInfo"));
if (header == null) {
throw new Fault(new IllegalArgumentException("未检测到账户与密码,请检查!正确格式:" + itemFormat));
}
// 获取根元素,即 <userInfo>标签
Element userInfoEle = (Element) header.getObject();
// 获取元素值
// NodeList getElementsByTagName(String name):按文档顺序返回具有给定标记名的所有子节点,特殊值“*”匹配所有标记。没有时,返回空集合。
// Node item(int index) 返回集合中的指定索引的元素项。如果索引大于或等于列表中的节点数,则返回 null。需要防止空指针异常。
// String getTextContent() 返回此节点及其子节点的文本内容。不包含任何标记。不执行空白规范化。
Node userNameNode = userInfoEle.getElementsByTagName("userName").item(0);
Node passwordNode = userInfoEle.getElementsByTagName("password").item(0);
if (userNameNode == null || passwordNode == null) {
throw new Fault(new IllegalArgumentException("未检测到账户与密码,请检查!正确格式:" + itemFormat));
}
String userNameInput = userNameNode.getTextContent();
String passwordInput = passwordNode.getTextContent();
// 为了演示简单,直接写死了,实际中建议放在配置文件中提供配置,或者是数据库中。
if (userName.equals(userNameInput) && password.equals(passwordInput)) {
log.info("webService 服务端自定义拦截器验证通过....");
// 放行
return;
}
throw new Fault(new IllegalArgumentException("用户名或密码不正确,请检查!正确格式:" + itemFormat));
}
}
2、Spring boot 项目发布服务时注册拦截器:
@Bean
public Endpoint userServiceEndpoint() {
EndpointImpl endpoint = new EndpointImpl(this.springBus(), this.userService());
endpoint.getInInterceptors().add(new LoggingInInterceptor());
// 添加自定义拦截器校验账户与密码,出/入千万别写反了.
endpoint.getInInterceptors().add(new WsUserInterceptor());
// 发布服务。
endpoint.publish("/userService");
return endpoint;
}
src/main/java/com/wmx/web_app/webService/WebServiceConfig.java · 汪少棠/web_app - Gitee.com。
客户端
1、自定义拦截器 :
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.headers.Header;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.example.webservice.user.IUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.namespace.QName;
import java.util.List;
/**
* {@link IUserService} 出拦截器,用于设置账户密码进行登陆。
* 1、通过在 <soap:Header> 下自定义标签元素进行验证。
* 2、客户端必须必须按着服服务端的约定标签格式进行传输,比如标签名称等,否则服务端永远无法解析到值。
*
* @author wangMaoXiong
* @version 1.0
* @date 2023/10/29 10:08
*/
public class WsUserOutInterceptor extends AbstractPhaseInterceptor<SoapMessage> {
private static final Logger log = LoggerFactory.getLogger(WsUserOutInterceptor.class);
/**
* webService 需要传递的账号与密码
*/
private String name;
private String password;
/**
* 使用构造器传入参数,构造器中必须使用 super 设置拦截器发生的时刻/阶段
* org.apache.cxf.phase.Phase 中提供了大量的常量表示拦截器发生的时刻
*
* @param name
* @param password
*/
public WsUserOutInterceptor(String name, String password) {
// 协议前进行拦截
super(Phase.PRE_PROTOCOL);
this.name = name;
this.password = password;
}
/**
* 默认情况下,客户端给服务端请求的 soap 消息如下:
* <pre>
* <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
* <soap:Body>
* <ns2:getStudentByIdResponse xmlns:ns2="http://web_service.lct.com/">
* <getStudentById>
* <birthday>2019-02-18T16:19:12.102+08:00</birthday>
* <punishStatus>5</punishStatus>
* <sID>11</sID>
* <sName>0a90959c-22ee-497b-b60d-0550b1f2adf7</sName>
* </getStudentById>
* </ns2:getStudentByIdResponse>
* </soap:Body>
* </soap:Envelope>
* </pre>
* 其中的 <body></body>部分为请求的主体,现在为 Envelope 设置头信息 <head></head>,将账户密码信息放在 <head></head>中,<head>与 body 同级。
* <head></head>中的内容设置为账户与密码元素,如:
* <pre>
* <head>
* <userInfo>
* <userName>admin</userName>
* <password>123456</password>
* </userInfo>
* </head>
* </pre>
* 其中的 userInfo、name、password 等元素名称必须和服务端约定的一致,因为服务端还需要根据这些名称解析出元素的值。
*
* @param message :在客户端请求服务端前,先进入此方法,然后将账户密码信息添加到 soap 消息头中
*/
@Override
public void handleMessage(SoapMessage message) {
// 创建文档对象
Document document = DOMUtils.createDocument();
// 创建标签元素
Element userInfoEle = document.createElement("userInfo");
Element userNameEle = document.createElement("userName");
Element passwordEle = document.createElement("password");
// 设置标签元素内容
userNameEle.setTextContent(this.name);
passwordEle.setTextContent(this.password);
// 添加子元素
userInfoEle.appendChild(userNameEle);
userInfoEle.appendChild(passwordEle);
// org.apache.cxf.headers.Header 最后将创建好的 soap 头信息添加到 SoapMessage 中
// QName 构造器中的值与后面的 userInfoEle 元素的标签名保持一致
List<Header> headerList = message.getHeaders();
headerList.add(new Header(new QName("userInfo"), userInfoEle));
}
}
src/main/java/org/example/webservice/interceptor/WsUserOutInterceptor.java · 汪少棠/java-se - Gitee.com。
2、注册拦截器:
@GetMapping("/wsClient/listUserByIds")
public List<User> listUserByIds() {
// 直接创建接口实现类的对象
UserService userService = new UserService();
// 然后调用getXxxPort方法得到目标接口
IUserService iUserService = userService.getUserServicePort();
Client client = ClientProxy.getClient(iUserService);
client.getOutInterceptors().add(new LoggingOutInterceptor());
client.getOutInterceptors().add(new WsUserOutInterceptor("admin", "123456"));
List<User> users = iUserService.listUserByIds(Lists.newArrayList(1L, 2L, 3L, 4L, 5L));
return users;
}
src/main/java/org/example/webservice/WebServiceClient.java · 汪少棠/java-se - Gitee.com。
接口调试工具
SoapUI
1、Soapui 是常用的接口测试工具之一,除了拥有类似 Postman 等工具一样常规的 http 请求测试,SoapUI 还支持 webService 接口测试。
2、https://wangmaoxiong.blog.csdn.net/article/details/106518975。
ReadyAPI 接口调试工具
1、https://blog.csdn.net/wangmx1993328/article/details/106518975#ReadyAPI 接口调试工具
2、相当于是 SoapUI 的收费版本,可以免费试用15天。
浏览器插件 Wizdler
1、识别页面上的WSDL信息,显示可用的服务和操作。单击操作以生成请求并查看响应。
doc/csdn/浏览器常用插件.md#wizdler---webservice服务接口调试 · 汪少棠/material - Gitee.com。