SpringBoot | 第三十四章:CXF構建WebService服務

前言

上一章節,講解了如何使用Spring-WS構建WebService服務。其實,建立WebService的方式有不少的,今天來看看如何使用apache cxf來構建及調用WebService服務。html

一點知識

何爲Apache-CXF

Apache CXF是一個開源的Services框架,CXF幫助您利用Frontend編程 API 來構建和開發Services,像JAX-WSJAX-RS。這些Services能夠支持多種協議,好比:SOAPXML/HTTPRESTful HTTP 或者CORBA,而且能夠在多種傳輸協議上運行,好比:HTTPJMS 或者JBICXF大大簡化了 Services 的建立,同時它能夠自然地和Spring進行無縫集成java

如下是官網給出的介紹:https://github.com/apache/cxfgit

最經常使用的是使用cxf開發web-service。自己是基於JAX-WS規範來實現的。固然,自己CXF也實現了JAX-RS規範來實現RESTFul Servicegithub

關於JAX-WS規範

JAX-WS全稱:Java API for XML-Based Web ServicesJAX-WS是一種編程模型,它經過支持將基於註釋的標準模型用於開發Web Service應用程序和客戶機來簡化應用程序開發。web

JAX-WS是Java程序設計語言一個用來建立Web服務的API。spring

  • 在服務器端,用戶只須要經過Java語言定義遠程調用所須要實現的接口SEI(service endpoint interface),並提供相關的實現,經過調用JAX-WS的服務發佈接口就能夠將其發佈爲WebService接口。
  • 在客戶端,用戶能夠經過JAX-WS的API建立一個代理(用本地對象來替代遠程的服務)來實現對於遠程服務器端的調用。固然JAX-WS也提供了一組針對底層消息進行操做的API調用,你能夠經過Dispatch直接使用SOAP消息或XML消息發送請求或者使用Provider處理SOAP或XML消息。

經常使用註解介紹

JAX-WS提供了一系列的註解,能夠對WebService的接口規範化。如下介紹下最經常使用的幾個註解。apache

  • @WebService:用於將Java類標記爲實現Web Service或者將服務端點接口 (SEI) 標記爲實現Web Service接口。 其包含的屬性有:編程

    • name:此屬性的值包含XML Web Service的名稱。在默認狀況下,該值是實現XML Web Service的類的名稱,wsdl:portType 的名稱。缺省值爲 Java 類的簡單名稱 + Service。(字符串)
    • targetNamespace:默認的值爲 "http://包名/" ,能夠經過此變量指定一個自定義的targetNamespace值。
    • serviceName:對外發布的服務名,指定Web Service的服務名稱:wsdl:service。缺省值爲 Java 類的簡單名稱 + Service。(字符串)
    • endpointInterface:
    • portName:wsdl:portName的值。缺省值爲WebService.name+Port
    • wsdlLocation:指定用於定義 Web Service 的 WSDL 文檔的 Web 地址
  • @WebMethod:表示做爲一項Web Service操做的方法。僅支持在使用@WebService 註解的類上使用@WebMethod註解。安全

    • operationName:指定與此方法相匹配的wsdl:operation 的名稱。缺省值爲 Java 方法的名稱。(字符串)
    • action:定義此操做的行爲。對於 SOAP 綁定,此值將肯定 SOAPAction 頭的值。缺省值爲 Java 方法的名稱。(字符串)
    • exclude:指定是否從 Web Service 中排除某一方法。缺省值爲 false。(布爾值)
  • @WebParam:用於定製從單個參數至Web Service消息部件和XML元素的映射。springboot

其餘註解,能夠查看:WebService註解總結

爲了有個直觀感覺,你們能夠看看如下這個wsdl文件,對應以上各註解屬性的值(加了前綴oKong)。

//@WebService 屬性示例
@WebService(targetNamespace = 'http://www.lqdev.cn/webservice' ,name = "oKongName", serviceName="oKongServiceName", portName = "oKongPortName",endpointInterface="cn.lqdev.learning.springboot.cxf.service.AuthorService")

//@webMethod @WebParam 經常使用屬性示例
@WebMethod(operationName="oKongOperationName",action="oKongAction")
String getAuthorName(@WebParam(name = "paramName") String name);

標記的有點花,⊙﹏⊙‖∣。你們能夠本身對照下。

SpringBoot整合CXF實例

接下來,咱們以一個簡單的示例來演示下,如何發佈服務及如何進行服務調用。

服務端構建

建立一個工程:spring-boot-cxf-service.

0.引入CXF的POM文件

<dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-spring-boot-starter-jaxws</artifactId>
            <version>3.2.5</version>
        </dependency>

1.建立實體,按JAX-WS規範,建立接口及其實現類。 AuthorDto.java

/**
 * 做者信息實體
 * @author oKong
 *
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthorDto {

    String name;
    
    List<String> hobby; 
    
    String birthday;
    
    String description;
    
    Sex sex;
}

Sex.java性別枚舉類

/**
 * 性別枚舉類
 * @author oKong
 *
 */
public enum Sex {

    MALE("male"),
    FEMALE("female");
    
    String value;
    
    Sex(String value) {
        this.value = value;
    }
    
    public String value() {
        return value;
    }

    public static Sex fromValue(String v) {
        for (Sex c : Sex.values()) {
            if (c.value.equals(v)) {
                return c;
            }
        }
        throw new IllegalArgumentException(v);
    }    
}

AuthorService.java接口類

/**
 * 建立服務接口
 * @author oKong
 *
 */
@WebService(targetNamespace = WsConst.NAMESPACE_URI ,name = "authorPortType")
public interface AuthorService {

    /**
     * 根據名稱獲取做者信息
     * @author 做者:oKong
     */
    @WebMethod(operationName="getAuthorByName")
    AuthorDto getAuthor(@WebParam(name = "authorName") String name);

    /**
     * 獲取做者列表信息
     * @author oKong
     */
    @WebMethod
    List<AuthorDto> getAuthorList();
    
    /**
     * 返回字符串測試
     * @author oKong
     */
    String getAuthorString(@WebParam(name = "authorName")String name);
}

AuthorServiceImpl.java接口實現類

@WebService(
         targetNamespace = WsConst.NAMESPACE_URI, //wsdl命名空間 
         name = "authorPortType",                 //portType名稱 客戶端生成代碼時 爲接口名稱
         serviceName = "authorService",           //服務name名稱
         portName = "authorPortName",             //port名稱
         endpointInterface = "cn.lqdev.learning.springboot.cxf.service.AuthorService")//指定發佈webservcie的接口類,此類也須要接入@WebService註解
public class AuthorServiceImpl implements AuthorService{

    @Override
    public AuthorDto getAuthor(String name) {
        AuthorDto author = new AuthorDto();
        author.setBirthday("1990-01-23");
        author.setName("姓名:" + name);
        author.setSex(Sex.MALE);
        author.setHobby(Arrays.asList("電影","旅遊"));
        author.setDescription("描述:一枚趔趄的猿。如今時間:" + new Date().getTime());
        return author;
    }

    @Override
    public List<AuthorDto> getAuthorList() {
        List<AuthorDto> resultList = new ArrayList<>();
        AuthorDto author = new AuthorDto();
        author.setBirthday("1990-01-23");
        author.setName("姓名:oKong");
        author.setSex(Sex.MALE);
        author.setHobby(Arrays.asList("電影","旅遊"));
        author.setDescription("描述:一枚趔趄的猿。如今時間:" + new Date().getTime());
        resultList.add(author);
        resultList.add(author);
        return resultList;
    }

    @Override
    public String getAuthorString(String name) {
        AuthorDto author = getAuthor(name);
        return author.toString();
    }
}

注意:相關注解能夠查看章節:經常使用註解介紹

主要是接口實現類的@WebService對應屬性值都要wsdl文件的映射關係。

@WebService(
         targetNamespace = WsConst.NAMESPACE_URI, //wsdl命名空間 
         name = "authorPortType",                 //portType名稱 客戶端生成代碼時 爲接口名稱
         serviceName = "authorService",           //服務name名稱
         portName = "authorPortName",             //port名稱
         endpointInterface = "cn.lqdev.learning.springboot.cxf.service.AuthorService")//指定發佈webservcie的接口類,此類也須要接入@WebService註解

2.建立常量類,配置類,設置訪問uri路徑等。

WsConst.java

/**
 * 常量類
 * @author oKong
 *
 */
public class WsConst {
    public static final String NAMESPACE_URI = "http://www.lqdev.cn/webservice";
}

CxfWebServiceConfig.java

/**
 * cxf配置類
 * @author oKong
 *
 */
@Configuration
public class CxfWebServiceConfig {
    
    //這裏須要注意  因爲springmvc 的核心類 爲DispatcherServlet
    //此處若不重命名此bean的話 本來的mvc就被覆蓋了。可查看配置類:DispatcherServletAutoConfiguration
    //一種方法是修改方法名稱 或者指定bean名稱 
    //這裏須要注意 若beanName命名不是 cxfServletRegistration 時,會建立兩個CXFServlet的。
    //具體可查看下自動配置類:Declaration org.apache.cxf.spring.boot.autoconfigure.CxfAutoConfiguration
    //也能夠不設置此bean 直接經過配置項 cxf.path 來修改訪問路徑的
    @Bean("cxfServletRegistration")
    public ServletRegistrationBean dispatcherServlet() {
        //註冊servlet 攔截/ws 開頭的請求 不設置 默認爲:/services/*
        return new ServletRegistrationBean(new CXFServlet(), "/ws/*");
    }
    
    /**
     * 申明業務處理類 固然也能夠直接 在實現類上標註 @Service
     * @author oKong
     */
    @Bean
    public AuthorService authorService() {
        return new AuthorServiceImpl();
    }
    
    /*
     * 非必要項
     */
    @Bean(name = Bus.DEFAULT_BUS_ID)
    public SpringBus springBus() {
        SpringBus springBus = new SpringBus();
        return springBus;
    }
    
    /*
     * 發佈endpoint
     */
    @Bean
    public Endpoint endpoint(AuthorService authorService) {
        EndpointImpl endpoint = new EndpointImpl(springBus(), authorService);
        endpoint.publish("/author");//發佈地址
        return endpoint;
    }
}

注意事項:

  • 配置ServletRegistrationBean時,須要注意設置方法的名稱或者bean的名稱時,不要和默認的DispatcherServlet類重名了,會致使原先的mvc接口沒法使用,由於被覆蓋了。
  • 修改訪問的路徑能夠經過設置ServletRegistrationBean來修改,但同時,要注意須要設置bean的名稱爲cxfServletRegistration,否則會形成註冊多個CXFServlet的。具體緣由可查看自動配置類:org.apache.cxf.spring.boot.autoconfigure.CxfAutoConfiguration

CxfAutoConfiguration

因此,修改訪問路徑還能夠經過配置項:cxf.path來設置。其默認的訪問url爲:/services

3.建立啓動類,同時啓動應用。

/**
 * cxf服務發佈示例
 * @author oKong
 *
 */
@SpringBootApplication
@Slf4j
public class CxfServiceApplication {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(CxfServiceApplication.class, args);
        log.info("spirng-boot-cxf-service-chapter34啓動!");
    }
}

啓動後,能夠從控制檯看見能夠訪問的url路徑信息。

2018-11-10 22:06:40.898  INFO 46364 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'CXFServlet' to [/ws/*]
2018-11-10 22:06:40.899  INFO 46364 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]

訪問:http://127.0.0.1:8080/ws/author?wsdl ,驗證是否發佈成功。

author

自此,webService發佈成功了。

客戶端調用

建立一個客戶端工程:spring-boot-cxf-client

0.引入cxf依賴。

<dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-spring-boot-starter-jaxws</artifactId>
            <version>3.2.5</version>
        </dependency>

1.建立wsdl文件,同時利用插件:cxf-codegen-plugin建立相關類。

<!-- cxf-codegen-plugin -->
      <plugin>
        <groupId>org.apache.cxf</groupId>
        <artifactId>cxf-codegen-plugin</artifactId>
        <version>3.2.5</version>
        <executions>
          <execution>
            <id>generate-sources</id>
            <phase>generate-sources</phase>
            <configuration>
              <sourceRoot>${project.build.directory}/generated/cxf</sourceRoot>
              <wsdlOptions>
                <wsdlOption>
                  <wsdl>src/main/resources/wsdl/author.wsdl</wsdl>
                  <wsdlLocation>classpath:wsdl/author.wsdl</wsdlLocation>
                </wsdlOption>
              </wsdlOptions>
            </configuration>
            <goals>
              <goal>wsdl2java</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

wsdl文件,放入main/resources/wsdl目錄下。以後執行:mvn generate-sources命令,就會自動建立相應的類文件了。拷貝相應的類文件至src/java目錄下便可。或者直接指定sourceRoot也是能夠的。

2.建立調用的配置類,這裏演示兩種方式。

WsConst.java

/**
 * 常量類
 * @author oKong
 *
 */
public class WsConst {
    public static final String NAMESPACE_URI = "http://www.lqdev.cn/webservice";
    public static final String SERVICE_ADDRESS= "http://127.0.0.1:8080/ws/author?wsdl";
}

CxfClinetConfig.java

/**
 * 配置類
 * 
 * @author oKong
 *
 */
@Configuration
public class CxfClientConfig {

    
    /**
     *  以接口代理方式進行調用 AuthorPortType接口
     */
    @Bean("cxfProxy")
    public AuthorPortType createAuthorPortTypeProxy() {
        JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean();
        jaxWsProxyFactoryBean.setServiceClass(AuthorPortType.class);
        jaxWsProxyFactoryBean.setAddress(WsConst.SERVICE_ADDRESS);//服務地址:http://127.0.0.1:8080/ws/autho

        return (AuthorPortType) jaxWsProxyFactoryBean.create();
    }
    
    /*
     * 採用動態工廠方式 不須要指定服務接口
     */
    @Bean
    public Client createDynamicClient() {
        JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance();
        Client client = dcf.createClient(WsConst.SERVICE_ADDRESS);
        return client;
    } 
}

注意:除了使用JaxWsProxyFactoryBeanJaxWsDynamicClientFactory調用外,還能夠直接使用自動生成的AuthorService類直接調用的,此類繼承至javax.xml.ws.Service。 如:

/*
     * 直接調用
     */
    @Bean("jdkProxy")
    public AuthorPortType createJdkService() {
        AuthorService authorService = new AuthorService();
        return authorService.getAuthorPortName();
    }

其實,最後都是使用AuthorPortType進行調用的。

3.建立控制層,進行調用示例。

/**
 * 調用示例
 * @author oKong
 *
 */
@RestController
@RequestMapping("/cxf")
public class DemoController {

    @Autowired
    Client client;
    
    @Autowired
    @Qualifier("cxfProxy")
    AuthorPortType authorPort;
    
    @GetMapping("/getauthorstring")
    public String getAuthorString(String authorName) {
        return authorPort.getAuthorString(authorName);
    }
    
    @GetMapping("/getauthor")
    public AuthorDto getAuthor(String authorName) {
        return authorPort.getAuthorByName(authorName);
    }
    
    @GetMapping("/getauthorlist")
    public List<AuthorDto> getAuthorList() {
        return authorPort.getAuthorList();
    }
    
    @GetMapping("/dynamic/{operation}")
    public Object getAuthorStringByDynamic(@PathVariable("operation")String operationName, String authorName) throws Exception {
        //這裏就簡單的判斷了 
        Object[] objects = null; 
//        client.getEndpoint().getBinding().getBindingInfo().getOperations()
        if ("getAuthorList".equalsIgnoreCase(operationName)) {
            objects = client.invoke(operationName);
        } else if ("getAuthorString".equalsIgnoreCase(operationName)) {
            objects = client.invoke(operationName, authorName);
        } else if ("getAuthorByName".equalsIgnoreCase(operationName)) {
            objects = client.invoke(operationName, authorName);
        } else {
            throw new RuntimeException("無效的調用方法");
        }
        return objects != null && objects.length > 0 ? objects[0] : "返回異常";
    }    
}

4.編寫啓動類,同時制定應用端口爲:8090。

/**
 * cxf-客戶端調用示例
 * 
 * @author oKong
 *
 */
@SpringBootApplication
@Slf4j
public class CxfClientApplication {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(CxfClientApplication.class, args);
        log.info("spring-boot-cxf-client-chapter34啓動!");
    }
}

端口號配置:

server.port=8090

5.啓動應用,依次訪問。查看是否調用成功。

http://127.0.0.1:8090/cxf/getauthorstring?authorName=oKong

http://127.0.0.1:8090/cxf//getauthorlist?authorName=oKong

動態工廠方式調用: http://127.0.0.1:8090/cxf/dynamic/getAuthorList?authorName=oKong

其餘的就不一一貼圖了,可自行訪問下。

異常捕獲

Cxf發生異常時,會統一拋出:org.apache.cxf.interceptor.Fault類的,因此想要捕獲異常,能夠在統一異常裏面進行捕獲,關於統一異常處理,能夠查看文章:第八章:統一異常、數據校驗處理

自定義攔截器

CXF的攔截器分爲兩種:InInterceptorOutInterceptor。顯而易見,InInterceptor能夠處理soap請求消息OutInterceptor能夠處理soap響應消息。其攔截器都繼承至AbstractPhaseInterceptor<Message>接口類,並且,自己也自帶了不少的攔截器,能夠自行添加看看,好比日誌攔截器之類的:LoggingInInterceptorLoggingOutInterceptor

請求流程圖:

攔截器鏈的階段:

輸入攔截器鏈有以下幾個階段,這些階段按照在攔截器鏈中的前後順序排列。

輸出攔截器鏈有以下幾個階段,這些階段按照在攔截器鏈中的前後順序排列。

具體名稱,可查看:org.apache.cxf.phase.Phase

如今,咱們自定義個實現攔截器,實現請求時header須要帶上特定參數,或者你們可不寫一些安全校驗的自定義攔截器,本例只是簡單的示例。

服務端攔截器

1.檢驗攔截器:CheckAuthInterceptor.java

/**
 * 簡易-安全校驗攔截器
 * 
 * @author oKong
 *
 */
@Slf4j
public class CheckAuthInterceptor extends AbstractPhaseInterceptor<SoapMessage> {

    public CheckAuthInterceptor() {
        super(Phase.PRE_INVOKE);// 攔截節點:調用以前
    }

    @Override
    public void handleMessage(SoapMessage message) throws Fault {
        log.info("檢驗攔截器開始檢驗:{}", message);
        // 處理方法
        List<Header> headers = message.getHeaders();

        // 判斷是否存header
        // 檢查headers是否存在
        if (headers == null | headers.size() < 1) {
            throw new Fault(new IllegalArgumentException("驗證失敗,請傳入正確參數(40001)"));//可自定義編碼規範
        }
        //取出header
        Header header = headers.get(0);
        //獲取對象
        Element element = (Element) header.getObject();//這裏獲取的就時 auth對象了
        NodeList tokenNode = element.getElementsByTagName("token");
        if(tokenNode == null || tokenNode.getLength() < 1) {
            //無token節點
            throw new Fault(new IllegalArgumentException("驗證失敗,請傳入正確參數(40002)"));//自定義編碼規範
        }
        //獲取token
        String token = tokenNode.item(0).getTextContent();
        log.info("請求的token爲:{}", token);
        //這裏能夠對token 有效性進行判斷
    }
}

2.Endpoint中加入攔截器配置。

/*
     * 發佈endpoint
     */
    @Bean
    public Endpoint endpoint(AuthorService authorService) {
        EndpointImpl endpoint = new EndpointImpl(springBus(), authorService);
        endpoint.publish("/author");//發佈地址
        endpoint.getInInterceptors().add(createCheckAuthInterceptor());//加入攔截器
//        endpoint.getOutInterceptors().add()//響應攔截器
        return endpoint;
    }
    
    @Bean
    public Interceptor<SoapMessage> createCheckAuthInterceptor(){
        return new CheckAuthInterceptor();
    }

客戶端攔截器

1.編寫攔截器。

/**
 * 簡易-安全校驗攔截器
 * @author oKong
 *
 */
public class AuthInterceptor extends AbstractPhaseInterceptor<SoapMessage>{
    
    public AuthInterceptor() {
        super(Phase.PREPARE_SEND);//準備請求時進行攔截
    }

    @Override
    public void handleMessage(SoapMessage message) throws Fault {
        //處理方法
        List<Header> headers = message.getHeaders();
        
        Document doc = DOMUtils.createDocument();
        Element element = doc.createElement("auth");
        Element tokenEle = doc.createElement("token");
        tokenEle.setTextContent(UUID.randomUUID().toString());
        element.appendChild(tokenEle);
        //這裏須要注意 默認狀況下 是使用 org.w3c.dom.Element對象設置對象值的。
        //也能夠指定 DataBinding 設置對象的。可繼承抽象類: org.apache.cxf.databinding.AbstractDataBinding
        //具體源碼可查看:org.apache.cxf.binding.soap.interceptor.SoapOutInterceptor
        Header tokenHeader = new SoapHeader(new QName(""), element);
//        tokenHeader.setDataBinding()
        headers.add(tokenHeader);
    }

}

這裏須要注意:

  • 設置header時,默認是org.w3c.dom.Element對象。
  • 自定義對象時,可設置DataBinding類來解析(何嘗試,只是看了一眼源碼,裏面有此邏輯,有興趣的同窗能夠自行試試)。

2.請求類中加入攔截器。 CxfClientConfig.java

/**
     *  以接口代理方式進行調用 AuthorPortType接口
     */
    @Bean("cxfProxy")
    public AuthorPortType createAuthorPortTypeProxy() {
        JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean();
        jaxWsProxyFactoryBean.setServiceClass(AuthorPortType.class);
        jaxWsProxyFactoryBean.setAddress(WsConst.SERVICE_ADDRESS);//服務地址:http://127.0.0.1:8080/ws/autho
        jaxWsProxyFactoryBean.getOutInterceptors().add(createInterceptor());//加入自定義攔截器
        return (AuthorPortType) jaxWsProxyFactoryBean.create();
    }
    
    /*
     * 採用動態工廠方式 不須要指定服務接口
     */
    @Bean
    public Client createDynamicClient() {
        JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance();
        Client client = dcf.createClient(WsConst.SERVICE_ADDRESS);
        client.getOutInterceptors().add(createInterceptor());
        return client;
    }
    
    @Bean
    public Interceptor<SoapMessage> createInterceptor() {
        return new AuthInterceptor();
    }

從新啓動後,再次請求就能夠看見相關日誌輸出了,能夠試着不設置token,看看有攔截。

正常請求

異常請求:

異常請求

參考資料

  1. http://cxf.apache.org/docs/springboot.html

  2. https://www.code996.cn/post/2017/cxf-interceptor/

總結

本章節主要簡單介紹了apache-cxf的使用。這文章示例寫下來,我發現比spring-ws更簡單呀,也更讓人容易理解、邏輯比較清晰,並且也能設置一些差別化的東西。不知道是否是真的對spring-ws瞭解的不夠呀,沒有發現spring-ws的優勢呀。自此,關於WebService的文章就暫時告一段落了。

最後

目前互聯網上不少大佬都有SpringBoot系列教程,若有雷同,請多多包涵了。原創不易,碼字不易,還但願你們多多支持。若文中有所錯誤之處,還望提出,謝謝。

老生常談

  • 我的QQ:499452441
  • 微信公衆號:lqdevOps

公衆號

我的博客:http://blog.lqdev.cn

完整示例:https://github.com/xie19900123/spring-boot-learning/tree/master/chapter-34

原文地址:http://blog.lqdev.cn/2018/11/12/springboot/chapter-thirty-four/

相關文章
相關標籤/搜索