最近项目中需要开发webservice对外提供服务,选用框架时主要考虑Axis和CXF。Axis跨语言是它的一大特点,但因为主程序是Spring Boot开发,CXF可以与它无缝对接。所以选用了CXF框架。在网上扒拉了许久,大多都是和Spring集成的例子,通过一番折腾,终于实现了Spring Boot和CXF的集成完成了任务的开发,在此坐下笔记。
一、添加依赖
项目是用gradle来管理依赖的,gradle文件如下:
group 'Test_CXF_Webservice'
version '1.0.1'
apply plugin: 'java'
apply plugin: 'spring-boot'
sourceCompatibility = 1.7
targetCompatibility = 1.7
repositories {
mavenCentral()
}
ext {
springBootVersion = "1.3.6.RELEASE"
springVersion = "4.2.7.RELEASE"
springSecurityVersion = "4.0.4.RELEASE"
cxfVersion = "3.1.7"
}
buildscript {
ext {
springBootVersion = "1.3.6.RELEASE"
}
// 这里主要添加spring-boot的插件的仓库
repositories {
maven { url "https://repo.spring.io/libs-release" }
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
dependencies {
compile (
"org.springframework.boot:spring-boot-starter-web:${springBootVersion}",
"org.apache.cxf:cxf-spring-boot-starter-jaxws:${cxfVersion}",
)
testCompile group: 'junit', name: 'junit', version: '4.11'
}
springBoot {
mainClass="com.webservice.WSApplication"
}
二、服务端
2.1、service
package com.webservice.service;
import com.webservice.bean.OperationResult;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
@WebService(name = "LayoutService", targetNamespace = "http://service.webservice.com/")
public interface Layout {
@WebMethod
String sayHello(@WebParam(name = "name") String name);
@WebMethod
OperationResult addLayout(@WebParam(name = "layoutName") String layoutName,
@WebParam(name = "layoutContent") String layoutContent);
}
这里实现了两个方法,其中第二个方法用于返回一个类对象。targetNamespace一般为包名的逆序。
package com.webservice.bean;
import java.io.Serializable;
public class OperationResult implements Serializable {
private static final long serialVersionUID = -5939599230753662529L;
private boolean succeed;
private String msg;
public OperationResult() {
}
public OperationResult(boolean succeed, String msg) {
this.succeed = succeed;
this.msg = msg;
}
//set get
@Override
public String toString() {
return "OperationResult{" +
"succeed=" + succeed +
", msg='" + msg + '\'' +
'}';
}
}
2.2、实现
package com.webservice.service;
import com.webservice.bean.OperationResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
@javax.jws.WebService(
serviceName = "LayoutServiceImpl", portName = "LayoutImpl",
targetNamespace = "http://service.webservice.com/",
endpointInterface = "com.webservice.service.Layout")
public class LayoutImpl implements Layout {
private static final Logger LOG = LoggerFactory.getLogger(LayoutImpl.class);
@Override
public String sayHello(String name) {
return "hello," + name;
}
@Override
public OperationResult addLayout(String layoutName, String layoutContent) {
LOG.info("layoutName:{}, layoutContent:{}", layoutName, layoutContent);
if (StringUtils.isEmpty(layoutName) || StringUtils.isEmpty(layoutContent)) {
return new OperationResult(false, "参数不能为空");
}
//TODO
return new OperationResult(true, null);
}
}
2.3、定义拦截器用于用户验证
package com.webservice.interceptor;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.binding.soap.saaj.SAAJInInterceptor;
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.NodeList;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPMessage;
public class AuthInterceptor extends AbstractPhaseInterceptor<SoapMessage>{
private static final Logger logger = LoggerFactory.getLogger(AuthInterceptor.class);
private SAAJInInterceptor saa = new SAAJInInterceptor();
private static final String USER_NAME = "admin";
private static final String USER_PASSWORD = "pass";
public AuthInterceptor() {
super(Phase.PRE_PROTOCOL);
getAfter().add(SAAJInInterceptor.class.getName());
}
@Override
public void handleMessage(SoapMessage message) throws Fault {
SOAPMessage mess = message.getContent(SOAPMessage.class);
if (mess == null) {
saa.handleMessage(message);
mess = message.getContent(SOAPMessage.class);
}
SOAPHeader head = null;
try {
head = mess.getSOAPHeader();
} catch (Exception e) {
logger.error("getSOAPHeader error: {}",e.getMessage(),e);
}
if (head == null) {
throw new Fault(new IllegalArgumentException("找不到Header,无法验证用户信息"));
}
NodeList users = head.getElementsByTagName("username");
NodeList passwords = head.getElementsByTagName("password");
if (users.getLength() < 1) {
throw new Fault(new IllegalArgumentException("找不到用户信息"));
}
if (passwords.getLength() < 1) {
throw new Fault(new IllegalArgumentException("找不到密码信息"));
}
String userName = users.item(0).getTextContent().trim();
String password = passwords.item(0).getTextContent().trim();
if(USER_NAME.equals(userName) && USER_PASSWORD.equals(password)){
logger.debug("admin auth success");
} else {
SOAPException soapExc = new SOAPException("认证错误");
logger.debug("admin auth failed");
throw new Fault(soapExc);
}
}
}
Interceptor是CXF架构中一个很有特色的模式。你可以在不对核心模块进行修改的情况下,动态添加很多功能。这对于CXF这个以处理消息为中心的服务框架来说是非常有用的,CXF通过在Interceptor中对消息进行特殊处理,实现了很多重要功能模块。这里就是采用拦截器进行用户验证。
2.4 配置
Spring Boot省去了诸多的XML配置,具体配置如下
package com.webservice.config;
import com.webservice.interceptor.AuthInterceptor;
import com.webservice.service.LayoutImpl;
import org.apache.cxf.Bus;
import org.apache.cxf.bus.spring.SpringBus;
import org.apache.cxf.jaxws.EndpointImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.xml.ws.Endpoint;
@Configuration
public class CXFConfig {
@Bean(name = Bus.DEFAULT_BUS_ID)
public SpringBus springBus() {
return new SpringBus();
}
@Bean
public LayoutImpl layout() {
return new LayoutImpl();
}
@Bean
public Endpoint endpoint() {
EndpointImpl endpoint = new EndpointImpl(springBus(), layout());
endpoint.publish("/layout");
endpoint.getInInterceptors().add(new AuthInterceptor());
return endpoint;
}
}
其中,Spring Boot已帮你自动注册了servlet,默认为
/services,如果需要修改(如修改成webservice),两个方式:
方式一:通过配置文件,在resources下新建application.properties文件
cxf.path=/webservice
方式二:通过代码,在CXFConfig中添加以下代码:
@Bean
public ServletRegistrationBean servletRegistrationBean() {
ServletRegistrationBean bean = new ServletRegistrationBean(new CXFServlet(), "/webservice/*");
bean.setLoadOnStartup(0);
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
2.5、主程序入口
package com.webservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
@SpringBootApplication
public class WSApplication extends SpringBootServletInitializer {
//可在在servlet中部署的war包
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(WSApplication.class);
}
public static void main(String[] args) throws Exception {
SpringApplication.run(WSApplication.class, args);
}
}
至此,服务端就完成了。
运行后,输入
http://localhost:8080/services/(使用默认配置),如下图
点击并查看wsdl,如下
<?xml version="1.0" encoding="UTF-8"?> <wsdl:definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://service.webservice.com/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:ns1="http://schemas.xmlsoap.org/soap/http" name="LayoutServiceImpl" targetNamespace="http://service.webservice.com/"> <wsdl:types> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://service.webservice.com/" elementFormDefault="unqualified" targetNamespace="http://service.webservice.com/" version="1.0"> <xs:element name="addLayout" type="tns:addLayout"/> <xs:element name="addLayoutResponse" type="tns:addLayoutResponse"/> <xs:element name="sayHello" type="tns:sayHello"/> <xs:element name="sayHelloResponse" type="tns:sayHelloResponse"/> <xs:complexType name="addLayout"> <xs:sequence> <xs:element minOccurs="0" name="layoutName" type="xs:string"/> <xs:element minOccurs="0" name="layoutContent" type="xs:string"/> </xs:sequence> </xs:complexType> <xs:complexType name="addLayoutResponse"> <xs:sequence> <xs:element minOccurs="0" name="return" type="tns:operationResult"/> </xs:sequence> </xs:complexType> <xs:complexType name="operationResult"> <xs:sequence> <xs:element minOccurs="0" name="msg" type="xs:string"/> <xs:element name="succeed" type="xs:boolean"/> </xs:sequence> </xs:complexType> <xs:complexType name="sayHello"> <xs:sequence> <xs:element minOccurs="0" name="name" type="xs:string"/> </xs:sequence> </xs:complexType> <xs:complexType name="sayHelloResponse"> <xs:sequence> <xs:element minOccurs="0" name="return" type="xs:string"/> </xs:sequence> </xs:complexType> </xs:schema> </wsdl:types> <wsdl:message name="addLayout"> <wsdl:part element="tns:addLayout" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:message name="addLayoutResponse"> <wsdl:part element="tns:addLayoutResponse" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:message name="sayHelloResponse"> <wsdl:part element="tns:sayHelloResponse" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:message name="sayHello"> <wsdl:part element="tns:sayHello" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:portType name="LayoutService"> <wsdl:operation name="addLayout"> <wsdl:input message="tns:addLayout" name="addLayout"> </wsdl:input> <wsdl:output message="tns:addLayoutResponse" name="addLayoutResponse"> </wsdl:output> </wsdl:operation> <wsdl:operation name="sayHello"> <wsdl:input message="tns:sayHello" name="sayHello"> </wsdl:input> <wsdl:output message="tns:sayHelloResponse" name="sayHelloResponse"> </wsdl:output> </wsdl:operation> </wsdl:portType> <wsdl:binding name="LayoutServiceImplSoapBinding" type="tns:LayoutService"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> <wsdl:operation name="addLayout"> <soap:operation soapAction="" style="document"/> <wsdl:input name="addLayout"> <soap:body use="literal"/> </wsdl:input> <wsdl:output name="addLayoutResponse"> <soap:body use="literal"/> </wsdl:output> </wsdl:operation> <wsdl:operation name="sayHello"> <soap:operation soapAction="" style="document"/> <wsdl:input name="sayHello"> <soap:body use="literal"/> </wsdl:input> <wsdl:output name="sayHelloResponse"> <soap:body use="literal"/> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:service name="LayoutServiceImpl"> <wsdl:port binding="tns:LayoutServiceImplSoapBinding" name="LayoutImpl"> <soap:address location="http://localhost:8080/services/layout"/> </wsdl:port> </wsdl:service> </wsdl:definitions>
通过此wdsl,编写客户端代码。
三、客户端
客户端可以通过两种方式调用服务端。
3.1、通过JaxWsDynamicClientFactory方式。
public class WSClient {
private static final Logger LOG = LoggerFactory.getLogger(WSClient.class);
private static final String USER_NAME = "admin";
private static final String PASS_WORD = "pass";
public static void main(String[] args) {
//方式一:通过JaxWsDynamicClientFactory方式
JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance();
Client client = dcf.createClient("http://localhost:8080/services/layout?wsdl");
client.getOutInterceptors().add(new ClientLoginInterceptor(USER_NAME, PASS_WORD));
Object[] objects = new Object[0];
try {
objects = client.invoke("sayHello", "jack");
} catch (java.lang.Exception e) {
e.printStackTrace();
LOG.error("[1]sayHello:{}", e.getMessage());
}
if (!ObjectUtils.isEmpty(objects)) {
LOG.debug("[1]sayHello result_class:" + objects[0].getClass());
LOG.debug("[1]sayHello result_msg:" + objects[0].toString());
} else {
LOG.debug("[1]getName result_null");
}
try {
objects = client.invoke("addLayout", new Object[] {"name", "content"});
} catch (java.lang.Exception e) {
e.printStackTrace();
LOG.error("[1]addLayout:{}", e.getMessage());
}
if (!ObjectUtils.isEmpty(objects)) {
LOG.debug("[1]addLayout result_class:" + objects[0].getClass());
OperationResult result = (OperationResult) objects[0];
LOG.debug("[1]addLayout result_succeed:" + result.isSucceed());
LOG.debug("[1]addLayout result_msg:" + result.getMsg());
} else {
LOG.debug("[1]addLayout result_null");
}
}
这种方式要注意的就是,如果调用的服务接口返回的是一个自定义对象,那么结果Object[]中的数据类型就成了这个自定义对象(组件帮你自动生成了这个对象),
但是你本地可能并没有这个类,所以需要自行转换处理,最简单的是新建一个跟返回结果一模一样的类进行强转。
客户端需要在header中添加用户信息,这样才能通过服务器的验证。package com.webservice.interceptor;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.headers.Header;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.namespace.QName;
import java.util.List;
public class ClientLoginInterceptor extends AbstractPhaseInterceptor<SoapMessage> {
private String username;
private String password;
public ClientLoginInterceptor(String username, String password) {
super(Phase.PREPARE_SEND);
this.username = username;
this.password = password;
}
@Override
public void handleMessage(SoapMessage soap) throws Fault {
List<Header> headers = soap.getHeaders();
Document doc = DOMUtils.createDocument();
Element auth = doc.createElement("authrity");
Element username = doc.createElement("username");
Element password = doc.createElement("password");
username.setTextContent(this.username);
password.setTextContent(this.password);
auth.appendChild(username);
auth.appendChild(password);
headers.add(0, new Header(new QName("tiamaes"),auth));
}
}
3.2、通过wsimport生成客户端代码
通过jdk自带的wsimport生成客户端代码。进入$JAVA_HOME/bin下,新建bin和src两个文件夹,执行以下命令:
wsimport -d ./bin -s ./src -keep http://localhost:8080/services/layout?wsdl
其中几个参数有以下几个,
-d:生成客户端执行类的class文件存放目录,
-s:生成客户端执行类的源文件存放目录,
-p:定义生成类的包名
-verbose:显示生成过程
需要注意的是:
无论-d或是-s后的目录必须在文件系统中存在,否则报错。
public class WSClient {
private static final Logger LOG = LoggerFactory.getLogger(WSClient.class);
private static final String USER_NAME = "admin";
private static final String PASS_WORD = "pass";
public static void main(String[] args) {
//方式二:通过wsimport生成客户端代码
LayoutServiceImpl impl = new LayoutServiceImpl();
impl.setHandlerResolver(new HandlerResolver() {
@Override
public List<Handler> getHandlerChain(PortInfo portInfo) {
List<Handler> handlerList = new ArrayList<Handler>();
handlerList.add(new ClientHandler(USER_NAME, PASS_WORD));
return handlerList;
}
});
try {
LayoutService layoutService = impl.getLayoutImpl();
String result_1 = layoutService.sayHello("jack");
LOG.debug("[2]sayHello:" + result_1);
OperationResult result_2 = layoutService.addLayout("name", "content");
LOG.debug("[2]addLayout result_succeed:" + result_2.isSucceed());
LOG.debug("[2]addLayout result_msg:" + result_2.getMsg());
} catch (SOAPFaultException e) {
LOG.error("SOAPFaultException occurs:{}", e.getMessage());
}
}
}
该方式的好处是可以像调用本地接口一样调用服务端方法,简单明了。缺点就是会生成一堆文件。
用户的头信息设置类:
package com.webservice.interceptor;
import javax.xml.namespace.QName;
import javax.xml.soap.*;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import java.util.Set;
public class ClientHandler implements SOAPHandler<SOAPMessageContext> {
private String username;
private String password;
public ClientHandler(String username, String password) {
this.username = username;
this.password = password;
}
public boolean handleMessage(SOAPMessageContext ctx) {
//出站,即客户端发出请求前,添加表头信息
Boolean request_p = (Boolean)ctx.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
if (request_p) {
try {
SOAPMessage msg = ctx.getMessage();
SOAPEnvelope env = msg.getSOAPPart().getEnvelope();
SOAPHeader hdr = env.getHeader();
if (hdr == null) hdr = env.addHeader();
//添加认证信息头
QName name = new QName("http://service.webservice.com/", "LayoutImpl");
SOAPHeaderElement header = hdr.addHeaderElement(name);
SOAPElement userElement = header.addChildElement("username");
userElement.addTextNode(username);
SOAPElement passElement = header.addChildElement("password");
passElement.addTextNode(password);
msg.saveChanges();
return true;
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
@Override
public boolean handleFault(SOAPMessageContext context) {
// TODO Auto-generated method stub
return false;
}
@Override
public void close(MessageContext context) {
// TODO Auto-generated method stub
}
@Override
public Set<QName> getHeaders() {
// TODO Auto-generated method stub
return null;
}
}
至此,一个简单的基于CXF的webservice完成!