Struts2 使用 XSLTResult 输出页面内容详解


truts2 内置提供了 xslt 结果类型,实现类为 org.apache.struts2.views.xslt.XSLTResult,它让你方便的把获得的 XML 数据内容,或者是用 OGNL 能访问到的某个属性(像 ContenxtMap、Request 等中的属性),通过一个 xslt 文件转换成你想要的格式。前面这句听来不怎么明白,后面慢慢道来。

在 Struts2 的 struts-default.xml 中定义了 chain、dispatcher、freemarker、httpheader、redirect、redirectAction、stream、 velocity、xslt 和 plainText 10 种类型的 Result;而在 Struts2 初期版本中的 jasper、chart、jsf 和 tiles 结果类型已移到相应的插件去实现了。

freemarker、velocity 和 xslt 可以很自由的使用各自的模板语言,velocity 渐渐淡出了我们的视野,那还剩下 freemarker 和 xslt。freemarker 要求实合并的变量是实体类型,满足了多数时候的需求,不过现在要说的 xslt 结果类型,向 xslt 文件送去的数据可以是实体类型,也可以是原生的 org.w3c.dom.Document 类型,当然到了 xslt 文件这一层处理的都是 org.w3c.dom.Document 类型。注意不能是别的 Document,如 org.dom4j.Document。这对于像调用 WebService 返回的是个 XML 内容然后用 xslt 文件格式化输出是最直截了当的,这就免去了我们想使用 Transformer.transform() 进行 xslt 转换 xml 的过程。

我们先作个准备,来看下 org.apache.struts2.views.xslt.XSLTResult 有哪些可配置的属性:

adapterFactory      可定制自己的,告诉 XSLTResult 怎么把暴露的变量转换为 org.w3c.dom.Document 类型
exposedValue         送出给 xslt 模板要处理的数据,可用 OGNL 引用当前上下文中的变量,也可以是个集合
stylesheetLocation xslt 文件的位置,如 /WEB-INF/xslt/foo.xslt,基于当前应用的路径,struts2.1.1 前用 location
noCache                    是否缓存 xslt 模板,可通过 struts 的常量  struts.xslt.nocache 进行全局设置
parse                          是否启用 OGNL  表达式来解析 stylesheetLocation 获得实际的 xstl 文件位置,默认为 true

另有两个在 struts2.1.1 之前的配置属性是 exludingPattern 和 matchingPattern。

有了前面的了解,现在可以看个具体的实例了:

1. Action 中执行方法的代码:

01
02
03
04
05
06
07
08
09
10
11
publicString execute(){
 
    Ticker user =newTicker();
    user.setId(100);
    user.setName("Unmi");
 
    Map<String, Object> contextMap = ActionContext.getContext().getContextMap();
    contextMap.put("user", user);
 
    returnSUCCESS;
 }

cc.unmi.model.User 类中有两属性 id 和 name

2. struts.xml 中对该 Action 的配置:

1
2
3
4
5
6
7
<actionname="user"class="cc.unmi.action.UserAction">
    <resulttype="xslt">
        <paramname="stylesheetLocation ">/xslt/user.xslt</param>
        <paramname="exposedValue">user</param>
        <paramname="noCache">true</param>
    </result>
</action>

上面的 exposedValue 是个 OGNL 表达式,这里 user 是取自 ContextMap 中的,可以是 UserAction 本身的属性,或者 Request、ServletContext 中的属性,或者是 ModelDriven  Action 的模型对象,总之是用 OGNL 能访问到的任何东西都可以在这里引用。如果每一个 exposedValue 都要申明为当前 Action 的属性就够呛的,个人觉得放 ContextMap 是个很好的选择。

比如 exposedValue 值可以用 user.name, 或用大括号包起来的多个值 {user1, user2},user1 和 user2 是当前 Action 的属性或是它的 model 中的属性是这么写。依据 OGNL 的规则,如果 user1 和 user2 不是 OGNL 上下文中根据对象的属性,则要写成 {#user1, #user2},比如它们作为 ActionContext.getContext().getContextMap() 中的值。

3. /xslt/user.xslt 文件,最简单较通用的内容

1
2
3
4
5
6
7
<?xmlversion="1.0"encoding="UTF-8"?>
<xsl:stylesheetversion="1.0"xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:outputmethod="xml"/>
    <xsl:templatematch="/result">
        <xsl:copy-ofselect="."/>
    </xsl:template>
</xsl:stylesheet>

最后,HttpServletResponse 响应的类型是由上面的 <xsl:output method="xml"/> 决定的。

4. 访问该 Action 输出的内容:

1
2
3
4
5
<?xmlversion="1.0"encoding="UTF-8"?>
<result>
    <id>100</id>
    <name>Unmi</name>
</result>

这是一个较简单的 bean 的输出,更深层次的 javabean,甚至带有 Map、List 属性的 javabean 会输出成什么样子的 xml,可以自己慢慢去试。

前面的 root 节点为什么是 <result> 呢,这要进到 org.apache.struts2.views.xslt.AdapterFactory 来寻下底。在 org.apache.struts2.views.xslt.XSLTResult 的 execute() 方法中是用:

1
2
3
4
5
6
7
Object result = invocation.getAction();
if(exposedValue !=null) {
    ValueStack stack = invocation.getStack();
    result = stack.findValue(exposedValue);
}
 
Source xmlSource = getDOMSourceForStack(result);

找到 exposedValue 值的,从 ValueStack 找,这是个 OGNL ValueStack,所以 exposedValue 就是个 OGNL 表达式。然后最后一行调用跑到当前类的:

1
2
3
4
protectedSource getDOMSourceForStack(Object value)
    throwsIllegalAccessException, InstantiationException {
    returnnewDOMSource(getAdapterFactory().adaptDocument("result", value) );
 }

这里可以看到设置了一个 "result" 字符串作为根节点的标签名。再深入一步,来到 org.apache.struts2.views.xslt.AdapterFactory 类:

1
2
3
4
5
6
7
public Document adaptDocument(String propertyName, Object propertyValue)
    throws IllegalAccessException, InstantiationException {
    //if ( propertyValue instanceof Document )
    //  return (Document)propertyValue;
 
     return new SimpleAdapterDocument(this, null, propertyName, propertyValue);
 }

方法返回的是一个 org.w3c.dom.Document 类型,再推送给 xslt 文件。发现到上面注释掉的代码,作者的本意还在,如果本身就是个 org.w3c.dom.Document 类型,那么直接给 xslt 就行,只是现在这一逻辑放到 SimpleAdapterDocument 类里去了而已。

也就是说,如果你给 exposedValue 就是一个 org.w3c.dom.Document 类型,那么会保持好原来的 XML 格式,无需再用 "result" 作为根节点名,同时在你的 xslt 文件中也不是用 <xsl:template match="/result"> 来匹配根节点,而可能是 <xsl:template match="/users">。

在调用 Service 返回 XML 数据,然后重新用 xslt 组织输出的应用场景中就好办了,不用解析原始的 XML 生成 JavaBean,再用标签显示,而是直接把原始的 XML 扔给 xslt 去处理。当然能力的发挥就要体现在 xslt 模板文件中了,xslt 中有不少函数,再不行 xslt 中可以使用 javascript 或 java 代码中的函数。

最后再看两种情况,exposedValue 是个单纯的字符串和是个用 {user1, user2} 表示的集合。

exposedValue 是个字符串,也就是在 Action 中返回前写成 contextMap.put("user", "Unmi"); xslt 文件还是那个 user.xslt,这种情况其实没必要说的,完全可以想见到它的输出就是: <result>Unmi</result>。但还是要总结一下,当 exposedValue 类型是基本类型,像 int,long 等,以及它们的包装类型,再加上字符串类型,形成的 XML 的结构将会是 <result>toString() 返回值</result>,如果是 null 将会报错,是不允许的。

之所以提到 exposedValue 是个简单字符串是为接下来作铺垫的,字符串是 "Unmi",输出为 <result>Unmi</result> 我是没意见的。但要是我的字符串是 "<user><name>Unmi</name></user>",期待它直接作为 XML 数据时,以 Struts2 现有的方式会输出为

1
2
3
4
<?xmlversion="1.0"encoding="UTF-8"?>
<result>
    &lt;user&gt;&lt;name&gt;Unmi&lt;/name&gt;&lt;/user&gt;
</result>

这就不是我想要的,它不能作为一个简单的字符串,我们希望它直接被转换为相应的 org.w3c.dom.Document,输出结果应该为 <user><name>Unmi</name></user>,这就涉及到定制自己的 AdapterFactory 在字符串符合 XML 格式时直接转换为 Document,而不是简单框上 <result>,当然事先转换为 Document 再作为 exposedValue 也行的,只是通用性不强。

exposedValue 为 {user1, user2} 时 -- 如果 user1 和 user2 是放在 ContextMap 中的话,用 {#user1, #user2} 的形式。在前面的 Action 的  execute() 方法里,往 ContextMap 放值的代码改为:

1
2
contextMap.put("user1", user);
contextMap.put("user2", user);

然后,在 struts.xml 配置文件中,exposedValue 属性值改为 {#user1, #user2},再看最后执行的页面输出:

01
02
03
04
05
06
07
08
09
10
11
<?xmlversion="1.0"encoding="UTF-8"?>
<result>
    <item>   
        <id>100</id>
        <name>Unmi</name>
    </item>
    <item>
        <id>100</id>
        <name>Unmi</name>
    </item>
</result>

要说明一下,在 XSLTResult.execute() 代码中,通过

ValueStack stack = invocation.getStack();
result = stack.findValue(“{#user1, #user2}”);

取到的是一个 ArrayList<User> 类型数据,遍历时把每一个元素用 <item> 包上。上面所有的如果不是直接提供给 xslt Document 类型,在 xml 中将会失去 javabean 本身的类型表述,而代之以 <result><item>。

再如果是个 Map 是什么样的情况,不妨看下,Action 里:

1
2
map.put("user", user);
contextMap.put("user", map);

exposedValue 还是 user

输出 XML 为:

1
2
3
4
5
6
7
8
9
<result>
    <entry>
        <key>user</key>
        <value>
            <id>100</id>
            <name>Unmi</name>
        </value>
    </entry>
</result>

<key><value> 来了,再和 List 一结合,就有些乱了,乱了,不带有类型信息的 XML 太抽象难懂,并且造成 xslt 文件的难写,因为节点名都是难以捉摸的。所以直接送 org.w3c.dom.Document 经 xslt 会高明许多,个人认为。


在上一篇: Struts2 使用 XSLTResult 输出页面内容详解 中说到了,如果在 Action 中送给 xstl 的是一个字符串,例如 String user = "<user><name>Unmi</name></user>",那么 xslt result 输出的将是:

<result>
&lt;user&gt;&lt;name&gt;Unmi&lt;/name&gt;&lt;/user&gt;
</result>

而不我们期望的

<user>
<name>Unmi</name>
</user>

那么怎么才能做到这一点呢?在 XSLTResult 有 adapterFactory 以及相应的 setter/getter 方法,但它们是 protected,所以也无法定制自己的 AdapterFactory 来判断是字符串就作为 Document 的内容。

我们再看 org.apache.struts2.views.xslt.AdapterFactory 的 API,里面说可以定义一个自己的 Adapter,然后调用 AdapterFactory 的 registerAdapterType(Class type, Class adapterType)  方法来注册,但问题是这是个实例方法,如何去调用,而且每次页页请求时,XSLTResult 的 AdapterFactory 都是不同的实例,更是无从定位。

其实 Struts2 为 XSLTResult 准备了几个 Adapter,见图:

struts2 xslt adapter 9 个,看看它们的源代码就能明白,为什么我们的 Map 会输出为 <key>..<value>,List 和数组会是一个个的 <item>。

根据什么数据类型分别应用哪一个 Adapter 是在 AdapterFactory 的 adaptNode() 方法中决定的。

先说明一下这个方法:

1. 如果在 adapterTypes 为该类型注册过了 adapter,则直接取用。问题仍然是 registerAdapterType() 怎么去调用。

2.  如果是 org.w3c.dom.Document,则取它的根据节点。Document 继承自 Node

3.  如果本身是个 org.w3c.dom.Node 的话,代理一下,也就是判断下节点类型,可能要外加一个根节点给它。

4. 类型是数组、集合、Map 则应用相应的 Adapter

5. 如果类型是 String、Number、Boolean 或是原始类型,则应用 StringAdpter

6. 余下情况则视为 Bean,应用 BeanAdapter

每种类型的 Adapter 最后是通过调用默认构造方法或 Class.newInstance() 方法实例化出来的,而且是多例的。

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
    publicNode adaptNode(AdapterNode parent, String propertyName, Object value) {
        Class adapterClass = getAdapterForValue(value);
        if(adapterClass !=null)
            returnconstructAdapterInstance(adapterClass, parent, propertyName, value);
 
        // If the property is a Document, "unwrap" it to the root element
        if(valueinstanceofDocument)
            value = ((Document) value).getDocumentElement();
 
        // If the property is already a Node, proxy it
        if(valueinstanceofNode)
            returnproxyNode(parent, (Node) value);
 
        // Check other supported types or default to generic JavaBean introspecting adapter
        Class valueType = value.getClass();
 
        if(valueType.isArray())
            adapterClass = ArrayAdapter.class;
        elseif(valueinstanceofString || valueinstanceofNumber || valueinstanceofBoolean || valueType.isPrimitive())
            adapterClass = StringAdapter.class;
        elseif(valueinstanceofCollection)
            adapterClass = CollectionAdapter.class;
        elseif(valueinstanceofMap)
            adapterClass = MapAdapter.class;
        else
            adapterClass = BeanAdapter.class;
 
        returnconstructAdapterInstance(adapterClass, parent, propertyName, value);
    }

上面很明白的告诉我们 Struts2 有一个 StringAdapter 来处理我们送给 xslt 的字符串内容,那它应该是个突破口。来见识一下 org.apache.struts2.views.xslt.StringAdapter, 关键是 buildChildAdapters() 方法:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
    protectedList<Node> buildChildAdapters() {
        Node node;
        if(getParseStringAsXML()) {
            log.debug("parsing string as xml: "+ getStringValue());
            // Parse the String to a DOM, then proxy that as our child
            node = DomHelper.parse(newInputSource(newStringReader(getStringValue())));
            node = getAdapterFactory().proxyNode(this, node);
        }else{
            log.debug("using string as is: "+ getStringValue());
            // Create a Text node as our child
            node =newSimpleTextNode(getAdapterFactory(),this,"text", getStringValue());
        }
 
        List<Node> children =newArrayList<Node>();
        children.add(node);
        returnchildren;
    }

说是它有一个 parseStringAsXML 属性(有相应的 getter/setter 方法),如果为 true 将会把你的字符串直接转换为 Document 的内容,否则作为一个文本节点。欣喜!这不正是我们想要的吗?赶紧,把这个 parseStringAsXML 属性设置为 true 问题就解决了。我也想啊,可以怎么设置啊,怎么去调用它的 setParseStringAsXML() 方法,也没办通过在 Struts2 的配置文件中去设置它。

看似能通的路又遇到险阻了,除非改写这个 StringAdapter,把它的 parseStringAsXML 默认为 true,作为 StringAdapter.class 放在 classes 目录中优先加载,想要它能在不同的 xslt Result 中可单独配置都不容易做到。

要不还是在把 String 送往 xslt 之前主动转换成 org.w3c.dom.Document 类型,用下面的代码:

1
2
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = builder.parse(newInputSource(newStringReader(YOUR_STRING));

地球都自转了一圈去找解决办法,痛苦却再次回到了原点。

再次思考下,最好的办法应该还是自定义自己的 XSLTResult 类,继承自 org.apache.struts2.views.xslt.XSLTResult,注册成新的 customXslt 结果类型。由它暴露出 parseStringAsXML 属性,在 execute() 方法中调用 AdapterFactory 的 registerAdapterType() 方法注册自己新的 CustomStringAdapter(StringAdapter 也得重写)。最后 parseStringAsXML 要经由 XSLTResult -> AdapterFactory -> CustomStringAdapter 的默认构造函数来注入了。这仍然是需要些小技巧的。

转载于:https://my.oschina.net/cronie/blog/172641

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值