前面一篇文章其实只是介绍了如何在Struts2中返回JSON数据到客户端的具体范例而无关其原理,内容与标题不符惹来标题党嫌疑确实是笔者发文 不够严谨,目前已修改标题,与内容匹配。本文将从struts2-json插件的源码角度出发,结合之前的应用范例来说明struts2-json插件返 回JSON数据的原理。
用winrar打开struts2-json-plugin-xx.jar(笔者使用版本为2.1.8.1),根目录下有一个struts-plugin.xml,这个文件想必大家都很了解,不做过多介绍了。打开该文件,内容非常简答,如下:
- <? xml version = "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 >
- < package name = "json-default" extends = "struts-default" >
- < result-types >
- < result-type name = "json" class = "org.apache.struts2.json.JSONResult" />
- </ result-types >
- < interceptors >
- < interceptor name = "json" class = "org.apache.struts2.json.JSONInterceptor" />
- </ interceptors >
- </ package >
- </ struts >
前文提到,如果要使用Struts2返回JSON数据到客户端,那么action所在的package必须继承自json-default包,原因 就在上边的配置文件中:这里的配置文件指定了该插件的包名为json-default,所以要使用该插件的功能,就必须继承自该包——json- default。
上面的配置文件中,配置了两个类:org.apache.struts2.json.JSONResult和 org.apache.struts2.json.JSONInterceptor,前者是结果类型,后者是一个拦截器。简单说一 下,org.apache.struts2.json.JSONResult负责将action中的“某些”(通过相关参数可以指定,前文已有详述)或 action中所有"可获取"(有getter方法的属性或一个有返回值的getter方法的返回值)数据序列化成JSON字符串,然后发送给客户 端;org.apache.struts2.json.JSONInterceptor负责拦截客户端到json-default包下的所有请求,并检查 客户端提交的数据是否是JSON类型,如果是则根据指定配置来反序列化JSON数据到action中的bean中(说的有点简单,其实该拦截器内部对数据 做了很多判断),拦截器不是本文的重点,介绍到此为止。看一张图,或许能够更加清晰明了的说明JSON插件执行的流程:
下面重点说说org.apache.struts2.json.JSONResult。
首先看一下org.apache.struts2.json.JSONResult源码的核心部分:
部分属性
- private String defaultEncoding = "ISO-8859-1" ;
- private List<Pattern> includeProperties;
- private List<Pattern> excludeProperties;
- private String root;
- private boolean wrapWithComments;
- private boolean prefix;
- private boolean enableGZIP = false ;
- private boolean ignoreHierarchy = true ;
- private boolean ignoreInterfaces = true ;
- private boolean enumAsBean = false ;
- private boolean excludeNullProperties = false ;
- private int statusCode;
- private int errorCode;
- private String contentType;
- private String wrapPrefix;
- private String wrapSuffix;
看一下上一篇文章中的相关参数配置:
- < package name = "json" extends = "json-default" namespace = "/test" >
- < action name = "testByAction"
- class = "cn.ysh.studio.struts2.json.demo.action.UserAction" method = "testByAction" >
- < result type = "json" >
-
-
- < param name = "root" > dataMap </ param >
-
- < param name = "excludeNullProperties" > true </ param >
-
- < param name = "includeProperties" >
- user.*
- </ param >
-
- < param name = "contentType" > text/html </ param >
-
- < param name = "excludeProperties" >
- SUCCESS
- </ param >
- </ result >
- </ action >
- </ package >
配置中出现了JSONResult的部分属性名,是的,JSONResult中的属性都可以根据需要在struts.xml中配置对应参数以改变默认值来满足我们的需要。
接下来看看它的两个核心方法:
- public void execute(ActionInvocation invocation) throws Exception {
- ActionContext actionContext = invocation.getInvocationContext();
- HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST);
- HttpServletResponse response = (HttpServletResponse) actionContext.get(StrutsStatics.HTTP_RESPONSE);
-
- try {
- String json;
- Object rootObject;
-
- if ( this .enableSMD) {
-
- rootObject = this .writeSMD(invocation);
- } else {
-
- if ( this .root != null ) {
- ValueStack stack = invocation.getStack();
- rootObject = stack.findValue(this .root);
- } else {
- rootObject = invocation.getAction();
- }
- }
-
- json = JSONUtil.serialize(rootObject, excludeProperties, includeProperties, ignoreHierarchy,
- enumAsBean, excludeNullProperties);
-
- json = addCallbackIfApplicable(request, json);
-
- boolean writeGzip = enableGZIP && JSONUtil.isGzipInRequest(request);
-
-
- writeToResponse(response, json, writeGzip);
-
- } catch (IOException exception) {
- LOG.error(exception.getMessage(), exception);
- throw exception;
- }
- }
-
-
-
-
-
-
-
-
- protected void writeToResponse(HttpServletResponse response, String json, boolean gzip)
- throws IOException {
- JSONUtil.writeJSONToResponse(new SerializationParams(response, getEncoding(), isWrapWithComments(),
- json, false , gzip, noCache, statusCode, errorCode, prefix, contentType, wrapPrefix,
- wrapSuffix));
- }
恕笔者愚钝,找了好多资料,始终不明白这里的"SMD"是个什么意思,所在这里包括下文,都将忽略"SMD"。
可以看到,Struts2序列化对象为JSON字符串的整个过程都被JSONUtil的serialize方法包办了,所以有必要跟入这个方法一探究竟:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- public static String serialize(Object object, Collection<Pattern> excludeProperties,
- Collection<Pattern> includeProperties, boolean ignoreHierarchy, boolean enumAsBean,
- boolean excludeNullProperties) throws JSONException {
- JSONWriter writer = new JSONWriter();
- writer.setIgnoreHierarchy(ignoreHierarchy);
- writer.setEnumAsBean(enumAsBean);
- return writer.write(object, excludeProperties, includeProperties, excludeNullProperties);
- }
该方法还有一个重载的兄弟方法,只是少了boolean enumAsBean这个参数,我们并不关心它,这里不讨论它。可以看到,这个方法更简单:构建一个JSONWriter实例,注入两个参数,然后调用该 实例的write方法。我们进入JSONWriter,查看write方法的源码:
-
-
-
-
-
-
- public String write(Object object, Collection<Pattern> excludeProperties,
- Collection<Pattern> includeProperties, boolean excludeNullProperties) throws JSONException {
- this .excludeNullProperties = excludeNullProperties;
- this .buf.setLength( 0 );
- this .root = object;
- this .exprStack = "" ;
- this .buildExpr = ((excludeProperties != null ) && !excludeProperties.isEmpty())
- || ((includeProperties != null ) && !includeProperties.isEmpty());
- this .excludeProperties = excludeProperties;
- this .includeProperties = includeProperties;
- this .value(object, null );
-
- return this .buf.toString();
- }
它同样有一个重载的方法,我们同样不关心,浏览整个方法,不难发现,它只是所做了一些赋值操作,然后将对象的序列化工作交给了value成员方法,那么我们进入value方法看一看:
-
-
-
- private void value(Object object, Method method) throws JSONException {
- if (object == null ) {
- this .add( "null" );
-
- return ;
- }
-
- if ( this .stack.contains(object)) {
- Class clazz = object.getClass();
-
-
- if (clazz.isPrimitive() || clazz.equals(String. class )) {
- this .process(object, method);
- } else {
- if (LOG.isDebugEnabled()) {
- LOG.debug("Cyclic reference detected on " + object);
- }
-
- this .add( "null" );
- }
-
- return ;
- }
-
- this .process(object, method);
- }
很简洁,进入process方法
-
-
-
- private void process(Object object, Method method) throws JSONException {
- this .stack.push(object);
-
- if (object instanceof Class) {
- this .string(object);
- } else if (object instanceof Boolean) {
- this .bool(((Boolean) object).booleanValue());
- } else if (object instanceof Number) {
- this .add(object);
- } else if (object instanceof String) {
- this .string(object);
- } else if (object instanceof Character) {
- this .string(object);
- } else if (object instanceof Map) {
- this .map((Map) object, method);
- } else if (object.getClass().isArray()) {
- this .array(object, method);
- } else if (object instanceof Iterable) {
- this .array(((Iterable) object).iterator(), method);
- } else if (object instanceof Date) {
- this .date((Date) object, method);
- } else if (object instanceof Calendar) {
- this .date(((Calendar) object).getTime(), method);
- } else if (object instanceof Locale) {
- this .string(object);
- } else if (object instanceof Enum) {
- this .enumeration((Enum) object);
- } else {
- this .bean(object);
- }
-
- this .stack.pop();
- }
发现它做了很多判断,并结合不同的方法来支持不同的数据类型,那么从这里我们可以知道Struts-json-plugin支持哪些数据类型了。对 于每一种支持的数据类型,Struts-json-plugin都有相应的方法来从从对象中抽取数据并封装成JSON字符串,以Map为例,我们看一下 map方法的源码:
-
-
-
- private void map(Map map, Method method) throws JSONException {
-
- his.add("{" );
-
- Iterator it = map.entrySet().iterator();
-
- boolean warnedNonString = false ;
- boolean hasData = false ;
- while (it.hasNext()) {
- Map.Entry entry = (Map.Entry) it.next();
-
- Object key = entry.getKey();
-
- String expr = null ;
- if ( this .buildExpr) {
- if (key == null ) {
- LOG.error("Cannot build expression for null key in " + this .exprStack);
- continue ;
- } else {
-
- expr = this .expandExpr(key.toString());
-
-
- if ( this .shouldExcludeProperty(expr)) {
- continue ;
- }
-
-
- expr = this .setExprStack(expr);
- }
- }
-
- if (hasData) {
- this .add( ',' );
- }
- hasData = true ;
-
- if (!warnedNonString && !(key instanceof String)) {
- LOG.warn("JavaScript doesn't support non-String keys, using toString() on "
- + key.getClass().getName());
- warnedNonString = true ;
- }
- this .value(key.toString(), method);
- this .add( ":" );
-
- this .value(entry.getValue(), method);
-
- if ( this .buildExpr) {
- this .setExprStack(expr);
- }
- }
-
- this .add( "}" );
- }
这个方法中比较重要的几行代码都做了注释,不再赘述。过滤某些属性,以使其不被序列化时struts2-JSON应用中非常常见的,比如在序列化一 个用户对象的时候,密码信息时不应该被传送到客户端的,所以要排除掉。了解shouldExcludeProperty方法的过滤规则,可以帮助我们更好 的使用此功能。源码如下:
- private boolean shouldExcludeProperty(String expr) {
- if ( this .excludeProperties != null ) {
- for (Pattern pattern : this .excludeProperties) {
- if (pattern.matcher(expr).matches()) {
- if (LOG.isDebugEnabled())
- LOG.debug("Ignoring property because of exclude rule: " + expr);
- return true ;
- }
- }
- }
-
- if ( this .includeProperties != null ) {
- for (Pattern pattern : this .includeProperties) {
- if (pattern.matcher(expr).matches()) {
- return false ;
- }
- }
-
- if (LOG.isDebugEnabled())
- LOG.debug("Ignoring property because of include rule: " + expr);
- return true ;
- }
-
- return false ;
- }
非常简单,就是简单的正则匹配,如果有排除配置,则先判断当前属性是否被排除,如果没有被排除,且有包含配置则检查是否被包含,如果没有被包含,则不序列化该属性,如果没有被排除且没有包含配置,则将序列化该属性。
源码跟踪到这里,已经没有继续下去的必要了,因为我们已经很清楚Struts2是如何将一个对象转换成JSON字符串并返回客户端的:
1、收集用户配置;
2、JSONWriter通过判断对象的类型来有针对性的抽取其中的属性值,对于嵌套的对象则采用递归的方式来抽取,抽取的同时,包装成符合JSON语法规范的字符串;
3、JSONUtil.writeJSONToResponse将序列化的JSON字符串按照相关配置发送到客户端;
不难看出,代码逻辑清晰,简单,朴素,没有半点花巧和卖弄,但确实是非常的精巧,表现出作者扎实的编程功底和过人的逻辑思维能力。尤其是递归抽取嵌套对象的属性值和获取当前属性的OGNL表达式的算法,堪称经典!
通过以上的源码跟踪,我们很清楚的了解Struts2序列化对象的原理和过程,并对相关参数的配置有了深刻的体会。只是令人感到奇怪的是,他并没有 使用json-lib.xx.jar中的API接口,而是以字符串拼接的方式手动构建JSON字符串,我想原因可能是因为它要用正则表达式包含或排除某些 属性的原因吧,仅作猜测,还望高人指点。
有很多人说不知道includeProperties和excludeProperties的正则表达式该怎么配置,我想说其实很简单,除了正则知 识外,就是"对象名.属性名",数组稍微不同,以为它有下标,所以是"数组对象名\[\d+\]\.属性名"。如果这里觉得说的不清楚,可以阅读以下 JSONWriter中关于OGNL表达式是如何获取的部分代码,就会明白正则该如何写了。
纯属个人理解,如有错误,烦请指正,不胜荣幸!
原创文章,转载请注明出处:http://yshjava.iteye.com/blog/1333602