Spring boot WebService 使用快速入门、拦截器

目录

Apache CXF 概述

Spring boot WebService 使用快速入门

服务端

客户端

原生 Java JDK 方式

Apache cxf 动态工厂方式

Apache cxf 代理方式

 Apache CXF 拦截器

日志拦截器

服务端

客户端

自定义拦截器

服务端

客户端

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.exeApache 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

原生JDK方式调用webService.gif

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));
    }
}

src/main/java/com/wmx/web_app/webService/interceptor/WsUserInInterceptor.java · 汪少棠/web_app - Gitee.com。 

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

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蚩尤后裔-汪茂雄

芝兰生于深林,不以无人而不芳。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值