1. 程式人生 > >Struts2萬用字元和它的各種坑

Struts2萬用字元和它的各種坑

Struts2和Servlet相比有幾個牛逼的地方。對OGNL表示式的整合以及萬用字元的運用就是其中兩個。

而J2EE標準中,與這兩個對應的分別是JSP中EL表示式的運用,以及urlPattern中的萬用字元。

前面的一篇文章中,講述了OGNL的使用(OGNL在功能上把EL秒成渣(~ ̄▽ ̄)~ )。

這篇文章就來說說Struts2中的萬用字元以及它的各種坑。

最基本的使用

1. *萬用字元

和Servlet標準中urlPattern萬用字元功能一樣,就是一個控制器類處理多個url的請求:通過合併一些相似的url對映,減少action對映的數量。

<action name
="/edit*" class="struts.webapp.example.Edit{1}Action">
<result name="failure">/mainMenu.jsp</result> <result>{1}.jsp</result> </action>

上面name屬性中的*萬用字元就是允許匹配任意以/edit開頭的url,比如/editSubscription, /editRegistration但是需要注意的是/editSubscription/add這種url是無法匹配的

在<action>標籤的其他屬性中,甚至子標籤<result>中以及result的子標籤<param>都可以使用{n}

這種方式去替換url對映中由萬用字元表示的部分,其中n的範圍是0到9,特殊地,當n=0時,表示完整的請求路徑。

比如/edit*匹配/editRegistration請求,那麼{1}就是Registration{0}就是/editRegistration(這方面和正則表示式類似)。

但是注意下面這個坑

<action name="List*s" class="actions.List{1}s">
  <result>list{1}s.jsp</result>
</action>

當url為ListAccounts,上面的配置會正常執行。當url為ListSponsors

時,由於ListSponsors中間也出現了s,所以最終會得到下面的匹配結果:

<action name="ListSpons" class="actions.ListSpons">
  <result>listSpons.jsp</result>
</action>

所以*號最好不要出現在中間(估計也沒人會這麼搞)。

2. ** 萬用字元

前面說到/edit*萬用字元無法匹配/editSubscription/add這樣的url。如果確實需要匹配這種url可以使用兩個*號, 也就是/edit**

官方文件中給出了一個萬用字元中特殊符號的列表:

特殊符號 意義
* 匹配除了斜槓(’/’)字元之外的零個或多個字元。
** 匹配零個或多個字元,包括斜槓(’/’)字元。
\字元 反斜線字元用作轉義序列。
所以\*匹配字元星號(*),\\匹配字元反斜槓(\)。

注意:如果需要讓Action的name屬性包含/,需要配置一個常量:

<constant name="struts.enable.SlashesInActionNames" value="true"/>

雖然能夠讓Action的name包含/,但Struts2官方並不推薦使用這種配置(因為有副作用),可以參看Struts2官網對Action name包含/的討論:https://issues.apache.org/jira/browse/WW-1383

另外如果需要對Action name中能出現的字元進行限制,可以配置如下變數:

`<constant name = "struts.allowed.action.names" value = "[a-z{}]" */>

上面這中使用*?的萬用字元模式有很多名字,有人把它叫做glob匹配模式,也有人把它叫做Ant-style

名稱空間的配置

雖然不推薦Action的name屬性包含/,但是對於使用/進行模組劃分有更好的解決方案——package的namespace屬性。

Struts2中,package標籤的namespace屬性是用來細分專案模組的,比如:

<!-- 沒有配置namespace,則為預設名稱空間 -->
<package name="default">
    <action name="foo" class="mypackage.simpleAction">
        <result name="success" type="dispatcher">greeting.jsp</result>
    </action>

    <action name="bar" class="mypackage.simpleAction">
        <result name="success" type="dispatcher">bar1.jsp</result>
    </action>
</package>
<!-- 配置namespace為"/",則為根名稱空間 -->
<package name="mypackage1" namespace="/">
    <action name="moo" class="mypackage.simpleAction">
        <result name="success" type="dispatcher">moo.jsp</result>
    </action>
</package>

<!-- 配置namespace為"/barspace",是精確名稱空間 -->
<package name="mypackage2" namespace="/barspace">
    <action name="bar" class="mypackage.simpleAction">
        <result name="success" type="dispatcher">bar2.jsp</result>
    </action>
</package>

名稱空間的匹配優先順序為:精確名稱空間 > 根名稱空間 > 預設名稱空間

名稱空間中包含請求引數

從Struts2.1開始,框架可以從名稱空間中提取請求引數,要使用該功能需要先配置一個常量:

<constant name="struts.patternMatcher" value="namedVariable"/>

定義名稱空間時可以包含{PARAM_NAME}這樣的模式串,從url中提取一些請求引數,比如:

@Namespace{"/users/{userID}");
public class DetailsAction exends ActionSupport {
  private Long userID;
  public void setUserID(Long userID) {...}
}

如果請求url為/users/10/detail,Struts2框架會自動從url中提取出10作為DetailsAction.userID欄位的值。

Action Name中包含請求引數

除了上面的名稱空間可以從url提取請求引數,Action也可以從url中提取請求引數,使用該功能需要配置兩個常量:

<constant name="struts.enable.SlashesInActionNames" value="true"/>
<constant name="struts.mapper.alwaysSelectFullNamespace" value="false"/>

然後配置Action對映:

<package name="edit" extends="struts-default" namespace="/edit">
    <action name="/person/*" class="org.apache.struts.webapp.example.EditAction">
        <param name="id">{1}</param>
        <result>/mainMenu.jsp</result>
    </action>
</package>

當請求url為/edit/person/123時,EditActionid欄位將會被設定為123

名稱空間和Action Name中攜帶引數,這兩個運用的場合還是比較多,比如各大部落格平臺的url就是這麼設計的,CSDN的部落格url:blog.csdn.net/{USER_NAME}/article/details/{ARTICLE_ID}。在Struts2中使用這種攜帶引數的url可能還會躡手躡腳(確實不怎麼好用),但是在另一個更牛逼的MVC框架——SpringMVC,它把這種url中攜帶引數的方式發揚光大,並且還衍生出RESTful架構模式(聽起來很牛逼的樣子,其實本質上就是在url中攜帶引數,同時與Http協議的POST、GET、PUT 和 DELETE請求方式進行結合)。

更牛逼的萬用字元—-正則表示式

從Struts2.1.9開始可以在Action的name屬性中定義正則表示式,這就大大的加強了Struts2的匹配能力,因為正則獨立於框架獨立於程式語言的,如果以前瞭解過正則,在Struts2的萬用字元對映的配置上基本不需要花什麼學習成本。

使用前的配置

要使用正則作為萬用字元,需要配置三個常量:

<constant name="struts.enable.SlashesInActionNames" value="true"/>
<constant name="struts.mapper.alwaysSelectFullNamespace" value="false"/>
<constant name="struts.patternMatcher" value="regex" />

第一種形式:{FIELD_NAME}

這是最簡單的一種匹配形式:url中的{FIELD_NAME}部分將會作為Action的欄位。比如:

<package name="books" extends="struts-default" namespace="/">
    <action name="/{type}/content/{title}" class="example.BookAction">
    <result>/books/content.jsp</result>
    </action>
</package>

當請求的url為/fiction/content/Frankenstein時,BookActiontype欄位會被設定為fictiontitle欄位會被設定為Frankenstein

第二種形式:{FIELD_NAME: REGEX}

看到上面的例子,也許你會說這和正則有個毛關係。

別急,這第二種形式才開始和正則有關。

這種形式語法為:{FIELD_NAME:REGEX},FIELD_NAME和上面第一種形式一樣,作為Action的欄位名;後面的REGEX,用來對url進行限制,只有滿足該正則的url才能匹配該Action。比如:

<package name="books" extends="struts-default" namespace="/">
    <action name="/{type}/{author:.+}/list" class="example.ListBooksAction">
    <result>/books/list.jsp</result>
    </action>
</package>

上面的.+就是用來對url進行限制的正則。

有關正則表示式的使用,可以參考這裡

比如對於/philosophy/AynRand/list請求路徑,ListBooksAction的type欄位會被設定為philosophyauthor欄位會被設定為AynRand

在<action>標籤的其他屬性中,以及它的子標籤中仍然可以使用{n}的符號獲取匹配組,比如:

<package name="books" extends="struts-default" namespace="/">
    <action name="/books/{ISBN}/content" class="example.BookAction">
    <result>/books/{1}.jsp</result>
    </action>
</package>

在action的method屬性中使用萬用字元

前面說到使用{n}可以獲取萬用字元的匹配組,這個匹配組可以用在<action>標籤屬性或子標籤下等多個地方,而放在<action>的method屬性下最能體現Struts2的靈活性。因為method屬性是根據請求url變化而動態呼叫Action類下的方法的,官方稱之為萬用字元方法(Wildcard Method)

1. 萬用字元方法

<action name="user_*" class="cn.hff.struts.UserAction" method="{1}">
    <result name="success">/user/{1}_success.jsp</result>
    <allowed-methods>add,delete,update,get</allowed-methods>
</action>

當請求的url為/user_add時,會呼叫UserAction的add方法。

注意:萬用字元方法中的一些坑

1.使用萬用字元方法進行對映,和使用!符號的“動態方法呼叫”可能會重疊,需要設定一個常量來禁用動態方法呼叫(Struts2.5預設是禁用狀態):

<constant name="struts.enable.DynamicMethodInvocation" value="false" />

2.對於Struts2.3之前的版本可以不配置<allowed-methods>,但是對於之後的版本,如果不配置<allowed-methods>將會出現如下異常:

Struts has detected an unhandled exception:
Message:There is no Action mapped for namespace [/] and action name [user_login] associated with context path [/shop].

具體原因可以繼續往下看

2. 動態方法呼叫

除了萬用字元方法,Struts2還提供了一種更便捷的方式進行Action的方法對映,而且官方還為他取了個牛逼哄哄的名字——動態方法呼叫(Dynamic Method Invocation)。不過官方文件中說,DMI方式存在安全性問題,所以Struts2中預設把這個功能關閉了(default.property檔案中struts.enable.DynamicMethodInvocation=false),如果需要使用需要設定常量將該功能開啟。

<constant name="struts.enable.DynamicMethodInvocation" value="true" />

動態方法呼叫和下面這種萬用字元方法在功能上類似(和上面萬用字元方法的配置相似,只是把下劃線_改成了!號):

<action name="use!*" class="cn.hff.struts.UserAction" method="{1}">
    ...
</action>

當請求url為use!register時,會呼叫UserAction的register方法。

不過官方文件中說動態方法呼叫和萬用字元方法的實現並不相同,並且推薦首選萬用字元方法而不是動態方法呼叫。

The Wildcard Method feature is implemented differently. When a Wildcard Method action is invoked, the framework acts as if the matching action had been hardcoded in the configuration. The framework "believes" it's executing the action Category!create and "knows" it is executing the create method of the corresponding Action class. Accordingly, we can add for a Wildcard Method action mapping its own validations, message resources, and type converters, just like a conventional action mapping. For this reason, the Wildcard Method is preferred.

3. strict-method-invocation

在Struts2.3中在package標籤中添加了一個屬性來限制DMI,該選項告訴Struts2框架拒絕所有未通過method屬性配置或者<allowed-methods>標籤標明的方法。

<allowed-methods>配置Action可訪問的方法,中間用逗號隔開。

在Struts2.5中不僅僅限制了DMI的呼叫,還對Action的可使用的方法進行了限制,也就是預設開啟了strict-method-invocation選項(在struts-default.xml檔案中可以看到)

總結起來就是:

  • Struts 2添加了一個struts.enable.DynamicMethodInvocation常量來開啟DMI呼叫方式
  • Struts 2.3在package標籤中添加了一個strict-method-invocation屬性以及一個<allowed-methods>標籤來限制DMI可呼叫的方法。
  • Struts 2.5將strict-method-invocation屬性預設值設定成了true