1. 問題
- Struts2 的 Action 我們將它定義為一個控制器,但是由于在 Action 中也可以來編寫一些業(yè)務(wù)邏輯,也有人會在 Action 輸入業(yè)務(wù)邏輯層。
- 但是在企業(yè)開發(fā)中,我們一般會將業(yè)務(wù)邏輯層單獨編寫,而不是將它與 action 層寫到一起。
- 之前的練習(xí)中,我們一直將屬性如 username 、 password 等保存在了 action 中。
- 這樣做了以后導(dǎo)致我們在調(diào)用業(yè)務(wù)邏輯層時可能需要將Action的對象傳過去。
- 但是這樣做無異于直接將 Servlet 傳給 Service,導(dǎo)致兩層之間的耦合,而且我們也不希望其他對象可以直接使用Action對象。
- 要解決這個問題,我們可以采用的一種方式是,專門在Action中定義一個屬性,用來封裝信息。然后只需要將這個對象傳個service即可。
- 但是這樣又帶來了一個新問題,誰來將屬性封裝進對象呢?答案就是ModelDriven攔截器
- 攔截器:
- 攔截器的作用和過濾器類似。
- 攔截器可以在請求到達Action之前進行攔截,希望在請求到達Action之前通過攔截器做一些準(zhǔn)備工作。
-
Struts2 簡單的運行流程:
- 請求首先到達StrutsPrepareAndExecuteFilter.doFilter()
- 在doFilter方法中,先獲取ActionMapping
- 判斷:如果ActionMapping為null,不是Struts請求,直接放行
- 如果ActionMapping不為null,是Struts請求,繼續(xù)處理
- 通過configurationManager加載Struts的配置信息,找到請求對應(yīng)的Action對象
- 根據(jù)配置信息創(chuàng)建ActionProxy代理類。
- 在StrutsActionProxy.execute()方法中調(diào)用DefaultActionInvocation.invoke()方法
- 對所有的攔截器進行迭代在去分別調(diào)用攔截器intercept方法,進行攔截請求
- intercept方法我們對請求進行一些處理,處理完畢以后繼續(xù)DefaultActionInvocation.invoke()方法
- 如此反復(fù)直到所有的攔截器都被調(diào)用
- 最后才去執(zhí)行Action的方法。

Struts2 運行流程 | center
2. 請求參數(shù)在哪封裝的:
- 請求參數(shù)在到達Action之前,會先經(jīng)過ParametersInterceptor攔截器,
- 在該攔截器中會自動對將請求參數(shù)封裝進值棧的棧頂對象,
- 他會根據(jù)屬性名將屬性值設(shè)置進棧頂對象的相應(yīng)屬性中,
- 如果棧頂中沒有該屬性,則繼續(xù)向第二對象進行封裝。
<!-- 查看 ParametersInterceptor 源碼-->
@Override
public String doIntercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
if (!(action instanceof NoParameters)) {
ActionContext ac = invocation.getInvocationContext();
final Map<String, Object> parameters = retrieveParameters(ac);
if (LOG.isDebugEnabled()) {
LOG.debug("Setting params " + getParameterLogMap(parameters));
}
if (parameters != null) {
Map<String, Object> contextMap = ac.getContextMap();
try {
ReflectionContextState.setCreatingNullObjects(contextMap, true);
ReflectionContextState.setDenyMethodExecution(contextMap, true);
ReflectionContextState.setReportingConversionErrors(contextMap, true);
ValueStack stack = ac.getValueStack();
setParameters(action, stack, parameters); // 在這方法中設(shè)置參數(shù)到對象中
} finally {
ReflectionContextState.setCreatingNullObjects(contextMap, false);
ReflectionContextState.setDenyMethodExecution(contextMap, false);
ReflectionContextState.setReportingConversionErrors(contextMap, false);
}
}
}
return invocation.invoke();
}
3. ModelDriven攔截器
- 雖然我們知道了參數(shù)是從什么地方壓入棧頂對象的,但是我們還是無法更好的處理方式把參數(shù)封裝成對象,放到對象中去。
- 所以我們需要使用ModelDriven 攔截器
- 我們查看 ModelDrivenInterceptor 源碼能發(fā)現(xiàn),在該攔截器中,對于實現(xiàn)了 ModelDriven 接口的Action ,會調(diào)用getModel() 方法把,getModel() 返回的對象壓入值棧棧頂,所以對于我們來說,可以使用一個JavaBean 作為參數(shù)的分裝
@Override
public String intercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
if (action instanceof ModelDriven) {
ModelDriven modelDriven = (ModelDriven) action;
ValueStack stack = invocation.getStack();
Object model = modelDriven.getModel();
if (model != null) {
stack.push(model);
}
if (refreshModelBeforeResult) {
invocation.addPreResultListener(new RefreshModelBeforeResult(modelDriven, model));
}
}
return invocation.invoke();
}
- 當(dāng)用戶觸發(fā) add 請求時, ModelDriven 攔截器將調(diào)用 EmployeeAction 對象的 getModel() 方法, 并把返回的模型(Employee實例)壓入到 ValueStack 棧.
- 接下來 Parameters 攔截器將把表單字段映射到 ValueStack 棧的棧頂對象的各個屬性中. 因為此時 ValueStack 棧的棧頂元素是剛被壓入的模型(Employee)對象, 所以該模型將被填充. 如果某個字段在模型里沒有匹配的屬性, Param 攔截器將嘗試 ValueStack 棧中的下一個對象
/**
* 實現(xiàn) ModelDriven 接口
*/
public class EmployeeAction extends ActionSupport implements RequestAware, ModelDriven<Employee>
//=================
// 重寫實現(xiàn) getModel 方法
@Override
public Employee getModel() {
System.out.println(" getModel() Method, ............");
if (employee == null) {
employee = new Employee();
}
// 如果通過 return new Employee() 的方式,雖然棧頂對象是Employee,但是在Action 中的Employee為Null
return employee;
}
- 但是我們還有一個問題需要解決, 我們?nèi)バ薷谋韱螖?shù)據(jù)的時候。通常需要使用Id 去數(shù)據(jù)庫中查詢出來對象。如果我們直接用下面的方式,在修改頁面是無法獲取到對應(yīng)的值。
- 示例中我們只是使得
employee引用復(fù)制了一份,并且指向了給employeeDao.getEmpById(employee.getId());對象,實際上我們在棧頂兌現(xiàn)給的值并沒有改變
- 示例中我們只是使得
public String input() {
// 因為點擊修改的時候,會put Id值到棧頂對象Employee 中,所以我們查詢出來的employee 并不會放到棧頂
// 如果這樣的話 我們把指向 ValueStack 棧頂?shù)囊?,改為了從?shù)據(jù)庫查詢出來的值。
// 所以 ValueStack 中的 employee 并沒有被賦值
employee = employeeDao.getEmpById(employee.getId());
System.out.println(employee);
return "input";
}
- 如圖所示| center
但是我們?nèi)绻粋€個的去賦值屬性又特別麻煩.。如果使用獲取ValueStack 的方式去重新放入棧頂再彈出的方式也不是一個好的方法。
Employee emp = employeeDao.getEmpById(employee.getId());
employee.setName(emp.getName());
employee.setRole(emp.getRole());
employee.setDept(emp.getDept());
employee.setAge(emp.getAge());
- 所以我們最好的方式,是在
getModel()方法中,根據(jù) Action 的屬性id去數(shù)據(jù)庫中獲取資源。但是程序中默認使用的是 defaultStack 攔截器棧。在defaultStack攔截器棧中,modelDriven攔截器之前并沒有params攔截器,如果需要對Action 參數(shù)賦值的話,就需要在modelDriven攔截器之前設(shè)置params攔截器
<interceptor-stack name="defaultStack">
<interceptor-ref name="exception"/>
<interceptor-ref name="alias"/>
<interceptor-ref name="servletConfig"/>
<interceptor-ref name="i18n"/>
<interceptor-ref name="prepare"/>
<interceptor-ref name="chain"/>
<interceptor-ref name="scopedModelDriven"/>
<interceptor-ref name="modelDriven"/>
<interceptor-ref name="fileUpload"/>
<interceptor-ref name="checkbox"/>
<interceptor-ref name="datetime"/>
<interceptor-ref name="multiselect"/>
<interceptor-ref name="staticParams"/>
<interceptor-ref name="actionMappingParams"/>
<interceptor-ref name="params"/>
<interceptor-ref name="conversionError"/>
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="workflow">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="debugging"/>
<interceptor-ref name="deprecation"/>
</interceptor-stack>
- 根據(jù)上面的結(jié)果,我們需要做的操作是:
- 先調(diào)用 param 攔截器封裝一部分屬性進入Action
- 再調(diào)用
modelDriven攔截器進入值棧 - 在 getModel() 方法中判斷id 是否為空,然后再返回不同的結(jié)果
- 于是,我們可以使用
paramsPrepareParamsStack攔截器棧,paramsPrepareParamsStack 這個攔截器棧中params攔截器會執(zhí)行兩次,- 一次在ModelDriven之前,一次在ModelDriven之后,
- 這樣我們就可以根據(jù)不同的請求參數(shù),給值棧棧頂放入不同對象。
- 在 struts.xml 修改默認的攔截器棧
<!-- 修改攔截器棧 -->
<interceptors>
<interceptor-stack name="myInterceptorStck" >
<interceptor-ref name="paramsPrepareParamsStack"></interceptor-ref>
</interceptor-stack>
</interceptors>
<!-- 設(shè)置使用的攔截器為 我們設(shè)置的攔截器棧: myInterceptorStck -->
<default-interceptor-ref name="myInterceptorStck"></default-interceptor-ref>
- 使用
paramsPrepareParamsStack攔截器棧后,修改后的getModel()和input方法
@Override
public Employee getModel() {
System.out.println(" getModel() Method ............");
if (id == null) {
employee = new Employee();
} else {
employee = employeeDao.getEmpById(id);
}
return employee;
}
// ====================
public String input() {
return "input";
}
- 這樣使得代碼就更加的簡潔了一些
- 流程
4. 新的問題
- 在使用
paramsPrepareParamsStack攔截器棧 之后,雖然已經(jīng)很簡潔了,但是我們出現(xiàn)了新的問題。- 在我們做
CRUD操作是,delete和update方法都帶有 id- 一個根據(jù) id 刪除用戶
- 一個是更新用戶信息
- 都有 id 就會導(dǎo)致調(diào)用getModel會去數(shù)據(jù)庫中查詢員工信息。
- 但是實際業(yè)務(wù)上我們并沒有這樣的需求
- 所以我們引出了
Preparable攔截器
- 在我們做
5. Preparable 攔截器
- Struts 2.0 中的 modelDriven 攔截器負責(zé)把 Action 類以外的一個對象壓入到值棧棧頂
- 而 prepare 攔截器負責(zé)準(zhǔn)備為 getModel() 方法準(zhǔn)備 model, 并且在 modelDriven 攔截器之前
-
PrepareInterceptor攔截器使用方法
- 若 Action 實現(xiàn) Preparable 接口,則 Action 方法需實現(xiàn) prepare() 方法
- PrepareInterceptor 攔截器將調(diào)用 prepare() 方法,prepareActionMethodName()方法 或 prepareDoActionMethodName ()方法
- PrepareInterceptor 攔截器根據(jù)
firstCallPrepareDo屬性決定獲取 prepareActionMethodName 、prepareDoActionMethodName的順序。默認情況下先獲取prepareActionMethodName (), 如果沒有該方法,就尋找prepareDoActionMethodName()。如果找到對應(yīng)的方法就調(diào)用該方法 - PrepareInterceptor 攔截器會根據(jù)
alwaysInvokePrepare屬性決定是否執(zhí)行prepare()方法
// com.opensymphony.xwork2.interceptor.PrepareInterceptor
private final static String PREPARE_PREFIX = "prepare";
private final static String ALT_PREPARE_PREFIX = "prepareDo";
private boolean alwaysInvokePrepare = true;
private boolean firstCallPrepareDo = false;
//===============
@Override
public String doIntercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
if (action instanceof Preparable) {
try {
String[] prefixes;
//根據(jù) firstCallPrepareDo 的值(默認為false)來決定調(diào)用兩個prepare 方法,可以在struts.xml 中修改
if (firstCallPrepareDo) {
prefixes = new String[] {ALT_PREPARE_PREFIX, PREPARE_PREFIX};
} else {
prefixes = new String[] {PREPARE_PREFIX, ALT_PREPARE_PREFIX};
}
//調(diào)用
PrefixMethodInvocationUtil.invokePrefixMethod(invocation, prefixes);
}
catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof Exception) {
throw (Exception) cause;
} else if(cause instanceof Error) {
throw (Error) cause;
} else {
throw e;
}
}
// 根據(jù) alwaysInvokePrepare 值(默認為true)來決定是否調(diào)用 Action 的 prepare()方法
if (alwaysInvokePrepare) {
((Preparable) action).prepare();
}
}
return invocation.invoke();
}
- 方法執(zhí)行順序:
- action.prepareXxx方法
- action.prepare方法
- action.getModel
- action.action方法
- 我們可以在prepareXxx方法做一個個性化的處理,可以在prepare方法做一些統(tǒng)一的處理。
public String add() {
employeeDao.save(employee);
return SUCCESS;
}
/**
* 該方法會在add方法執(zhí)行之前執(zhí)行
* 也會在getModel方法執(zhí)行之前執(zhí)行,放到值棧棧頂中
*/
public void prepareDoAdd() {
System.out.println("prepareDoAdd............");
employee = new Employee();
}
//======================================================
public String del() {
employeeDao.delete(employee.getId());
return SUCCESS;
}
//======================================================
public String input() {
return "input";
}
public void prepareInput() {
System.out.println("prepareInput.........");
employee = employeeDao.getEmpById(id);
}
//======================================================
public String update() {
employeeDao.update(employee);
return SUCCESS;
}
/**
* 在update方法之前,初始化employ,然后由 getModel 方法放到棧頂
*/
public void prepareUpdate() {
System.out.println("prepareUpdate..............");
employee = new Employee();
}
//======================================================
@Override
public Employee getModel() {
System.out.println("getModel。。。。。。。。。。。");
return employee;
}
//======================================================
/**
* 該方法中,可以做一些統(tǒng)一的處理
*/
@Override
public void prepare() throws Exception {
System.out.println("prepare..................");
/*
* 例如:對于 delete 方法,雖然該方法只需要使用 id,
* 但是我們調(diào)用該方法之前,調(diào)用 getModel() 返回的是個 null
* 所以我們可以在這 做一次判斷
*/
if (employee == null) {
employee = new Employee();
}
}
- 對于我們 如果想修改
PrepareInterceptor攔截器中的一些參數(shù)。- 修改 prepareXxx 和 prepareDoXxxx 調(diào)用的順序
- 修改
alwaysInvokePrepare的值,使得 Action 不調(diào)用 prepare() 方法 - 參考 docs 中 Interceptor Parameter Overriding
<!-- 修改攔截器棧 -->
<interceptors>
<interceptor-stack name="myInterceptorStck" >
<interceptor-ref name="paramsPrepareParamsStack">
<!-- 修改攔截器棧屬性值,name.filed -->
<param name="prepare.alwaysInvokePrepare">false</param>
<param name="prepare.firstCallPrepareDo">true</param>
</interceptor-ref>
</interceptor-stack>
</interceptors>

