`
zengbo0710
  • 浏览: 404314 次
社区版块
存档分类
最新评论

Struts源码研究 - Action-Input属性篇

阅读更多
初学Struts,写了一个很简单的应用,主要功能和页面如下:
1、首页显示一个“添加新用户”的链接,点击该链接出发一个forward动作,页面导向到添加用户的jsp页面
2、添加用户的jsp页面中,可供用户输入“用户名”和“用户描述”两项
3、用户输入完毕,将做输入数据合法性检查,检查通过,将输入信息保存进入文件(使用了Properties类),然后返回首页;检查失败返回添加用户页面
4、数据合法性检查分成两块,第一部分检查条件使用Struts的Validator,检查条件配置在Validator.xml中;第二部分检查放在ActionForm中,
检查失败将错误信息置入ActionErrors中,然后返回到添加用户的页面并显示错误信息。

JSP页面、ActionForm和Action类的代码书写都参照了struts-example应用,所以这里代码不再列举,请看附件中的代码包
这里值得一提的是,在开发过程中,碰到了一个小问题,正是由于该问题,才导致查看Struts源码,刨根问底的查找错误原因的过程
该错误发生在Struts的配置文件中,首先将错误的配置文件列出如下:
====================================================
<?xml version="1.0" encoding="ISO-8859-1" ?>

<!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
"http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">

<struts-config>

<!-- ======================================== Form Bean Definitions -->

<form-beans>

<form-bean

name="CreateUserForm"

type="com.zchome.CreateUserForm"/>


</form-beans>

<!-- ================================= Global Exception Definitions -->

<global-exceptions>

</global-exceptions>

<!-- =================================== Global Forward Definitions -->

<global-forwards>

<!-- Default forward to "Welcome" action -->

<!-- Demonstrates using index.jsp to forward -->

<forward name="welcome" path="/Welcome.do"/>

</global-forwards>

<!-- =================================== Action Mapping Definitions -->

<action-mappings>

<!-- Default "Welcome" action -->

<!-- Forwards to Welcome.jsp -->

<action

path="/Welcome"

type="org.apache.struts.actions.ForwardAction"

parameter="/jsp/Welcome.jsp"/>


<action path="/createuserpage" forward="/jsp/createuser.jsp">
</action>


<action

path="/docreateuser"

type="com.zchome.CreateUserAction"

name="CreateUserForm"

scope="request"

input="createuser">

<forward name="createusersuccess" path="/jsp/Welcome.jsp"/>

<forward name="createuser" path="/jsp/createuser.jsp"/>

</action>


</action-mappings>


<!-- ===================================== Controller Configuration -->

<controller>
<set-property property="processorClass" value="org.apache.struts.tiles.TilesRequestProcessor"/>
</controller>

<!-- ================================ Message Resources Definitions -->

<message-resources parameter="resources.application"/>

<!-- ======================================= Plug Ins Configuration -->

<!-- ========== Tiles plugin =================== -->
<!-- -->
<!--
This plugin initialize Tiles definition factory. This later can takes some
parameters explained here after. The plugin first read parameters from web.xml, then
overload them with parameters defined here. All parameters are optional.
The plugin should be declared in each struts-config file.
- definitions-config: (optional)
Specify configuration file names. There can be several comma
separated file names (default: ?? )
- moduleAware: (optional - struts1.1)
Specify if the Tiles definition factory is module aware. If true (default),
there will be one factory for each Struts module.
If false, there will be one common factory for all module. In this later case,
it is still needed to declare one plugin per module. The factory will be
initialized with parameters found in the first initialized plugin (generally the
one associated with the default module).
true : One factory per module. (default)
false : one single shared factory for all modules
- definitions-parser-validate: (optional)
Specify if xml parser should validate the Tiles configuration file.
true : validate. DTD should be specified in file header. (default)
false : no validation

Paths found in Tiles definitions are relative to the main context.
-->
<!-- comment following if struts1.0.x -->
<plug-in className="org.apache.struts.tiles.TilesPlugin" >
<set-property property="definitions-config"
value="/WEB-INF/tiles-defs.xml" />
<set-property property="moduleAware" value="true" />
<set-property property="definitions-parser-validate" value="true" />
</plug-in>

<!-- end comment if struts1.0.x -->

<plug-in className="org.apache.struts.validator.ValidatorPlugIn">
<set-property
property="pathnames"
value="/WEB-INF/validator-rules.xml,/WEB-INF/validation.xml"/>
</plug-in>

</struts-config>
====================================================
首先描述一下系统的出错背景:
1、从首页点击链接来到添加用户的页面 正常
2、在添加用户页面中输入Vlidator.xml文件中定义的错误数据,弹出Javascript对话框,提示出错 正常
3、在添加用户页面中输入合法数据,数据保存进入文件并重定向到首页 正常
4、在添加用户页面中输入ActionForm中定义的非法数据,系统应返回到添加用户的页面 出错!!!
OK,来着重看这个添加动作的定义,如下:
====================================================
<action

path="/docreateuser"

type="com.zchome.CreateUserAction"

name="CreateUserForm"

scope="request"

input="createuser">

<forward name="createusersuccess" path="/jsp/Welcome.jsp"/>

<forward name="createuser" path="/jsp/createuser.jsp"/>

</action>
====================================================
从以上的定义可以看出,如果Validate验证出错,Struts应该为我们重定向到input域所定义的uri,即/jsp/createuser.jsp
看起来应该没有问题,再来看看出错信息,如下:
====================================================
java.lang.IllegalArgumentException: Path createuser does not start with a "/" character
at org.apache.catalina.core.ApplicationContext.getRequestDispatcher(ApplicationContext.java:1179)
at org.apache.catalina.core.ApplicationContextFacade.getRequestDispatcher(ApplicationContextFacade.java:174)
at org.apache.struts.action.RequestProcessor.doForward(RequestProcessor.java:1062)
at org.apache.struts.tiles.TilesRequestProcessor.doForward(TilesRequestProcessor.java:274)
at org.apache.struts.action.RequestProcessor.internalModuleRelativeForward(RequestProcessor.java:1012)
at org.apache.struts.tiles.TilesRequestProcessor.internalModuleRelativeForward(TilesRequestProcessor.java:345)
at org.apache.struts.action.RequestProcessor.processValidate(RequestProcessor.java:980)
at org.apache.struts.action.RequestProcessor.process(RequestProcessor.java:255)
at org.apache.struts.action.ActionServlet.process(ActionServlet.java:1482)
at org.apache.struts.action.ActionServlet.doPost(ActionServlet.java:525)

。。。以下省略。。。
====================================================
出错信息清楚的说明,“createuser”这个path应该以“/”字符开头
为定位这个错误,从以上错误信息,开始打开Struts的源码RequestProcessor.java进行研究,首先来到这一段:
====================================================
public class RequestProcessor {

。。。。。。

protected boolean processValidate(HttpServletRequest request,
HttpServletResponse response,
ActionForm form,
ActionMapping mapping)
throws IOException, ServletException {

if (form == null) {
return (true);
}

// Was this request cancelled?
if (request.getAttribute(Globals.CANCEL_KEY) != null) {
if (log.isDebugEnabled()) {
log.debug(" Cancelled transaction, skipping validation");
}
return (true);
}

// Has validation been turned off for this mapping?
if (!mapping.getValidate()) {
return (true);
}

// Call the form bean's validation method
if (log.isDebugEnabled()) {
log.debug(" Validating input form properties");
}
ActionMessages errors = form.validate(mapping, request);
if ((errors == null) || errors.isEmpty()) {
if (log.isTraceEnabled()) {
log.trace(" No errors detected, accepting input");
}
return (true);
}

// Special handling for multipart request
if (form.getMultipartRequestHandler() != null) {
if (log.isTraceEnabled()) {
log.trace(" Rolling back multipart request");
}
form.getMultipartRequestHandler().rollback();
}

// Has an input form been specified for this mapping?
String input = mapping.getInput();
if (input == null) {
if (log.isTraceEnabled()) {
log.trace(" Validation failed but no input form available");
}
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
getInternal().getMessage("noInput",
mapping.getPath()));
return (false);
}

// Save our error messages and return to the input form if possible
if (log.isDebugEnabled()) {
log.debug(" Validation failed, returning to '" + input + "'");
}
request.setAttribute(Globals.ERROR_KEY, errors);

if (moduleConfig.getControllerConfig().getInputForward()) {
ForwardConfig forward = mapping.findForward(input);
processForwardConfig( request, response, forward);
} else {
internalModuleRelativeForward(input, request, response);
}

return (false);

}
====================================================
在出错信息中,提到了internalModuleRelativeForward这个方法,所以着重看以上代码的最后几行,可以看到,如果
moduleConfig.getControllerConfig().getInputForward()这个方法返回了false,那么internalModuleRelativeForward
这个方法将被调用。inputForward是什么?ModuleConfig是管理所有配置信息的一个manager类,那么moduleConfig.getControllerConfig()
这个方法返回的肯定是ControllerConfig这个类的一个实例,那么inputForward肯定是ControllerConfig类的一个成员变量了
再看看struts-config.xml,里面有<controller>这个标签,初步猜测ControllerConfig应该是读取这个标签的一个配置类
而<controller>这个标签应该定义了ActionServlet作为Controller的一些行为!
OK,再来看ControllerConfig这个类中有关inputForward这个成员变量的一些代码,如下:
====================================================
/**
* <p>Should the <code>input</code> property of {@link ActionConfig}
* instances associated with this module be treated as the
* name of a corresponding {@link ForwardConfig}. A <code>false</code>
* value treats them as a module-relative path (consistent
* with the hard coded behavior of earlier versions of Struts.</p>
*
* @since Struts 1.1
*/
protected boolean inputForward = false;

public boolean getInputForward() {
return (this.inputForward);
}

public void setInputForward(boolean inputForward) {
this.inputForward = inputForward;
}
====================================================
开始有点明白了,原来inputForward这个属性默认值是false,那么由于没有配置这个属性,那么上述的那个方法
moduleConfig.getControllerConfig().getInputForward()自然就返回false了,Bingo!
那么重点就转移到了internalModuleRelativeForward这个方法了,看这个方法的源代码,如下:
====================================================
protected void internalModuleRelativeForward(
String uri,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {

// Construct a request dispatcher for the specified path
uri = moduleConfig.getPrefix() + uri;

// Delegate the processing of this request
// FIXME - exception handling?
if (log.isDebugEnabled()) {
log.debug(" Delegating via forward to '" + uri + "'");
}
doForward(uri, request, response);
}

protected void doForward(
String uri,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {

// Unwrap the multipart request, if there is one.
if (request instanceof MultipartRequestWrapper) {
request = ((MultipartRequestWrapper) request).getRequest();
}

RequestDispatcher rd = getServletContext().getRequestDispatcher(uri);
if (rd == null) {
response.sendError(
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
getInternal().getMessage("requestDispatcher", uri));
return;
}
rd.forward(request, response);
}
====================================================
从上可以看到,这个方法是将uri = moduleConfig.getPrefix() + uri;这个东东传给了doForward方法
而doForward这个方法又调用了javax.servlet.ServletContext的方法getRequestDispatcher这个方法
既然出错信息中是路径出了问题,那么看来这个参数uri非常的重要,极有可能就是这个uri发生了错误导致了出错
OK,开始剖析这个uri,从头开始看,这个uri是这样被赋值的:
uri = moduleConfig.getPrefix() + uri

1、moduleConfig.getPrefix()这个方法返回的应该是""
(这个请看ActionServlet的Init方法,如果在web.xml文件中定义ActionServlet的时候,给定了一些init-params,那么这个prefix就有可能
不为空,这里不再列举了)
2、代码右边的这个uri是从processValidate这个方法中定义的input,如下:
String input = mapping.getInput();
这个input应该是struts-config.xml文件中定义的那个action的input,也就是“createuser”,如果Struts将其做了进一步的解析,那么这个
input应该进一步被转化成为“/jsp/createuser.jsp”

好,到此为止,可以看到,这个uri不是“createuser”,那就是“/jsp/createuser.jsp”,再来看getRequestDispatcher这个方法的定义,
翻开Servlet的API文档,可以看到如下一段话:
====================================================
public RequestDispatcher

getRequestDispatcher(java.lang.String path)Returns a RequestDispatcher object that acts as a wrapper
for the resource located at the given path. A RequestDispatcher object can be used to forward a request
to the resource or to include the resource in a response. The resource can be dynamic or static.
The pathname must begin with a "/" and is interpreted as relative to the current context root.
Use getContext to obtain a RequestDispatcher for resources in foreign contexts. This method returns null
if the ServletContext cannot return a RequestDispatcher.
====================================================
终于有拨云见日的感觉了,因为这段话和出错信息实在是太一致了!由上面这段话,我们可以断定,uri这个变量的值
肯定是“createuser”,而不是我们所希望的“/jsp/createuser.jsp”。为什么会这样呢?显然是struts-config.xml中配置
有些还是不对,或是缺了点什么。想到这里,很自然的就联想到上面所提到的InputForward这个配置项了,因为从字面意思上
看来,这个配置项的用处就应该是将input的值解析成forward中对应的值,而且在ControllerConfig中,这个变量默认值是
false,所以猜测将其改成true是不是就可以了呢?
为了寻找答案,再次翻开struts-example(因为这个例子中的action也定义了input),终于找到了答案,和之前猜测的果然
十分吻合,如下:
====================================================
<controller>
<set-property property="inputForward" value="true"/>
</controller>
====================================================
至此,问题解决,正确的action配置可以是如下两种:
====================================================
1、不使用inputForward
<action

path="/docreateuser"

type="com.zchome.CreateUserAction"

name="CreateUserForm"

scope="request"

input="/jsp/createuser.jsp">

<forward name="createusersuccess" path="/jsp/Welcome.jsp"/>

</action>

2、使用InputForward
<action

path="/docreateuser"

type="com.zchome.CreateUserAction"

name="CreateUserForm"

scope="request"

input="createuser">

<forward name="createusersuccess" path="/jsp/Welcome.jsp"/>

<forward name="createuser" path="/jsp/createuser.jsp"/>

</action>

<controller>
<set-property property="inputForward" value="true"/>
<set-property property="processorClass" value="org.apache.struts.tiles.TilesRequestProcessor"/>
</controller>

====================================================
而且,从问题的定位过程中,还学到了一招,就是javax.servlet.RequestDispatcher:
RequestDispatcher rd = getServletContext().getRequestDispatcher(uri);
if (rd == null) {
response.sendError(
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
getInternal().getMessage("requestDispatcher", uri));
return;
}
rd.forward(request, response);

以后再做页面重定向,只要给定相对的uri就可以了,再也不用写上一层的虚拟目录名或自己拼URL了

Struts源码研究 - html-Cancel标签应用注意事项篇

Struts里的html:Cancel标签是在Form中经常运用的一个标签,主要功能就是cancel当前Form,一般写法如下:

=======================================================
<html:cancel>
<bean:message key="createuser.cancelbutton"/>
</html:cancel>
=======================================================

这个标签将生成如下的HTML代码:
<input type="submit" name="org.apache.struts.taglib.html.CANCEL" value="返回" onclick="bCancel=true;">
bCancel=true是一段javascript,bCancel是在使用Struts的Validator时,Struts自动为我们加的一段Javascript代码里的一个变量
这段Javascript简略摘要如下:

=======================================================
<script type="text/javascript" language="Javascript1.1">

<!-- Begin

var bCancel = false;

function validateCreateUserForm(form) {
if (bCancel)
return true;
else
return validateMaxLength(form) && validateRequired(form) && validateMinLength(form);
}

。。。以下省略
=======================================================

由上可以看到,这个bCancel=true时,Javascript将自动将表单提交(return true),也就是说,如果我们在后台Action的代码里
没有对这个Cancel动作写特定代码的话,这个Cancel标签产生的效果和submit按钮产生的动作完全一致!!(因为这个按钮的
type也等于submit)
这一点需要非常的注意!所以,一般来说,我们在Action类的execute方法里面会加上如下的一段代码来处理这个Cancel动作:

=======================================================
// Was this transaction cancelled?
if (isCancelled(request)) {
return (mapping.findForward("createusersuccess"));
}
=======================================================

有了以上的代码,Cancel动作就有了相应的处理代码,转到相关的页面了。
本来事情已经解决,但本着对Struts源码研究的精神,我们还需要对以上代码研究一下
OK,让我们来看一下isCancelled这个方法在什么地方被定义了,内容是什么?
首先发现,这个方法被定义在Action类里面,代码如下:

=======================================================
/**
* <p>Returns <code>true</code> if the current form's cancel button was
* pressed. This method will check if the <code>Globals.CANCEL_KEY</code>
* request attribute has been set, which normally occurs if the cancel
* button generated by <strong>CancelTag</strong> was pressed by the user
* in the current request. If <code>true</code>, validation performed
* by an <strong>ActionForm</strong>'s <code>validate()</code> method
* will have been skipped by the controller servlet.</p>
*
* @param request The servlet request we are processing
* @see org.apache.struts.taglib.html.CancelTag
*/
protected boolean isCancelled(HttpServletRequest request) {

return (request.getAttribute(Globals.CANCEL_KEY) != null);

}
=======================================================

哦,原来是在request对象中查找Globals.CANCEL_KEY这个key值是否绑定了一个对象,如果是,那么就代表按下Cancel按钮后,
Struts会在request对象中绑定一个对象,并以这个key值来命名
那Struts是在什么地方绑定了这个对象呢?很自然的,让我们从头找起
从ActionServlet的process方法开始找起,历经多次方法调用,终于找到了根源,原来是在RequestProcessor.java中,代码如下:

=======================================================
/**
* <p>Process an <code>HttpServletRequest</code> and create the
* corresponding <code>HttpServletResponse</code>.</p>
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
*
* @exception IOException if an input/output error occurs
* @exception ServletException if a processing exception occurs
*/
public void process(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {

//。。。省略代码若干
// Process any ActionForm bean related to this request
ActionForm form = processActionForm(request, response, mapping);
//答案就在这个processPopulate方法中
processPopulate(request, response, form, mapping);
if (!processValidate(request, response, form, mapping)) {
return;
}

/**
* Populate the properties of the specified ActionForm instance from
* the request parameters included with this request. In addition,
* request attribute <code>Globals.CANCEL_KEY</code> will be set if
* the request was submitted with a button created by
* <code>CancelTag</code>.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param form The ActionForm instance we are populating
* @param mapping The ActionMapping we are using
*
* @exception ServletException if thrown by RequestUtils.populate()
*/
protected void processPopulate(HttpServletRequest request,
HttpServletResponse response,
ActionForm form,
ActionMapping mapping)
throws ServletException {

if (form == null) {
return;
}

// Populate the bean properties of this ActionForm instance
if (log.isDebugEnabled()) {
log.debug(" Populating bean properties from this request");
}

form.setServlet(this.servlet);
form.reset(mapping, request);

if (mapping.getMultipartClass() != null) {
request.setAttribute(Globals.MULTIPART_KEY,
mapping.getMultipartClass());
}

RequestUtils.populate(form, mapping.getPrefix(), mapping.getSuffix(),
request);

// Set the cancellation request attribute if appropriate
if ((request.getParameter(Constants.CANCEL_PROPERTY) != null) ||
(request.getParameter(Constants.CANCEL_PROPERTY_X) != null)) {

request.setAttribute(Globals.CANCEL_KEY, Boolean.TRUE);
}

}

=======================================================

OK,看最后几行代码,Struts从request中取得Constants.CANCEL_PROPERTY这个参数,如果这个参数不为空,那么他就将
TRUE这个对象以Globals.CANCEL_KEY为key值,放到了request对象中
至于这个Constants.CANCEL_PROPERTY这个值是什么,现在都可以猜到了,显然就是html:Cancel这个标签生成的HTML代码
中,Cancel这个按钮的名称嘛!查了一下,果然是:

<input type="submit" name="org.apache.struts.taglib.html.CANCEL" value="返回" onclick="bCancel=true;">
而Constants.CANCEL_PROPERTY这个值就是org.apache.struts.taglib.html.CANCEL

至此,真相大白,看来以后使用html:Cancel这个标签要小心了 :)  


分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics