struts2: 玩转 rest-plugin
近期使用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,放在其它路径,这样二者就不冲突了
<!-- Overwrite Convention -->
<constant name="struts.convention.action.suffix" value="Controller" />
<constant name="struts.convention.action.mapAllMatches" value="true" />
<!--<constant name="struts.rest.content.restrictToGET" value="false" />-->
<constant name="struts.convention.default.parent.package" value="rest-default" />
<constant name="struts.convention.package.locators" value="action" />
<!-- <constant name="struts.rest.namespace" value="/rest" /> -->
<constant name="struts.convention.action.includeJars" value=".*?/_wl_cls_gen.*?jar(!/)?" />
<constant name="struts.convention.exclude.parentClassLoader" value="true" />
<constant name="struts.convention.action.fileProtocols" value="jar,zip,vfsfile,vfszip" /> <constant name="struts.mapper.class" value="org.apache.struts2.dispatcher.mapper.PrefixBasedActionMapper" />
<constant name="struts.mapper.prefixMapping" value="/rest:rest,:struts" />
<constant name="struts.mapper.alwaysSelectFullNamespace" value="false" /> <package name="default" namespace="/rest" extends="rest-default" />
二、拦截器及ModelDrive的问题
如果自定义拦截器(比如:自定义异常拦截器),默认情况下是无法拦截rest的Action
解决办法:
a) strut2.xml中定义二个package:rest-package、page-package,并在这二个package中,加上自己的拦截器,完整strut2.xml参考下面的内容:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
"http://struts.apache.org/dtds/struts-2.3.dtd"> <struts> <bean name="xmlHandler" type="org.apache.struts2.rest.handler.ContentTypeHandler"
class="com.cnblogs.yjmyzz.handler.XStreamHandler" /> <bean name="jsonHandler" type="org.apache.struts2.rest.handler.ContentTypeHandler"
class="com.cnblogs.yjmyzz.handler.JacksonHandler" /> <!-- Overwrite Convention -->
<constant name="struts.convention.action.suffix" value="Controller" />
<constant name="struts.convention.action.mapAllMatches" value="true" />
<!--<constant name="struts.rest.content.restrictToGET" value="false" />-->
<constant name="struts.convention.default.parent.package"
value="rest-default" />
<constant name="struts.convention.package.locators" value="action" />
<!-- <constant name="struts.rest.namespace" value="/rest" /> -->
<constant name="struts.convention.action.includeJars" value=".*?/_wl_cls_gen.*?jar(!/)?" />
<constant name="struts.convention.exclude.parentClassLoader"
value="true" />
<constant name="struts.convention.action.fileProtocols" value="jar,zip,vfsfile,vfszip" /> <constant name="struts.mapper.class"
value="org.apache.struts2.dispatcher.mapper.PrefixBasedActionMapper" />
<constant name="struts.mapper.prefixMapping" value="/rest:rest,:struts" />
<constant name="struts.mapper.alwaysSelectFullNamespace"
value="false" /> <package name="base-default" extends="struts-default">
<global-results>
<result name="error">/WEB-INF/common/error.jsp</result>
</global-results> <global-exception-mappings>
<exception-mapping exception="java.lang.Exception"
result="error" />
</global-exception-mappings>
</package> <package name="rest-package" namespace="/rest" extends="base-default">
<result-types>
<result-type name="redirect"
class="org.apache.struts2.dispatcher.ServletRedirectResult">
<param name="statusCode">303</param>
</result-type>
<result-type name="redirectAction"
class="org.apache.struts2.dispatcher.ServletActionRedirectResult">
<param name="statusCode">303</param>
</result-type>
</result-types>
<interceptors>
<interceptor name="rest"
class="org.apache.struts2.rest.ContentTypeInterceptor" />
<interceptor name="restWorkflow"
class="org.apache.struts2.rest.RestWorkflowInterceptor" />
<interceptor name="messages"
class="org.apache.struts2.interceptor.MessageStoreInterceptor" />
<interceptor name="exceptionInterceptor"
class="com.cnblogs.yjmyzz.interceptor.ExceptionInterceptor">
</interceptor>
<interceptor-stack name="restDefaultStack">
<interceptor-ref name="exception" />
<interceptor-ref name="alias" />
<interceptor-ref name="servletConfig" />
<interceptor-ref name="messages">
<param name="operationMode">AUTOMATIC</param>
</interceptor-ref>
<interceptor-ref name="prepare" />
<interceptor-ref name="i18n" />
<interceptor-ref name="chain" />
<interceptor-ref name="debugging" />
<interceptor-ref name="profiling" />
<interceptor-ref name="actionMappingParams" />
<interceptor-ref name="scopedModelDriven" />
<interceptor-ref name="modelDriven">
<param name="refreshModelBeforeResult">true</param>
</interceptor-ref>
<interceptor-ref name="fileUpload" />
<interceptor-ref name="checkbox" />
<interceptor-ref name="staticParams" />
<interceptor-ref name="params">
<param name="excludeParams">dojo\..*</param>
</interceptor-ref>
<interceptor-ref name="rest" />
<interceptor-ref name="conversionError" />
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse,index,show,edit,editNew,deleteConfirm,destroy,create</param>
</interceptor-ref>
<interceptor-ref name="restWorkflow">
<param name="excludeMethods">input,back,cancel,browse,index,show,edit,editNew,deleteConfirm,destroy,create</param>
</interceptor-ref>
<interceptor-ref name="exceptionInterceptor" />
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="restDefaultStack" />
<default-class-ref class="org.apache.struts2.rest.RestActionSupport" />
</package> <package name="page-package" namespace="/" extends="base-default">
<interceptors>
<interceptor name="exceptionInterceptor"
class="com.cnblogs.yjmyzz.interceptor.ExceptionInterceptor">
</interceptor>
<interceptor-stack name="appStack">
<interceptor-ref name="defaultStack">
<param name="modelDriven.refreshModelBeforeResult">true</param>
</interceptor-ref>
<interceptor-ref name="exceptionInterceptor" />
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="appStack" />
</package> </struts>
b) 所有rest Action继承自一个自定义基类,所有常规page的Action,继承自另一个自定义基类
这二个基类用@ParentPackage 指定package,分别对应struts2.xml中的配置,这样运行时,不管是rest action,还是非rest action,都能被拦截器拦截
package com.cnblogs.yjmyzz.action.base; import org.apache.struts2.convention.annotation.ParentPackage; import com.opensymphony.xwork2.ModelDriven;
import com.opensymphony.xwork2.ValidationAwareSupport; @ParentPackage("rest-package")
public abstract class RestBaseAction extends ValidationAwareSupport implements
ModelDriven<Object> { private static final long serialVersionUID = -8773131281804917145L; public abstract Object getModel(); }
 package com.cnblogs.yjmyzz.action.base;
 import org.apache.struts2.convention.annotation.ParentPackage;
 import com.opensymphony.xwork2.ActionSupport;
 @ParentPackage("page-package")
 public class PageBaseAction extends ActionSupport {
     private static final long serialVersionUID = 2323603138082550798L;
 }
另外:官方的示例为了简便,在setId方法里,直接给Model赋值了,但这有点误导,因为拦截器拦截到的方法,并不是setId(),而是show()/index()之类的方法,所以应该在show方法里,调用 model = xxx.getModel(id),否则按原来的写法,如果getModel这里报错 -> setId()报错,但show()方法并没有出错,拦截器会认为没有异常发生。
// GET /rest/orders/1
public HttpHeaders show() {
if (id != null) {
// 如果id=x,演示拦截异常处理
if (id.equals("x")) {
testException();
}
this.model = ordersService.get(id);
}
return new DefaultHttpHeaders("show");
} public void setId(String id) {
this.id = id;
}
三、返回XML节点的别名(alias)问题
默认情况下,返回的xml根节点为dto对应的完整package名,看上去很别扭
解决方法:
dto的class上,用@XStreamAlias指定别名
 @XStreamAlias("order")
 public class Order {}
然后再创建自己的XmlHandler,为了节省系统开销,下面的代码用了一个单例:
 package com.cnblogs.yjmyzz.handler;
 import com.thoughtworks.xstream.XStream;
 public class XStreamFactory {
     private XStreamFactory() {
     }
     private static XStream xStream = null;
     public static XStream getInstance() {
         if (xStream == null) {
             xStream = new XStream();
             xStream.setMode(XStream.NO_REFERENCES);
         }
         return xStream;
     }
 }
package com.cnblogs.yjmyzz.handler; import java.io.IOException;
import java.io.Reader;
import java.io.Writer; import org.apache.struts2.rest.handler.ContentTypeHandler; import com.cnblogs.yjmyzz.dto.Order;
import com.cnblogs.yjmyzz.dto.OrderList;
import com.thoughtworks.xstream.XStream; public class XStreamHandler implements ContentTypeHandler { public String fromObject(Object obj, String resultCode, Writer out)
throws IOException {
if (obj != null) {
XStream xstream = XStreamFactory.getInstance();
xstream.processAnnotations(obj.getClass());
xstream.toXML(obj, out);
}
return null;
} public void toObject(Reader in, Object target) {
XStream xstream = XStreamFactory.getInstance();
xstream.alias("data", OrderList.class);
xstream.alias("order", Order.class);
xstream.processAnnotations(target.getClass());
xstream.fromXML(in, target);
} public String getContentType() {
return "application/xml";
} public String getExtension() {
return "xml";
} }
注:别名一定要在toObject方法里,明确指定,否则别名的注解不起作用。
最后在struts2.xml里,还要注册bean,参考前面完整的xml内容。
四、返回JSON的Date属性格式化的问题
默认情况下,如果model有日期型属性,返回的json格式十分长,看上去太臃肿,类似的,可以自己定义ContentTypeHandler来解决
 package com.cnblogs.yjmyzz.handler;
 import org.codehaus.jackson.map.ObjectMapper;
 public class JacksonFactory {
     private JacksonFactory() {
     }
     private static ObjectMapper objectMapper = null;
     public static ObjectMapper getObjectMapper() {
         if (objectMapper == null) {
             objectMapper = new ObjectMapper();
         }
         return objectMapper;
     }
 }
package com.cnblogs.yjmyzz.handler; import java.io.IOException;
import java.io.Reader;
import java.io.Writer; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.rest.handler.ContentTypeHandler;
import org.springframework.beans.BeanUtils; public class JacksonHandler implements ContentTypeHandler { Logger logger = LogManager.getLogger(this.getClass()); public String fromObject(Object obj, String resultCode, Writer out)
throws IOException {
if (obj != null) {
JacksonFactory.getObjectMapper().writeValue(out, obj);
}
return null;
} public void toObject(Reader in, Object target) {
try {
Object origin = JacksonFactory.getObjectMapper().readValue(in,
target.getClass());
BeanUtils.copyProperties(origin, target); } catch (Exception e) {
e.printStackTrace();
logger.error(e);
} } public String getContentType() {
return "application/json;charset=UTF-8";
} public String getExtension() {
return "json";
} }
五、restful service 该返回哪种视图,xhtml? json? xml?
通常用rest-plugin,是为了开发rest-service,但是官网的示例返回的默认都是页面视图,这个显然不适合,最理想情况是,如果在页面上操作,操作完以后,应该返回页面视图(即: xxx.xhtml),如果是用xml参数进来的,应该返回xml视图(即: xxx.xml),如果是ajax用json post过来的,应该返回到json视图(即:xxx.json)
解决办法:根据Request的Header来判断来源,然后做相应的分支处理
// POST /orders
public HttpHeaders create() throws IOException {
ordersService.save(model);
HttpServletResponse response = ServletActionContext.getResponse();
HttpServletRequest request = ServletActionContext.getRequest();
String accept = request.getHeader("Accept");
if (accept.contains("text/html")) { // 页面视图过来的
response.sendRedirect("orders/");
} else if (accept.contains("text/xml")) { // 发送xml过来的
response.sendRedirect("orders/" + model.getId() + ".xml");
} else { // 其它的返回json视图
response.sendRedirect("orders/" + model.getId() + ".json");
}
return null;
}
六、json post到service,model取不到值的问题
这个问题最恶心,连官方默认提供的org.apache.struts2.rest.handler.JsonLibHandler都有问题,原因在json反序列化的机制,大家可以感受下这段代码:
@Test
public void testJson() {
String test = "{\"id\":\"3\",\"clientName\":\"Bob\",\"amount\":33,\"createTime\":\"1413947088717\"}";
Order order = new Order(); System.out.println(order);
System.out.println(order.hashCode()); System.out.println("----"); toObjectJson(test, order); System.out.println("----"); System.out.println(order);
System.out.println(order.hashCode());
} public void toObjectJson(String in, Object target) {
try {
target = JacksonFactory.getObjectMapper().readValue(in,
target.getClass());
System.out.println(target);
System.out.println(target.hashCode()); } catch (Exception e) {
e.printStackTrace(); }
}
输出结果:
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,完全豪无关联。因此,不得不改造成
     public void toObject(Reader in, Object target) {
         try {
             Object origin = JacksonFactory.getObjectMapper().readValue(in,
                     target.getClass());
             BeanUtils.copyProperties(origin, target);
         } catch (Exception e) {
             e.printStackTrace();
             logger.error(e);
         }
     }
手动把新对象的属性,复制到target对象上,这样就保证了反序列后的结果,在toObject执行完以后,会反映到target上。
注:可能有朋友会问了,为什么只有json会这样,xml不会呢?再仔细看下XStreamHandler的toObject方法
     public void toObject(Reader in, Object target) {
         XStream xstream = XStreamFactory.getInstance();
         xstream.alias("data", OrderList.class);
         xstream.alias("order", Order.class);
         xstream.processAnnotations(target.getClass());
         xstream.fromXML(in, target);
     }
最后一行xstream.fromXML(in, target);这是开始xml->object的入口,这里传递的就是target的地址对应的值,而不是象json那样是xxx.getClass()。如果进一步看源码,最后会发现执行的是com.thoughtworks.xstream.core.TreeUnmarshaller类里的
public TreeUnmarshaller(
Object root, HierarchicalStreamReader reader, ConverterLookup converterLookup,
Mapper mapper) {
this.root = root;
this.reader = reader;
this.converterLookup = converterLookup;
this.mapper = mapper;
}
整个过程,都没有新对象实例创建,所以相应的变化,能一直保持到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修改而来)
struts2: 玩转 rest-plugin的更多相关文章
- Struts2实例详解(转载)
		Struts2(上) 一. 经典的MVC模式 二. Struts1.x对MVC的实现 三. Struts1.x的主要组件和作用 组件 作用 ActionSer ... 
- Struts2的学习链接
		---- Struts2的学习途径 (downpour) http://www.iteye.com/wiki/struts2/1306-struts2-way-of-learning ---- Str ... 
- C#以post方式调用struts rest-plugin service的问题
		struts2: 玩转 rest-plugin一文中,学习了用struts2开发restful service的方法,发现用c#以post方式调用时各种报错,但java.ajax,包括firefox ... 
- Struts2 - Conversion Plugin
		转载:http://www.cnblogs.com/ikuman/archive/2013/11/04/3403073.html 1.struts2自2.1以后推荐使用Convention Plugi ... 
- Struts2 Convention Plugin ( struts2 零配置 )
		Struts2 Convention Plugin ( struts2 零配置 ) convention-plugin 可以用来实现 struts2 的零配置.零配置的意思并不是说没有配置,而是通过约 ... 
- 恕我直言!!!对于Maven,菜鸟玩dependency,神仙玩plugin
		打包是一项神圣.而庄严的工作.package意味着我们离生产已经非常近了.它会把我们之前的大量工作浓缩成为一个.或者多个文件.接下来,运维的同学就可以拿着这些个打包文件在生产上纵横四海了. 这么一项庄 ... 
- Service Plugin / Agent - 每天5分钟玩转 OpenStack(73)
		Core Plugin/Agent 负责管理核心实体:net, subnet 和 port.而对于更高级的网络服务,则由 Service Plugin/Agent 管理.Service Plugin ... 
- 详解 ML2 Core Plugin(II) - 每天5分钟玩转 OpenStack(72)
		上一节我们讨论了 ML2 Plugin 解决的问题,本节将继续研究 ML2 的架构. ML2 对二层网络进行抽象和建模,引入了 type driver 和 mechansim driver. 这两类 ... 
- 详解 ML2 Core Plugin(I) - 每天5分钟玩转 OpenStack(71)
		我们在 Neutron Server 小节学习到 Core Plugin,其功能是维护数据库中 network, subnet 和 port 的状态,并负责调用相应的 agent 在 network ... 
随机推荐
- php页面静态化
			如何优化页面响应时间: 动态页面静态化 优化数据库 使用负载均衡 使用缓存 如果页面中的一些内容不经常改动,可以使用动态页面静态化.好处是:减少服务器脚本的计算时间:降低服务器的响应时间. 1.动态U ... 
- 磁带机Media is unrecognized
			早晨检查磁带备份作业时,发现有个驱动的作业一直处于"Queue"状态,检查发现驱动有磁带,在Alert里面发现出现下面"Media is unrecognized&quo ... 
- jsp有哪些内置对象?作用分别是什么?分别有什么方法?
			JSP共有以下9个内置的对象: request 用户端请求,此请求会包含来自GET/POST请求的参数 response 网页传回用户端的回应 pageContext 网页的属性是在这里管理 sess ... 
- C#邮件发送问题(一)
			邮件发送需考虑很多因素,包括发送邮件客户端(一般编码实现),发送和接收邮件服务器设置等.如果使用第三方邮件服务器作为发送服务器,就需要考虑该服务器的发送限制,(如发送邮件时间间隔,单位时间内发送邮件数 ... 
- 高性能MySQL笔记 第6章 查询性能优化
			6.1 为什么查询速度会慢 查询的生命周期大致可按照顺序来看:从客户端,到服务器,然后在服务器上进行解析,生成执行计划,执行,并返回结果给客户端.其中“执行”可以认为是整个生命周期中最重要的阶段. ... 
- Javascript为元素添加事件处理函数
			document.getElementById("test").onclick = function(){ ... }; 
- java 如何在pdf中生成表格
			1.目标 在pdf中生成一个可变表头的表格,并向其中填充数据.通过泛型动态的生成表头,通过反射动态获取实体类(我这里是User)的get方法动态获得数据,从而达到动态生成表格. 每天生成一个文件夹存储 ... 
- ubuntu编译最新版本WebKit
			好久都没更新webkit 源码在ubuntu上编译了,网上搜了一下,基本上都是早期编译的webkit版本.可能是大家都去搞高大上的谷歌浏览器了吧. 今天就以ubuntu14.04版本作为编译环境来讲讲 ... 
- java报表工具FineReport的SQL编辑框的语法简介
			感谢大家捧场,这里继续分享关于SQL编辑框的一些语法心得总结,因为数据集定义的面板,也是FineReport报表中最常用的模块之一. 1.我理解的执行过程. 这里其实是生成一个字符串,FineRepo ... 
- Codeforces Round #283 Div.2 D Tennis Game --二分
			题意: 两个人比赛,给出比赛序列,如果为1,说明这场1赢,为2则2赢,假如谁先赢 t 盘谁就胜这一轮,谁先赢 s 轮则赢得整个比赛.求有多少种 t 和 s 的分配方案并输出t,s. 解法: 因为要知道 ... 
