zoukankan      html  css  js  c++  java
  • 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,放在其它路径,这样二者就不冲突了

     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" />
    View Code

    二、拦截器及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>
    View Code

    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 }
    View Code
     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 }
    View Code

    另外:官方的示例为了简便,在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     }
    View Code

    三、返回XML节点的别名(alias)问题

    默认情况下,返回的xml根节点为dto对应的完整package名,看上去很别扭

    解决方法:

    dto的class上,用@XStreamAlias指定别名

    1 @XStreamAlias("order")
    2 public class Order {}
    View Code

    然后再创建自己的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 }
    View Code
     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 }
    View Code

    注:别名一定要在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 }
    View Code
     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 }
    View Code

    五、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     }
    View Code

    六、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     }
    View Code

    输出结果:

    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     }
    View Code

    手动把新对象的属性,复制到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     }
    View Code

    最后一行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     }
    View Code

    整个过程,都没有新对象实例创建,所以相应的变化,能一直保持到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修改而来)

  • 相关阅读:
    指针数组、数组指针以及二维数组
    jquery的基本动画方法
    jquery面试需要看的基本东西
    bootstrap
    node全栈工程师
    setTimeout 0秒
    随便写的
    Bootstrap2和3的区别
    记忆的代码
    offsetWidth与scrollLeft
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/struts2-rest-plugin-advanced-usage.html
Copyright © 2011-2022 走看看