前言
上一章節,講解了如何使用
Spring-WS
構建WebService
服務。其實,創建WebService
的方式有很多的,今天來看看如何使用apache cxf
來構建及調用WebService
服務。
一點知識
何為Apache-CXF
Apache CXF
是一個開源的Services
框架,CXF
幫助您利用Frontend
編程 API 來構建和開發Services,像JAX-WS
、JAX-RS
。這些Services
可以支持多種協議,比如:SOAP
、XML/HTTP
、RESTful HTTP
或者CORBA
,並且可以在多種傳輸協議上運行,比如:HTTP
、JMS
或者JBI
,CXF大大簡化了 Services 的創建,同時它可以天然地和Spring進行無縫集成。
以下是官網給出的介紹:https://github.com/apache/cxf
最常用的是使用cxf
開發web-service
。本身是基於JAX-WS
規范來實現的。當然,本身CXF
也實現了JAX-RS
規范來實現RESTFul Service
。
關於JAX-WS規范
JAX-WS
全稱:Java API for XML-Based Web Services
。JAX-WS
是一種編程模型,它通過支持將基於注釋的標准模型用於開發Web Service
應用程序和客戶機來簡化應用程序開發。
JAX-WS
是Java程序設計語言一個用來創建Web服務的API。
- 在服務器端,用戶只需要通過
Java
語言定義遠程調用所需要實現的接口SEI(service endpoint interface)
,並提供相關的實現,通過調用JAX-WS
的服務發布接口就可以將其發布為WebService
接口。 - 在客戶端,用戶可以通過
JAX-WS
的API創建一個代理(用本地對象來替代遠程的服務)來實現對於遠程服務器端的調用。當然JAX-WS
也提供了一組針對底層消息進行操作的API調用,你可以通過Dispatch
直接使用SOAP消息或XML消息發送請求或者使用Provider處理SOAP或XML消息。
常用注解介紹
JAX-WS
提供了一系列的注解,可以對WebService
的接口規范化。以下介紹下最常用的幾個注解。
-
@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
元素的映射。
其他注解,可以查看: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
。
所以,修改訪問路徑還可以通過配置項: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 ,驗證是否發布成功。
自此,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;
}
}
注意:除了使用JaxWsProxyFactoryBean
和JaxWsDynamicClientFactory
調用外,還可以直接使用自動生成的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的攔截器分為兩種:InInterceptor
和OutInterceptor
。顯而易見,InInterceptor
可以處理soap請求消息,OutInterceptor
可以處理soap響應消息。其攔截器都繼承至AbstractPhaseInterceptor<Message>
接口類,而且,本身也自帶了很多的攔截器,可以自行添加看看,比如日志攔截器之類的:LoggingInInterceptor
和LoggingOutInterceptor
。
請求流程圖:
攔截器鏈的階段:
輸入攔截器鏈有如下幾個階段,這些階段按照在攔截器鏈中的先后順序排列。
輸出攔截器鏈有如下幾個階段,這些階段按照在攔截器鏈中的先后順序排列。
具體名稱,可查看: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,看看有攔截。
異常請求:
參考資料
總結
本章節主要簡單介紹了
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/