首页 > 代码库 > IT忍者神龟之Struts2-Json-Plugin 的使用(翻译自官方文档)

IT忍者神龟之Struts2-Json-Plugin 的使用(翻译自官方文档)

在 Struts2 中要使用 Ajax 获得 Json 数据我认为目前还是 struts2-json-plugin 了。当然你你可以用手工用像 XStream、Google Gson、Jackson 这样的工具手工把 Java 对象转换成 Json 字符串再写往 Response 去,要写的代码自然多不了,还得留心字符集与 content type。而 struts2-json-plugin 毫无疑问是与 Struts2 最亲近了,只需你配置一些属性就能得到你想的结果。

本想分几篇逐步介绍如何使用 struts2-json-plugin 的,然而就在现在发现官方的 struts2-json-plugin 指南已经很详细了,所以干脆翻译一下http://struts.apache.org/2.2.1.1/docs/json-plugin.html,同时自己加深对它的理解。

JSON 插件提供了一个 "json" 结果类型来把 action 序列化成 JSON. 这一序列化的过程是递归的, 意即整个对象图,从 action 类开始 (未包括基类) 将会被序列化 (可以用 "root" 属性来指定自己的根对象). 如果使用了 json 拦截器, action 将可通过请求中的 JSON 内容组装出来, 该拦截器遵循以下几条规则:

  1. "content-type" 必须为 "application/json"
  2. JSON 内容必须是格式良好的, 参考 json.org 中的语法.
  3. Action 里必须有欲获取值的属性的相应 public 的 "setter" 方法.
  4. 所支持的类型有: 原始类型 (int,long...String), Date, List, Map, 原始类型数组, 其他的类 (将会支持更多), 和其他类型的数组.
  5. JSON 中的任何将要被填入到 list 或 map 中的对象会是 Map 类型(属性映射到值), 任何整数都是 Long 类型, 任何小数会是 Double 类型, 任何数组会是 List 类型.

给定下面的 JSON 字符串:

01
02
03
04
05
06
07
08
09
10
{
   "doubleValue": 10.10,
   "nestedBean": {
      "name": "Mr Bean"
   },
   "list": ["A", 10, 20.20, {
      "firstName": "El Zorro"
   }],
   "array": [10, 20]
}
 action 中必须有一个 "setDoubleValue" 方法, 参数为 "float" 或者 "double"(拦截器将会把值转换为相应的类型). 还必须有一个 "setNestedBean" 方法,它的参数类型可以为任何类类型, 其中含有参数为 "String" 的 "setName" 方法. 还必须有一个参数为 "List" 的 "setList" 方法, 这个 List 中将会包含: "A" (String), 10 (Long), 20.20 (Double), Map ("firstName" -> "El Zorro"). "setArray" 方法可以是 "List", 或任何数字类型数组作参数的.
序列化你的对象成 javascript 的 JSON, 参考 json2

安装

本插件可通过把插件  jar 包到你的应用的 /WEB-INF/lib 目录来完成安装. 没有别的文件需要拷贝或被创建.

使用 maven 的话, 加入下列到你的 pom 中:

1
2
3
4
5
6
7
8
9
<dependencies>
   ...
   <dependency>
       <groupId>org.apache.struts</groupId>
       <artifactId>struts2-json-plugin</artifactId>
       <version>STRUTS_VERSION</version>
   </dependency>
   ...
</dependencies>
 

定制化序列化和反序列化

使用 JSON 注解来达到定制序列化和反序列化过程. 可用的 JSON 注解如下:

名称描述默认值序列化反序列化
name定制字段名emptyyesno
serialize标识为可被序列化trueyesno
deserialize标识为可被反序列化truenoyes
format用于格式化或解析 Date 字段的格式"yyyy-MM-dd‘T‘HH:mm:ss"yesyes

排除属性

逗号分隔的正则表达式列表可传递给 JSON Result 和 Interceptor(拦截器), 被任何 一个正则表达式匹配的属性将会在序列化过程时忽略掉:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<!-- Result fragment -->
<resulttype="json">
  <paramname="excludeProperties">
    login.password,
    studentList.*\.sin
  </param>
</result>
 
<!-- Interceptor fragment -->
<interceptor-refname="json">
  <paramname="enableSMD">true</param>
  <paramname="excludeProperties">
    login.password,
    studentList.*\.sin
  </param>
</interceptor-ref>
包含属性

逗号分隔的正则表达式列表可被传递给 JSON Result, 用于限制哪些属性可用于序列化. 只有当能够匹配任何一个正则表达式的属性才会包含在序列化输出中.

注:
排除属性表达式优先于包含属性的表达式.  那就是说, 如果包含和排除表达式应用于同一个结果, 包含表达式对于被排除表达式匹配到的属性是不起作用的.
 
1
2
3
4
5
6
7
8
<!-- Result fragment -->
<resulttype="json">
  <paramname="includeProperties">
    ^entries\[\d+\]\.clientNumber,
    ^entries\[\d+\]\.scheduleNumber,
    ^entries\[\d+\]\.createUserId
  </param>
</result>
根对象

使用 "root" 属性(OGNL 表达式) 指定被用于序列化的根对象.

1
2
3
4
5
<resulttype="json">
  <paramname="root">
    person.job
  </param>
</result>
"root" 属性(OGNL 表达式) 也可以用于拦截器来指定被组装的对象, 确保这个对象不会是 null.
1
2
3
<interceptor-refname="json">
  <paramname="root">bean1.bean2</param>
</interceptor-ref>
包装

可能会有某些原因,你想要用些文本对 JSON 输出包装一下, 像用注释包裹, 加上前缀, 或使用文件上载让结果显示在 textarea 之中. 用 wrapPrefix 在开始处加上内容,wrapPostfix 添加内容在尾端. 这两个参数优先使用,而  "wrapWithComments" 和 "prefix" 自从 0.34 后就不推荐使用. 例子:

进行注释:

1
2
3
4
<resulttype="json">
  <paramname="wrapPrefix">/*</param>
  <paramname="wrapSuffix">*/</param>
</result>

添加前缀:

1
2
3
<resulttype="json">
  <paramname="wrapPrefix">{}&&</param>
</result>
 包裹上传的文件内容:
1
2
3
4
<resulttype="json">
  <paramname="wrapPrefix"><![CDATA[<html><body><textarea>]]></param>
  <paramname="wrapSuffix"><![CDATA[</textarea></body></html>]]></param>
</result>

包裹以注释

wrapWithComments 自 0.34 不推荐使用, 建议用 wrapPrefix 和 wrapSuffix.
wrapWithComments 可使得安全的 JSON 文本变得不安全. 例如,

["*/ alert(‘XSS‘); /*"]

谢谢 Douglas Crockford 的提示! 应考虑用 prefix.

假如被序列化的 JSON 是 {name: ‘El Zorro‘}. 那么此时输出就会是: {}&& ({name: ‘El Zorro‘}

假如 "wrapWithComments" (默认为 false) 属性被设为 true, 生成的被包裹上注释的 JSON 就如下:

01
02
03
04
05
06
07
08
09
10
/* {
   "doubleVal": 10.10,
   "nestedBean": {
      "name": "Mr Bean"
   },
   "list": ["A", 10, 20.20, {
      "firstName": "El Zorro"
   }],
   "array": [10, 20]
} */
 欲取消上面的注释,可用:
var responseObject = eval("("+data.substring(data.indexOf("\/\*")+2, data.lastIndexOf("\*\/"))+")");

前缀

prefix 从 0.34 后不建议用, 请用 wrapPrefix 和 wrapSuffix.

假如参数 prefix 被设置为 true, 生成的 JSON 将被附上前缀 "{}&& ". 这有助于防止被劫持. 详细内容请看 this Dojo Ticket:

1
2
3
<resulttype="json">
  <paramname="prefix">true</param>
</result>
基类

默认时,定义在 "root" 对象的基类中的属性不会被序列化, 要序列化来自于所有基类(直到 Object) 中的属性,需在 JSON result 里设置 "ignoreHierarchy" 为 false:

1
2
3
<resulttype="json">
  <paramname="ignoreHierarchy">false</param>
</result>

枚举类型

默认的, Enum 被序列化为 name=value 对,这里的 value = http://www.mamicode.com/name().

1
2
3
4
  public enum AnEnum {
     ValueA,
     ValueB
  }

 JSON: "myEnum":"ValueA"

使用 result 的参数 "enumAsBean" 可使得 Enum 像一个 bean 一样的被序列化,特定的属性为 _name,值为 name().  所有的枚举属性都会被序列化. 

01
02
03
04
05
06
07
08
09
10
  publicenum AnEnum {
     ValueA("A"),
     ValueB("B");
 
     privateString val;
 
     publicAnEnum(val) { this.val = val; } publicgetVal() {
        returnval;
     }
   }
 JSON: myEnum: { "_name": "ValueA", "val": "A" }

在 struts.xml 中启用该参数:

1
2
3
<resulttype="json">
  <paramname="enumAsBean">true</param>
</result>

压缩输出.

设置 enableGZIP 属性为 true 可用 gzip 压缩响应输出. 在请求后 "Accept-Encoding" 头中必须包含  "gzip" 才能正常工作.

1
2
3
<resulttype="json">
  <paramname="enableGZIP">true</param>
</result>

防止浏览器缓存响应数据

noCache 设置为 true(默认为 false) 会设置如下响应头:

  • Cache-Control: no-cache
  • Expires: 0
  • Pragma: No-cache
1
2
3
<resulttype="json">
  <paramname="noCache">true</param>
</result>

排除值为 null 的属性

默认的,为 null 的字段也被序列化,生成像 {property_name: null}. 这能够通过设置 excludeNullProperties 为 true 来防止.

1
2
3
<resulttype="json">
  <paramname="excludeNullProperties">true</param>
</result>

状态和错误代码

使用 statusCode 来设置响应状态代码:

1
2
3
<resulttype="json">
  <paramname="statusCode">304</param>
</result>

同时可用 errorCode 来发送一个错误(the server might end up sending something to the client which is not the serialized JSON):

1
2
3
<resulttype="json">
  <paramname="errorCode">404</param>
</result>

JSONP

To enable JSONP, set the parameter callbackParameter in either the JSON Result or the Interceptor. A parameter with that name will be read from the request, and it value will be used as the JSONP function. Assuming that a request is made with the parameter "callback"="exec":

1
2
3
<resulttype="json">
  <paramname="callbackParameter">callback</param>
</result>

And that the serialized JSON is {name: ‘El Zorro‘}. Then the output will be: exec({name: ‘El Zorro‘})

Content Type

Content type will be set to application/json-rpc by default if SMD is being used, or application/json otherwise. Sometimes it is necessary to set the content type to something else, like when uploading files with Dojo and YUI. Use the contentType parameter in those cases.

1
2
3
<resulttype="json">
  <paramname="contentType">text/html</param>
</result>

Example

Setup Action

This simple action has some fields:

Example:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
importjava.util.HashMap;
importjava.util.Map;
 
importcom.opensymphony.xwork2.Action;
 
publicclass JSONExample {
    privateString field1 = "str";
    privateint[] ints = {10,20};
    privateMap map = newHashMap();
    privateString customName = "custom";
 
    //‘transient‘ fields are not serialized
    privatetransient String field2;
 
    //fields without getter method are not serialized
    privateString field3;
 
    publicString execute() {
        map.put("John","Galt");
        returnAction.SUCCESS;
    }
 
    publicString getField1() {
        returnfield1;
    }
 
    publicvoid setField1(String field1) {
        this.field1 = field1;
    }
 
    publicint[] getInts() {
        returnints;
    }
 
    publicvoid setInts(int[] ints) {
        this.ints = ints;
    }
 
    publicMap getMap() {
        returnmap;
    }
 
    publicvoid setMap(Map map) {
        this.map = map;
    }
 
    @JSON(name="newName")
    publicString getCustomName() {
        returnthis.customName;
    }
}

Write the mapping for the action

  1. Add the map inside a package that extends "json-default"
  2. Add a result of type "json"

Example:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<?xmlversion="1.0"encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd">
 
<struts>
 
  <packagename="example" extends="json-default">
     <actionname="JSONExample">
        <resulttype="json"/>
     </action>
  </package>
 
</struts>

JSON example output

1
2
3
4
5
6
7
8
   "field1": "str",
   "ints": [10, 20],
   "map": {
       "John":"Galt"
   },
   "newName":"custom"
}

JSON RPC

The json plugin can be used to execute action methods from javascript and return the output. This feature was developed with Dojo in mind, so it uses Simple Method Definition to advertise the remote service. Let‘s work it out with an example(useless as most examples).

First write the action:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
packagesmd;
 
importcom.googlecode.jsonplugin.annotations.SMDMethod;
importcom.opensymphony.xwork2.Action;
 
publicclass SMDAction {
    publicString smd() {
        returnAction.SUCCESS;
    }
 
    @SMDMethod
    publicBean doSomething(Bean bean, intquantity) {
        bean.setPrice(quantity * 10);
        returnbean;
    }
}

Methods that will be called remotely must be annotated with the SMDMethod annotation, for security reasons. The method will take a bean object, modify its price and return it. The action can be annotated with the SMD annotation to customize the generated SMD (more on that soon), and parameters can be annotated withSMDMethodParameter. As you can see, we have a "dummy", smd method. This method will be used to generate the Simple Method Definition (a definition of all the services provided by this class), using the "json" result.

The bean class:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
packagesmd;
 
publicclass Bean {
    privateString type;
    privateint price;
 
    publicString getType() {
        returntype;
    }
 
    publicvoid setType(String type) {
        this.type = type;
    }
 
    publicint getPrice() {
        returnprice;
    }
 
    publicvoid setPrice(intprice) {
        this.price = price;
    }
 
}

The mapping:

01
02
03
04
05
06
07
08
09
10
<packagename="RPC"namespace="/nodecorate"extends="json-default">
    <actionname="SMDAction"method="smd">
        <interceptor-refname="json">
            <paramname="enableSMD">true</param>
        </interceptor-ref>
        <resulttype="json">
             <paramname="enableSMD">true</param>
        </result>
    </action>
</package>

Nothing special here, except that both the interceptor and the result must be applied to the action, and "enableSMD" must be enabled for both.

Now the javascript code:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
<s:url id="smdUrl"namespace="/nodecorate"action="SMDAction"/>
<script type="text/javascript">
    //load dojo RPC
    dojo.require("dojo.rpc.*");
 
    //create service object(proxy) using SMD (generated by the json result)
    varservice = newdojo.rpc.JsonService("${smdUrl}");
 
    //function called when remote method returns
    varcallback = function(bean) {
        alert("Price for " + bean.type + " is " + bean.price);
    };
 
    //parameter
    varbean = {type: "Mocca"};
 
    //execute remote method
    vardefered = service.doSomething(bean, 5);
 
    //attach callback to defered object
    defered.addCallback(callback);
</script>

Dojo‘s JsonService will make a request to the action to load the SMD, which will return a JSON object with the definition of the available remote methods, using that information Dojo creates a "proxy" for those methods. Because of the asynchronous nature of the request, when the method is executed, a deferred object is returned, to which a callback function can be attached. The callback function will receive as a parameter the object returned from your action. That‘s it.

Proxied objects

As annotations are not inherited in Java, some user might experience problems while trying to serialize objects that are proxied. eg. when you have attached AOP interceptors to your action.

In this situation, the plugin will not detect the annotations on methods in your action.

To overcome this, set the "ignoreInterfaces" result parameter to false (true by default) to request that the plugin inspects all interfaces and superclasses of the action for annotations on the action‘s methods.

NOTE: This parameter should only be set to false if your action could be a proxy as there is a performance cost caused by recursion through the interfaces.

01
02
03
04
05
06
07
08
09
10
11
<actionname="contact"method="smd">
   <interceptor-refname="json">
      <paramname="enableSMD">true</param>
      <paramname="ignoreSMDMethodInterfaces">false</param>
   </interceptor-ref>
   <resulttype="json">
      <paramname="enableSMD">true</param>
      <paramname="ignoreInterfaces">false</param>
   </result>
   <interceptor-refname="default"/>
</action>

IT忍者神龟之Struts2-Json-Plugin 的使用(翻译自官方文档)