近期使用struts2的rest-plugin,參考官方示例struts2-rest-showcase,做了一個restful service小項目,但官網提供的這個示例過於簡單,埋下了巨坑無數,下面是一些遇到的問題及解決辦法:
注:下面這些問題,很多是相互關聯的,要解決一個,得同時解決另一個。
一、與config-browser-plugin、convension-plugin、非rest Action 共存的問題
rest-plugin的氣場實在太強,一旦使用,config-browser-plugin、convension-plugin這二個plugin就掛了
解決思路:將所有rest服務,都放在/rest/路徑下,用package的namespace把它隔離出來,其它常規的action,放在其它路徑,這樣二者就不沖突了

1 <!-- Overwrite Convention --> 2 <constant name="struts.convention.action.suffix" value="Controller" /> 3 <constant name="struts.convention.action.mapAllMatches" value="true" /> 4 <!--<constant name="struts.rest.content.restrictToGET" value="false" />--> 5 <constant name="struts.convention.default.parent.package" value="rest-default" /> 6 <constant name="struts.convention.package.locators" value="action" /> 7 <!-- <constant name="struts.rest.namespace" value="/rest" /> --> 8 <constant name="struts.convention.action.includeJars" value=".*?/_wl_cls_gen.*?jar(!/)?" /> 9 <constant name="struts.convention.exclude.parentClassLoader" value="true" /> 10 <constant name="struts.convention.action.fileProtocols" value="jar,zip,vfsfile,vfszip" /> 11 12 <constant name="struts.mapper.class" value="org.apache.struts2.dispatcher.mapper.PrefixBasedActionMapper" /> 13 <constant name="struts.mapper.prefixMapping" value="/rest:rest,:struts" /> 14 <constant name="struts.mapper.alwaysSelectFullNamespace" value="false" /> 15 16 <package name="default" namespace="/rest" extends="rest-default" />
二、攔截器及ModelDrive的問題
如果自定義攔截器(比如:自定義異常攔截器),默認情況下是無法攔截rest的Action
解決辦法:
a) strut2.xml中定義二個package:rest-package、page-package,並在這二個package中,加上自己的攔截器,完整strut2.xml參考下面的內容:

1 <?xml version="1.0" encoding="UTF-8" ?> 2 3 4 <!DOCTYPE struts PUBLIC 5 "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN" 6 "http://struts.apache.org/dtds/struts-2.3.dtd"> 7 8 <struts> 9 10 <bean name="xmlHandler" type="org.apache.struts2.rest.handler.ContentTypeHandler" 11 class="com.cnblogs.yjmyzz.handler.XStreamHandler" /> 12 13 <bean name="jsonHandler" type="org.apache.struts2.rest.handler.ContentTypeHandler" 14 class="com.cnblogs.yjmyzz.handler.JacksonHandler" /> 15 16 <!-- Overwrite Convention --> 17 <constant name="struts.convention.action.suffix" value="Controller" /> 18 <constant name="struts.convention.action.mapAllMatches" value="true" /> 19 <!--<constant name="struts.rest.content.restrictToGET" value="false" />--> 20 <constant name="struts.convention.default.parent.package" 21 value="rest-default" /> 22 <constant name="struts.convention.package.locators" value="action" /> 23 <!-- <constant name="struts.rest.namespace" value="/rest" /> --> 24 <constant name="struts.convention.action.includeJars" value=".*?/_wl_cls_gen.*?jar(!/)?" /> 25 <constant name="struts.convention.exclude.parentClassLoader" 26 value="true" /> 27 <constant name="struts.convention.action.fileProtocols" value="jar,zip,vfsfile,vfszip" /> 28 29 <constant name="struts.mapper.class" 30 value="org.apache.struts2.dispatcher.mapper.PrefixBasedActionMapper" /> 31 <constant name="struts.mapper.prefixMapping" value="/rest:rest,:struts" /> 32 <constant name="struts.mapper.alwaysSelectFullNamespace" 33 value="false" /> 34 35 <package name="base-default" extends="struts-default"> 36 <global-results> 37 <result name="error">/WEB-INF/common/error.jsp</result> 38 </global-results> 39 40 <global-exception-mappings> 41 <exception-mapping exception="java.lang.Exception" 42 result="error" /> 43 </global-exception-mappings> 44 </package> 45 46 <package name="rest-package" namespace="/rest" extends="base-default"> 47 <result-types> 48 <result-type name="redirect" 49 class="org.apache.struts2.dispatcher.ServletRedirectResult"> 50 <param name="statusCode">303</param> 51 </result-type> 52 <result-type name="redirectAction" 53 class="org.apache.struts2.dispatcher.ServletActionRedirectResult"> 54 <param name="statusCode">303</param> 55 </result-type> 56 </result-types> 57 <interceptors> 58 <interceptor name="rest" 59 class="org.apache.struts2.rest.ContentTypeInterceptor" /> 60 <interceptor name="restWorkflow" 61 class="org.apache.struts2.rest.RestWorkflowInterceptor" /> 62 <interceptor name="messages" 63 class="org.apache.struts2.interceptor.MessageStoreInterceptor" /> 64 <interceptor name="exceptionInterceptor" 65 class="com.cnblogs.yjmyzz.interceptor.ExceptionInterceptor"> 66 </interceptor> 67 <interceptor-stack name="restDefaultStack"> 68 <interceptor-ref name="exception" /> 69 <interceptor-ref name="alias" /> 70 <interceptor-ref name="servletConfig" /> 71 <interceptor-ref name="messages"> 72 <param name="operationMode">AUTOMATIC</param> 73 </interceptor-ref> 74 <interceptor-ref name="prepare" /> 75 <interceptor-ref name="i18n" /> 76 <interceptor-ref name="chain" /> 77 <interceptor-ref name="debugging" /> 78 <interceptor-ref name="profiling" /> 79 <interceptor-ref name="actionMappingParams" /> 80 <interceptor-ref name="scopedModelDriven" /> 81 <interceptor-ref name="modelDriven"> 82 <param name="refreshModelBeforeResult">true</param> 83 </interceptor-ref> 84 <interceptor-ref name="fileUpload" /> 85 <interceptor-ref name="checkbox" /> 86 <interceptor-ref name="staticParams" /> 87 <interceptor-ref name="params"> 88 <param name="excludeParams">dojo\..*</param> 89 </interceptor-ref> 90 <interceptor-ref name="rest" /> 91 <interceptor-ref name="conversionError" /> 92 <interceptor-ref name="validation"> 93 <param name="excludeMethods">input,back,cancel,browse,index,show,edit,editNew,deleteConfirm,destroy,create</param> 94 </interceptor-ref> 95 <interceptor-ref name="restWorkflow"> 96 <param name="excludeMethods">input,back,cancel,browse,index,show,edit,editNew,deleteConfirm,destroy,create</param> 97 </interceptor-ref> 98 <interceptor-ref name="exceptionInterceptor" /> 99 </interceptor-stack> 100 </interceptors> 101 <default-interceptor-ref name="restDefaultStack" /> 102 <default-class-ref class="org.apache.struts2.rest.RestActionSupport" /> 103 </package> 104 105 <package name="page-package" namespace="/" extends="base-default"> 106 <interceptors> 107 <interceptor name="exceptionInterceptor" 108 class="com.cnblogs.yjmyzz.interceptor.ExceptionInterceptor"> 109 </interceptor> 110 <interceptor-stack name="appStack"> 111 <interceptor-ref name="defaultStack"> 112 <param name="modelDriven.refreshModelBeforeResult">true</param> 113 </interceptor-ref> 114 <interceptor-ref name="exceptionInterceptor" /> 115 </interceptor-stack> 116 </interceptors> 117 <default-interceptor-ref name="appStack" /> 118 </package> 119 120 </struts>
b) 所有rest Action繼承自一個自定義基類,所有常規page的Action,繼承自另一個自定義基類
這二個基類用@ParentPackage 指定package,分別對應struts2.xml中的配置,這樣運行時,不管是rest action,還是非rest action,都能被攔截器攔截

1 package com.cnblogs.yjmyzz.action.base; 2 3 import org.apache.struts2.convention.annotation.ParentPackage; 4 5 import com.opensymphony.xwork2.ModelDriven; 6 import com.opensymphony.xwork2.ValidationAwareSupport; 7 8 @ParentPackage("rest-package") 9 public abstract class RestBaseAction extends ValidationAwareSupport implements 10 ModelDriven<Object> { 11 12 private static final long serialVersionUID = -8773131281804917145L; 13 14 public abstract Object getModel(); 15 16 }

1 package com.cnblogs.yjmyzz.action.base; 2 3 import org.apache.struts2.convention.annotation.ParentPackage; 4 5 import com.opensymphony.xwork2.ActionSupport; 6 7 @ParentPackage("page-package") 8 public class PageBaseAction extends ActionSupport { 9 10 private static final long serialVersionUID = 2323603138082550798L; 11 12 }
另外:官方的示例為了簡便,在setId方法里,直接給Model賦值了,但這有點誤導,因為攔截器攔截到的方法,並不是setId(),而是show()/index()之類的方法,所以應該在show方法里,調用 model = xxx.getModel(id),否則按原來的寫法,如果getModel這里報錯 -> setId()報錯,但show()方法並沒有出錯,攔截器會認為沒有異常發生。

1 // GET /rest/orders/1 2 public HttpHeaders show() { 3 if (id != null) { 4 // 如果id=x,演示攔截異常處理 5 if (id.equals("x")) { 6 testException(); 7 } 8 this.model = ordersService.get(id); 9 } 10 return new DefaultHttpHeaders("show"); 11 } 12 13 public void setId(String id) { 14 this.id = id; 15 }
三、返回XML節點的別名(alias)問題
默認情況下,返回的xml根節點為dto對應的完整package名,看上去很別扭
解決方法:
dto的class上,用@XStreamAlias指定別名

1 @XStreamAlias("order") 2 public class Order {}
然后再創建自己的XmlHandler,為了節省系統開銷,下面的代碼用了一個單例:

1 package com.cnblogs.yjmyzz.handler; 2 3 import com.thoughtworks.xstream.XStream; 4 5 public class XStreamFactory { 6 7 private XStreamFactory() { 8 } 9 10 private static XStream xStream = null; 11 12 public static XStream getInstance() { 13 if (xStream == null) { 14 xStream = new XStream(); 15 xStream.setMode(XStream.NO_REFERENCES); 16 } 17 return xStream; 18 } 19 20 }

1 package com.cnblogs.yjmyzz.handler; 2 3 import java.io.IOException; 4 import java.io.Reader; 5 import java.io.Writer; 6 7 import org.apache.struts2.rest.handler.ContentTypeHandler; 8 9 import com.cnblogs.yjmyzz.dto.Order; 10 import com.cnblogs.yjmyzz.dto.OrderList; 11 import com.thoughtworks.xstream.XStream; 12 13 public class XStreamHandler implements ContentTypeHandler { 14 15 public String fromObject(Object obj, String resultCode, Writer out) 16 throws IOException { 17 if (obj != null) { 18 XStream xstream = XStreamFactory.getInstance(); 19 xstream.processAnnotations(obj.getClass()); 20 xstream.toXML(obj, out); 21 } 22 return null; 23 } 24 25 public void toObject(Reader in, Object target) { 26 XStream xstream = XStreamFactory.getInstance(); 27 xstream.alias("data", OrderList.class); 28 xstream.alias("order", Order.class); 29 xstream.processAnnotations(target.getClass()); 30 xstream.fromXML(in, target); 31 } 32 33 public String getContentType() { 34 return "application/xml"; 35 } 36 37 public String getExtension() { 38 return "xml"; 39 } 40 41 }
注:別名一定要在toObject方法里,明確指定,否則別名的注解不起作用。
最后在struts2.xml里,還要注冊bean,參考前面完整的xml內容。
四、返回JSON的Date屬性格式化的問題
默認情況下,如果model有日期型屬性,返回的json格式十分長,看上去太臃腫,類似的,可以自己定義ContentTypeHandler來解決

1 package com.cnblogs.yjmyzz.handler; 2 3 import org.codehaus.jackson.map.ObjectMapper; 4 5 public class JacksonFactory { 6 7 private JacksonFactory() { 8 9 } 10 11 private static ObjectMapper objectMapper = null; 12 13 public static ObjectMapper getObjectMapper() { 14 if (objectMapper == null) { 15 objectMapper = new ObjectMapper(); 16 } 17 return objectMapper; 18 } 19 20 }

1 package com.cnblogs.yjmyzz.handler; 2 3 import java.io.IOException; 4 import java.io.Reader; 5 import java.io.Writer; 6 7 import org.apache.logging.log4j.LogManager; 8 import org.apache.logging.log4j.Logger; 9 import org.apache.struts2.rest.handler.ContentTypeHandler; 10 import org.springframework.beans.BeanUtils; 11 12 public class JacksonHandler implements ContentTypeHandler { 13 14 Logger logger = LogManager.getLogger(this.getClass()); 15 16 public String fromObject(Object obj, String resultCode, Writer out) 17 throws IOException { 18 if (obj != null) { 19 JacksonFactory.getObjectMapper().writeValue(out, obj); 20 } 21 return null; 22 } 23 24 public void toObject(Reader in, Object target) { 25 try { 26 Object origin = JacksonFactory.getObjectMapper().readValue(in, 27 target.getClass()); 28 BeanUtils.copyProperties(origin, target); 29 30 } catch (Exception e) { 31 e.printStackTrace(); 32 logger.error(e); 33 } 34 35 } 36 37 public String getContentType() { 38 return "application/json;charset=UTF-8"; 39 } 40 41 public String getExtension() { 42 return "json"; 43 } 44 45 }
五、restful service 該返回哪種視圖,xhtml? json? xml?
通常用rest-plugin,是為了開發rest-service,但是官網的示例返回的默認都是頁面視圖,這個顯然不適合,最理想情況是,如果在頁面上操作,操作完以后,應該返回頁面視圖(即: xxx.xhtml),如果是用xml參數進來的,應該返回xml視圖(即: xxx.xml),如果是ajax用json post過來的,應該返回到json視圖(即:xxx.json)
解決辦法:根據Request的Header來判斷來源,然后做相應的分支處理

1 // POST /orders 2 public HttpHeaders create() throws IOException { 3 ordersService.save(model); 4 HttpServletResponse response = ServletActionContext.getResponse(); 5 HttpServletRequest request = ServletActionContext.getRequest(); 6 String accept = request.getHeader("Accept"); 7 if (accept.contains("text/html")) { // 頁面視圖過來的 8 response.sendRedirect("orders/"); 9 } else if (accept.contains("text/xml")) { // 發送xml過來的 10 response.sendRedirect("orders/" + model.getId() + ".xml"); 11 } else { // 其它的返回json視圖 12 response.sendRedirect("orders/" + model.getId() + ".json"); 13 } 14 return null; 15 }
六、json post到service,model取不到值的問題
這個問題最惡心,連官方默認提供的org.apache.struts2.rest.handler.JsonLibHandler都有問題,原因在json反序列化的機制,大家可以感受下這段代碼:

1 @Test 2 public void testJson() { 3 String test = "{\"id\":\"3\",\"clientName\":\"Bob\",\"amount\":33,\"createTime\":\"1413947088717\"}"; 4 Order order = new Order(); 5 6 System.out.println(order); 7 System.out.println(order.hashCode()); 8 9 System.out.println("----"); 10 11 toObjectJson(test, order); 12 13 System.out.println("----"); 14 15 System.out.println(order); 16 System.out.println(order.hashCode()); 17 } 18 19 public void toObjectJson(String in, Object target) { 20 try { 21 target = JacksonFactory.getObjectMapper().readValue(in, 22 target.getClass()); 23 System.out.println(target); 24 System.out.println(target.hashCode()); 25 26 } catch (Exception e) { 27 e.printStackTrace(); 28 29 } 30 }
輸出結果:
id:null,clientName:null,amount:0,createTime:Wed Oct 22 15:05:12 CST 2014
29791
----
id:3,clientName:Bob,amount:33,createTime:Wed Oct 22 11:04:48 CST 2014
2137470
----
id:null,clientName:null,amount:0,createTime:Wed Oct 22 15:05:12 CST 2014
29791
雖然傳遞的參數是Object,因java只有值傳遞,這里傳遞的值即為對象的“指針地址值”,但是json內部反序列化時,入口並非這個指針值,而是xxx.getClass(),即類型指針,導致最后toObject執行完,原來的指針是啥還是啥,跟反序列過程中"新創建"出來的新Object instance,完全豪無關聯。因此,不得不改造成

1 public void toObject(Reader in, Object target) { 2 try { 3 Object origin = JacksonFactory.getObjectMapper().readValue(in, 4 target.getClass()); 5 BeanUtils.copyProperties(origin, target); 6 7 } catch (Exception e) { 8 e.printStackTrace(); 9 logger.error(e); 10 } 11 12 }
手動把新對象的屬性,復制到target對象上,這樣就保證了反序列后的結果,在toObject執行完以后,會反映到target上。
注:可能有朋友會問了,為什么只有json會這樣,xml不會呢?再仔細看下XStreamHandler的toObject方法

1 public void toObject(Reader in, Object target) { 2 XStream xstream = XStreamFactory.getInstance(); 3 xstream.alias("data", OrderList.class); 4 xstream.alias("order", Order.class); 5 xstream.processAnnotations(target.getClass()); 6 xstream.fromXML(in, target); 7 }
最后一行xstream.fromXML(in, target);這是開始xml->object的入口,這里傳遞的就是target的地址對應的值,而不是象json那樣是xxx.getClass()。如果進一步看源碼,最后會發現執行的是com.thoughtworks.xstream.core.TreeUnmarshaller類里的

1 public TreeUnmarshaller( 2 Object root, HierarchicalStreamReader reader, ConverterLookup converterLookup, 3 Mapper mapper) { 4 this.root = root; 5 this.reader = reader; 6 this.converterLookup = converterLookup; 7 this.mapper = mapper; 8 }
整個過程,都沒有新對象實例創建,所以相應的變化,能一直保持到toObject調用完成后。
七、id參數太單一的問題
這個其實並不是大太的問題,GET方式下,url里本來就不適合傳遞過多參數,實在想用多個參數,做個約定,比如 /orders/show/a-b-c,即id值為"a-b-c",然后拆解一下,a,b,c對應不同的含義即可
POST方式,更不成問題,直接post過來一段xml或json,最終映射成model,想要多少參數都不是問題
最后給出源碼示例:struts-rest-ex-src.zip (基於官網的rest-showcase修改而來)