使用CXF開發RestFul WebService問題解決方案
最近在項目中,因為幾個系統自己需要數據交換,所以采用進來都比較流行的RestFul風格WebService,實現框架采用apache的cxf,apache的東西一直以來都是比較的好用,回話少說,進入正題。
首先,提出兩個問題,已經解決方案,后面跟上代碼。
1、cxf中如何實現java中泛型的數據序列化和反序列化(通常使用json、xml格式,cxf默認不支持泛型)
2、cxf后臺異常,在前臺如何反映
問題1答案比較簡單,cxf中的數據序列化是可以替換掉使用你實現MessageBodyReader<Object>和MessageBodyWriter<Object>接口就可以啦,針對xml,cxf采用stax2、jaxb、xmlschema、Woodstox庫,針對json默認使用jettison實現的幾乎都是codehaus作品。知道cxf序列化和反序列化方式就比較容易解決問題啦。默認情況下cxf的jettison對泛型序列化存在問題,因為時間緊(一天就要做好restful webservice部署),沒有具體去研究實現問題,我只是在之前使用過jackson,去處理json問題,而且cxf擁有jackson的MessageBodyReader和MessageBodyWriter實現類,我只要導入包并告訴cxf使用我指定的json provider就可以了,所以在客戶端和服務器端雙方都指定json privoder,jackson 庫對json序列化實現非常的到位,異常的強大。我們都知道,只要java源碼中指定的泛型類我沒都可以反射出來,如果使用泛型站位符,就沒法反射,因為java中的擦除法的原因(比如List<String>、List<T>,前面是清楚的指定泛型參數類型,后面一種是在運行時指定),我這里討論的也是指定泛型參數類型情況下,jackson在這種情況下已經支持,所以不需要自己實現MessageBodyReader和MessageBodyWriter接口。如果是使用xml方式,除自己實現接口外,有更簡單的方法,那就是在你的泛型類上面指定@XmlSeeAlso({某某類1.class,某某類2.class...})
問題2同樣的比較簡單,因為基于http的restful實現時,服務器返回數據的時候都會告訴客戶端一個響應狀態嗎,就是我們常看到的200、404、500等,cxf框架的rs webservice客戶端實現是通過判斷狀態大于等于300時,拋出異常webapplicationexception,所以如果服務器端有異常時,通過設置狀態就可以實現,并返回Response(通過實現ExceptionMapper<E extends Throwable>接口,并加入到provider實現),如果客戶端需要錯誤消息(這里不得不說jcp設計的jsr311比較的細膩),可以在Response中設置,客戶端catch掉webapplicationexception異常,并可以讀取錯誤消息。cxf到這里還沒有完,cxf提供一個ResponseExceptionMapper接口,客戶端實現這個接口并加入到provider中,客戶端在調用的時候就不用去處理cxf的異常webapplicationexception,而是你自己接口的異常,因為客戶端在調用webservice時,cxf創建調用接口的代理,代理在接收到300錯誤時,他知道服務器是返回webapplicationexception異常,他就是用你的ResponseExceptionMapper處理異常,因為這個接口中唯一方法fromResponse(Response r)返回的是一個異常。也就是說,實現這個類方法時,可以讀取webapplicationexception中的Response所包含的消息,并要求返回一次異常對象。這樣就達到客戶端不用關心webapplicationexception異常而是關系自己接口上面聲明的異常。
代碼:
@XmlRootElement(name="Customer")
public class Customer {
private String id;
private String name;
private Date birthday;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
}
@XmlRootElement(name="Me")
public class Me implements Serializable {
private String name;
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
}
@XmlRootElement(name = "Page")
@XmlSeeAlso({Customer.class,Me.class})
public class Page<T> implements Serializable{
/**
*
*/
private static final long serialVersionUID = 5859907455479273251L;
public static int DEFAULT_PAGE_SIZE = 10;
private int pageSize = DEFAULT_PAGE_SIZE; // 每頁的記錄數
private long start; // 當前頁第一條數據在List中的位置,從0開始
private List<T> data; // 當前頁中存放的記錄,類型一般為List
private long totalCount; // 總記錄數
/**
* 構造方法,只構造空頁.
*/
public Page() {
this(0, 0, DEFAULT_PAGE_SIZE, new ArrayList<T>());
}
public Page(int pageSize) {
this(0, 0, pageSize, new ArrayList<T>());
}
/**
* 默認構造方法.
*
* @param start
* 本頁數據在數據庫中的起始位置
* @param totalSize
* 數據庫中總記錄條數
* @param pageSize
* 本頁容量
* @param data
* 本頁包含的數據
*/
public Page(long start, long totalSize, int pageSize, List<T> data) {
this.pageSize = pageSize;
this.start = start;
this.totalCount = totalSize;
this.data = data;
}
/**
* 取總記錄數.
*/
public long getTotalCount() {
return this.totalCount;
}
/**
* 取總頁數.
*/
public long getTotalPageCount() {
if (totalCount % pageSize == 0)
return totalCount / pageSize;
else
return totalCount / pageSize + 1;
}
/**
* 取每頁數據容量.
*/
public int getPageSize() {
return pageSize;
}
/**
* 取當前頁中的記錄.
*/
public List<T> getResult() {
return data;
}
/**
* 取該頁當前頁碼,頁碼從1開始.
*/
public long getCurrentPageNo() {
return start / pageSize + 1;
}
/**
* 該頁是否有下一頁.
*/
public boolean hasNextPage() {
return this.getCurrentPageNo() < this.getTotalPageCount();
}
/**
* 該頁是否有上一頁.
*/
public boolean hasPreviousPage() {
return this.getCurrentPageNo() > 1;
}
/**
* 獲取任一頁第一條數據在數據集的位置,每頁條數使用默認值.
*
* @see #getStartOfPage(int,int)
*/
protected static int getStartOfPage(int pageNo) {
return getStartOfPage(pageNo, DEFAULT_PAGE_SIZE);
}
/**
* 獲取任一頁第一條數據在數據集的位置.
*
* @param pageNo
* 從1開始的頁號
* @param pageSize
* 每頁記錄條數
* @return 該頁第一條數據
*/
public static int getStartOfPage(int pageNo, int pageSize) {
return (pageNo - 1) * pageSize;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this,
ToStringStyle.SHORT_PREFIX_STYLE);
}
/**
* @return the start
*/
public long getStart() {
return start;
}
/**
* @param start the start to set
*/
public void setStart(long start) {
this.start = start;
}
/**
* @return the data
*/
public List<T> getData() {
return data;
}
/**
* @param data the data to set
*/
public void setData(List<T> data) {
this.data = data;
}
/**
* @param pageSize the pageSize to set
*/
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
/**
* @param totalCount the totalCount to set
*/
public void setTotalCount(long totalCount) {
this.totalCount = totalCount;
}
}public class ServiceException extends RuntimeException {
private static final long serialVersionUID = 7607640803750403555L; public ServiceException() { super(); }
public ServiceException(String message) { super(message); }
public ServiceException(String message, Throwable cause) { super(message, cause); }
public ServiceException(Throwable cause) { super(cause); } }
@Path(value = "/customer")
@Produces({"application/xml","application/json"})
public interface CustomerService {
@GET
@Path(value = "/{id}/info")
Customer findCustomerById(@PathParam("id")String id);
@GET
@Path(value = "/search")
Customer findCustomerByName(@QueryParam("name")String name);
@POST
@Path(value = "/all")
List<Customer> findAllCustomer();
@POST
@Path(value = "/page")
Page<Customer> findPageCustomer() throws ServiceException;
@POST
@Path(value = "/pageMe")
Page<Me> findPage();
}
public class CustomerServiceImpl implements CustomerService {
public Customer findCustomerById(String id) {
Customer customer = new Customer();
customer.setId(id);
customer.setName(id);
customer.setBirthday(Calendar.getInstance().getTime());
return customer;
}
public Customer findCustomerByName(String name) {
Customer customer = new Customer();
customer.setId(name);
customer.setName(name);
customer.setBirthday(Calendar.getInstance().getTime());
return customer;
}
/** (non-Javadoc)
* @see edu.xdev.restful.CustomerService#findAllCustomer()
*/
@Override
public List<Customer> findAllCustomer() {
List<Customer> tar = new LinkedList<Customer>();
Customer customer = new Customer();
customer.setId("e24234");
customer.setName("張三");
customer.setBirthday(Calendar.getInstance().getTime());
tar.add(customer);
customer = new Customer();
customer.setId("324324");
customer.setName("李四");
customer.setBirthday(Calendar.getInstance().getTime());
tar.add(customer);
return tar;
}
/** (non-Javadoc)
* @see edu.xdev.restful.CustomerService#findPageCustomer()
*/
public Page<Customer> findPageCustomer() throws ServiceException {
List<Customer> tar = new LinkedList<Customer>();
Customer customer = new Customer();
customer.setId("e24234");
customer.setName("張三");
customer.setBirthday(Calendar.getInstance().getTime());
tar.add(customer);
customer = new Customer();
customer.setId("324324");
customer.setName("李四");
customer.setBirthday(Calendar.getInstance().getTime());
tar.add(customer);
Page<Customer> page = new Page<Customer>(1, 2, 1, tar);
if(1==1){
throw new ServiceException("abcd");
}
return page;
}
/** (non-Javadoc)
* @see edu.xdev.restful.CustomerService#findPage()
*/
public Page<Me> findPage() {
List<Me> tar = new LinkedList<Me>();
Me m = new Me();
m.setName("中文");
tar.add(m);
m = new Me();
m.setName("English");
tar.add(m);
Page<Me> page = new Page<Me>(1, 2, 1, tar);
return page;
}
}@Provider
public class InvokeFaultExceptionMapper implements ExceptionMapper<ServiceException> {
/** (non-Javadoc)
* @see javax.ws.rs.ext.ExceptionMapper#toResponse(java.lang.Throwable)
*/
@Override
public Response toResponse(ServiceException ex) {
ResponseBuilder rb = Response.status(Response.Status.INTERNAL_SERVER_ERROR);
rb.type("application/json;charset=UTF-8");
rb.entity(ex);
rb.language(Locale.SIMPLIFIED_CHINESE);
Response r = rb.build();
return r;
}
}
public class ServiceExceptionMapper implements ResponseExceptionMapper<ServiceException>{
/** (non-Javadoc)
* @see org.apache.cxf.jaxrs.client.ResponseExceptionMapper#fromResponse(javax.ws.rs.core.Response)
*/
@Override
public ServiceException fromResponse(Response r) {
Object obj = r.getEntity();
ObjectMapper mapper = new ObjectMapper();
mapper.configure(org.codehaus.jackson.map.DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
try {
return mapper.readValue(obj.toString(), ServiceException.class);
} catch (JsonParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (JsonMappingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return new ServiceException(obj.toString());
}
}
public class Server {
/**
* @param args
*/
public static void main(String[] args) {
JAXRSServerFactoryBean factoryBean = new JAXRSServerFactoryBean();
factoryBean.getInInterceptors().add(new LoggingInInterceptor());
factoryBean.getOutInterceptors().add(new LoggingOutInterceptor());
factoryBean.setResourceClasses(CustomerServiceImpl.class);
List<Object> list = new LinkedList<Object>();
list.add(new org.codehaus.jackson.jaxrs.JacksonJsonProvider());
list.add(new InvokeFaultExceptionMapper());
factoryBean.setProviders(list);
factoryBean.setAddress("http://localhost:9000/ws/jaxrs");
factoryBean.create();
}
}
public class ClientTest {
private static List<Object> getJacksonJsonProvider(){
List<Object> providers = new LinkedList<Object>();
providers.add(new ServiceExceptionMapper());
JacksonJsonProvider provider = new JacksonJsonProvider();
provider.configure(org.codehaus.jackson.map.DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
providers.add(provider);
return providers;
}
/**
* @param args
*/
public static void main(String[] args) {
try{
Page<Me> pages = getServiceInstance(CustomerService.class).findPage();
for(Me u:pages.getResult()){
System.out.println(u.getName());
}
Page<Customer> page = getServiceInstance(CustomerService.class).findPageCustomer();
for(Customer u:page.getResult()){
System.out.println(u.getName());
}
}catch(WebApplicationException e){
if(e instanceof WebApplicationException){
WebApplicationException we = (WebApplicationException)e;
System.out.println(we.getMessage());
//System.out.println(we.getCause().getMessage());
}
e.printStackTrace();
}catch(Exception e){
e.printStackTrace();
}
}
private static Map<Class<?>, Object> repos = new HashMap<Class<?>, Object>();
private static String baseUrl;
static {
baseUrl = "http://localhost:9000/ws/jaxrs";
}
@SuppressWarnings("unchecked")
public static <T> T getServiceInstance(Class<T> clazz){
T t = (T) repos.get(clazz);
if(t==null){
synchronized (clazz) {
t = (T) repos.get(clazz);
if(t==null){
t = JAXRSClientFactory.create(baseUrl, clazz, getJacksonJsonProvider());
Client client = WebClient.client(t);
WebClient.getConfig(client).getInInterceptors().add(new LoggingInInterceptor());
WebClient.getConfig(client).getInFaultInterceptors().add(new LoggingInInterceptor());
WebClient.getConfig(client).getOutFaultInterceptors().add(new LoggingOutInterceptor());
WebClient.getConfig(client).getOutInterceptors().add(new LoggingOutInterceptor());
client.accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON).acceptEncoding("UTF-8");
repos.put(clazz, t);
}
}
}
return t;
}
}
總結:
問題1:針對json格式使用jackson替換jettison庫。針對xml格式,只要在指定泛型參數類上面同同過@XmlSeeAlso注解指定泛型參數類class即可。
問題2:通過ExceptionMapper接口和webapplicationexception異常實現,如果想更進一步可以加上ResponseExceptionMapper完成更舒坦的WebService設計
這里特別指出一下。MessageBodyReader、MessageBodyWriter、ExceptionMapper、webapplicationexception、XmlSeeAlso都是java規范中的api,ResponseExceptionMapper為cxf中的api。如果大家選擇maven依賴管理cxf,注意cxf默認的jax-rs api依賴,其中2.7.4中默認依賴是javax.ws.rs-api-2.0-m10.jar,cxf2.5.10默認依賴是jsr311-api.1.1.1.jar。也就是說,要默認按照它依賴的jar,不要以為jax-rs 2.0的api還是m階段,就降低api使用低版本正是版本jsr311-api.1.1.1.jar,這里在cxf中是有問題的。cxf官網上面明明說cxf目前實JAX-RS 1.1 and JAX-RS 1.0 (JSR-311),可實際已經開始支持jax-rs2版本,而jax-rs2 還沒正式發布,所以cxf對jax-rs2實現自然就有問題。我開發時,被這里害慘啦,最終選2.5.10版本