近期使用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,放在其它路径,这样二者就不冲突了
代码语言:javascript复制 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参考下面的内容:
代码语言:javascript复制 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,都能被拦截器拦截
代码语言:javascript复制 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 }
代码语言:javascript复制 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()方法并没有出错,拦截器会认为没有异常发生。
代码语言:javascript复制 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指定别名
代码语言:javascript复制1 @XStreamAlias("order")
2 public class Order {}
然后再创建自己的XmlHandler,为了节省系统开销,下面的代码用了一个单例:
代码语言:javascript复制 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 }
代码语言:javascript复制 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来解决
代码语言:javascript复制 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 }
代码语言:javascript复制 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来判断来源,然后做相应的分支处理
代码语言:javascript复制 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反序列化的机制,大家可以感受下这段代码:
代码语言:javascript复制 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,完全豪无关联。因此,不得不改造成
代码语言:javascript复制 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方法
代码语言:javascript复制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类里的
代码语言:javascript复制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修改而来)