Struts控制器组件负责接受用户请求、更新模型,以及选择合适的视图组件返回给用户。控制器组件有助于将模型层和视图层分离,有了这种分离,就可以在同一个模型的基础上得心应手地开发多种类型的视图。Struts控制器组建主要包括:
· ActionServlet 组件:充当 Struts 框架的中央控制器。
·RequestProcessor组件:充当每个子应用模块的请求处理器。
·Action组件:负责处理一项具体的业务。
Struts 框架采用控制器组件来预处理所有的客户请求,这种集中控制方式可以满足 MVC 设计模式的两大需求:
·首先,控制器在用户输入数据和模型之间充当媒介 / 翻译者的角色,提供一些通用功能,如安全、登入和其他针对具体用户请求的重要服务,当系统的这些通用功能出现需求变更时,部需要修改整个应用,只需要修改局部的控制器组件即可。
·其次,由于所有的请求都经过控制器过滤,因此可以降低视图组件之间,以及视图组件和模型组件之间的相互依赖关系,提高每个组件的相对独立性。由控制器组件来决定把合适的视图组件返回给用用户,这可以减少视图组件之间直接的,错综复杂的连接关系,使应用更加灵活,便于维护。
Struts 框架采用 ActionServlet 和 RequestProcessor 组件进行集中控制,并采用 Action 组件来处理单项业务。
一 控制器组件的控制机制
Struts 的控制器组件主要完成以下任务:
·接受用户请求
·根据用户请求,调用合适的模型组件来执行相应的业务逻辑。
·获取业务逻辑执行结果。
·根据当前状态以及业务逻辑执行结果,选择合适的视图组件返回给用户。
1 Action 类
org.apache.struts.action.ActionServlet 类是 Struts 框架的核心控制器组件,所有的用户请求都先有 ActionServlet 来处理,然后再由 ActionServlet 把请求转发给其他组件。 Struts 框架只允许在一个应用中配置一个 ActionServlet 类,在应用的生命周期中,仅创建 ActionServlet 类的一个实例,这个 ActionServlet 实例可以同时响应多个用户请求。
<!--[if !supportLists]--> (a) <!--[endif]--> Struts 框架初始化过程
<!--[if !supportLists]-->(1) <!--[endif]-->调用initInternal()方法,初始化Struts框架内在的消息资源,如与系统日志相关的同志、警告和错误消息。
<!--[if !supportLists]-->(2) <!--[endif]-->调用initOther()方法,从web.xml文件中加载ActionServlet的初始化参数,如config参数。
<!--[if !supportLists]-->(3) <!--[endif]-->调用initServlet()方法,从web.xml文件中加载ActionServlet的URL映射信息。此外还会注册web.xml和Struts配置文件所使用的DTD文件,这些DTD文件用来验证web.xml和Struts配置文件的语法。
<!--[if !supportLists]-->(4) <!--[endif]-->调用initModuleConfig()方法,加载并解析子应用模块的Struts配置文件;创建ModuleConfig对象,把它存储在ServletContext中。
<!--[if !supportLists]-->(5) <!--[endif]-->调用initModuleMessageResources()方法,加载并初始化默认子应用模块的消息资源:创建MessageResources对象,把它存储在ServletContext中。
<!--[if !supportLists]-->(6) <!--[endif]-->调用initModuleDataSources()方法,加载并初始化默认子应用模块的数据源。如果在Struts配置文件中没有定义<data-sources>元素,就忽略这一流程。
<!--[if !supportLists]-->(7) <!--[endif]-->调用InitModulePlugins()方法,加载并初始化默认子应用模块的所有插件。
<!--[if !supportLists]-->(8) <!--[endif]-->当默认子应用模块被成功地初始化后,如果还包括其他子应用模块,将重复流程(4)~(7),分别对其他子应用模块进行初始化。
<!--[if !supportLists]-->(b) <!--[endif]-->ActionServlet的process()方法
当ActionServlet实例接受到HTTP请求之后,在doGet()或doPost()方法都会调用process()方法来处理请求。一下是ActionServlet的process()方法的源代码:
protected voidprocess (HttpServletRequest request, HttpServletResponse response)
throwIOException, ServletException {
ModuleUtils.getInstance().selectModule(request,getServletContext());
getRequestProcessor(getModuleConfig(request)).process(request,response);
}
在 process() 方法中,首先调用 org.apache.struts.util.ModuleUtils 类的 selectModule() 方法,这个方法选择负责处理当前请求的子应用模块,然后把与子应用模块相关的 ModuleConfig 和 MessageResources 对象存储倒 request 范围中,这使得框架的其余组件可以方便地从 request 范围中读取这些对象,从而获取应用配置信息和消息资源。
process()方法的第二步操作为获得RequestProcessor类的实例,然后调用RequestProcessor类的process()方法,来完成十几的预处理请求操作。
<!--[if !supportLists]--> (c) <!--[endif]--> 扩展ActionServlet 类
在Sturts 1.1 之前的版本中,ActionServlet类本身包含了很多处理请求的代码。从Struts 1.1 开始,多数功能被移到 org.apache.struts.action.RequestProcessor 类中,以便减轻ActionServlet类的控制负担。
尽管新版本的 Struts 框架允许在应用中创建矿展 ActionServlet 类的子类,但是这在多数情况下没有必要,因为控制器的多数控制功能位于 RequestProcessor 类中。
如果实际应用确实需要创建自己的 ActionServlet 类,则可以创建一个 ActionServlet 的子类,然后在 web.xml 文件中配置这个客户化 ActionServlet 类。
如果覆盖了init()方法,应该确保首先调用super.init(),它保证ActionServlet的默认初始化操作被执行。除了覆盖init()方法外,事实上,还可以根据十几需要覆盖ActionServlet的任何其他方法。
2 RequestProcessor类
对于多应用模块的Struts应用,每个子应用模块都有各自的RequestProcessor实例。在ActionServlet的process()方法中,一旦选择了正确的子应用模块,就会调用子应用模块的RequestProcessor实例的process()方法来处理请求。在ActionServlet调用这个方法时,会把当前的request和response对象传给它。
Struts框架只允许应用中存在一个ActionServlet类,但是可以存在多个客户化的RequestProcessor类,每个子应用模块都可以拥有单独的RequestProcessor类。如果想修改RequestProcessor类的一些默认功能,可以覆盖RequestProcessor基类中的相关方法。
<!--[if !supportLists]-->(a) <!--[endif]-->RequestProcessor类的process()方法
RequestProcessor类的process()方法负责实际的预处理请求操作。
RequestProcessor类的process()方法一次执行一下流程:
(1) 调用processMultipart()方法。如果HTTP请求方式为POST,并且请求的contentType属性以“multipart/form-data”开头,标准的HttpServletRequest对象将被重新包装,以方便处理“multipart”类型的HTTP请求。如果请求方式为GET,或者contentType属性不是“multipart”,就直接返回原始的HttpServletRequest对象。
(2) 调用processPath()方法,获得请求URI的路径,这一信息可用于选择合适的Struts Action组件。
(3) 调用processLocale()方法,当ControllerConfig对象的locale属性为true,将读取用户请求中包含的Locale信息,然后把Locale实例保存在session范围内。
(4) 调用processContent()方法,读取ControllerConfig对象的contentType属性,然后调用response.setContentType(contentType)方法,设置响应结果的文档类型和字符编码。
(5) 调用processNoCache()方法,读取ControllerConfig对象的nocache属性,如果nocache属性为true,在响应结果中将加入特定的头参数:Pragma、Cache-Control和Expires,防止页面被存储在客户浏览器的缓存中。
(6) 调用processPreprocess()方法。该方法不执行任何操作,直接返回true。子类可以覆盖这个方法,执行客户化的预处理请求操作。
(7) 调用processMapping()方法,寻找和用户请求的URI匹配的ActionMapping。如果不存在这样的ActionMapping,则向用户返回恰当的错误消息。
(8) 调用processRoles()方法,先判断是否为Action配置了安全角色,如果配置了安全角色,就调用isUserRole()方法判断当前用户是否具备必需的角色;如果不具备,就结束请求处理流程,向用户返回恰当的错误消息。
(9) 调用processActionForm()方法,先判断是否为ActionMapping配置了ActionForm,如果配置了ActionForm,就先从ActionForm的存在范围内寻找该ActionForm实例;如果不存在,就创建一个实例。接下来把它保存在合适的范围中,保存时使用的属性key为ActionMapping的name属性。
(10) 调用processActionForm()方法。如果为ActionMapping配置了ActionForm,就先调用ActionForm的reset()方法,再把请求中的表单数据组装到ActionForm中。
(11) 调用processValidate()方法,如果为ActionMapping配置了ActionForm,并且ActionMapping的validate属性为true,就调用ActionForm的validate()方法。如果validate()方法返回的ActionErrors对象中包含ActionMessage对象,说明表单验证失败,就把ActionErrors对象存储在request范围内,再把请求转发到ActionMapping的input属性指定的Web组件。如果ActionForm的validate()方法执行表单验证成功,就继续执行下一步请求处理流程。
(12) 调用processForward()方法,判断是否在ActionMapping中配置了forward属性。如果配置了这个属性,就调用RequestDispatcher的forward()方法,请求处理流程结束,否则继续下一步。
(13) 调用processInclude()方法,判断是否在ActionMapping中配置了include属性。如果配置了这个属性,就调用RequestDispatcher的include()方法,请求处理流程结束,否则继续下一步。
(14) 调用processActionCreate()方法,先判断是否在Action缓存中存在这个Action实例,如果不存在,就创建一个Action实例,把它保存在Action缓存中。
(15) 调用processActionPerform()方法,该方法再调用Action实例的execute()方法。execute()方法位于try/catch代码中,以便捕获异常。
(16) 调用processActionForward()方法,把Action的execute()方法返回的ActionForward对象作为参数传给它。processActionForward()根据ActionForward对象包含的请求转发信息来执行请求转发或重定向。
<!--[if !supportLists]-->(b) <!--[endif]-->扩展RequestProcessor类
开发人员可以很方便地创建客户化的RequestProcessor类。
在Struts配置文件中,<controller>元素的processorClass属性RequestProcessor类。
3 Action类
Action类是用户请求和业务逻辑之间的桥梁。每个Action充当客户的一项业务代理。在RequestProcessor类预处理请求时,在创建了Action的实例后,就调用自身的processActionPerform()方法,该方法再调用Action类的execute()方法。
Action的execute()方法调用模型的业务方法,完成用户请求的业务逻辑,然后根据执行结果把请求转发给其他合适的Web组件。
<!--[if !supportLists]-->(a) <!--[endif]-->Action类缓存
为了确保线程安全(thread-safe),在一个应用的生命周期中,Struts框架只会为每个Action类创建一个Action实例。所有的客户请求共享一个Action实例,并且所有请求线程可以同时执行它的execute()方法。
RequestProcessor类包含一个HashMap,作为存放所有Action实例的缓存,每个Action实例在缓存中存放的属性key为Action类名。在RequestProcessor类的processActionCreate()方法中,首先检查在HashMap中是否存在Action实例,如果存在,就返回这个实例;否则,就创建一个新的Action实例。创建Action实例的代码位于同步代码块中,以保证只有一个线程创建Action实例。一旦线程创建了Action实例并把它存放到HashMap中,以后所有的线程就会直接使用这个缓存中的实例。
<!--[if !supportLists]-->(b) <!--[endif]-->ActionForward类
Action类的execute()方法返回一个ActionForward对象。ActionForward对象代表了Web资源的逻辑抽象,这里的Web资源可以是JSP页、Java Servlet或Action。从execute()方法中返回ActionForward对象有两种方法:
·在execute()方法中动态创建一个ActionForward实例
·在Struts配置文件中配置<forward>元素。
配置了<forward>元素后,在Struts框架初始化时就会创建存放<forward>元素;
在execute()方法中只需调用ActionMapping实例的findForward()方法,来获取特定的ActionForward实例。findForward()方法先调用findForwardConfig()方法,在Action级别(即<action>元素内的<forward>子元素)寻找匹配的ActionForward对象。如果没有,再在全局级别(即<global-forwards>元素内的<forward>子元素)中寻找匹配的ActionForward对象。如果找到,就返回该ActionForward对象。如果findForward()方法没有找到匹配的ActionForward对象,它不会抛出异常,而是返回null。在浏览器端,用户将收到一个空白页。
采用第二种方式,无需在程序中硬编码来指定转发资源的无力路径,而是在配置文件中配置转发资源,程序中秩序引用转发资源的逻辑名即可,这提高了应用的灵活性和可维护性。
<!--[if !supportLists]-->(c) <!--[endif]-->创建支持多线程的Action类
在Struts应用的生命周期中,只会为每个Action类创建一个实例,所有的客户请求共享这个实例。因此,必需保证在多线程环境中,Action也能正常工作。保证线程安全的重要元素是在Action类中仅仅使用局部变量,谨慎地使用实例变量。
如果在Action的execute()方法中定义了局部变量,对于每个调用execute()方法的线程,Java虚拟机会在每个线程的堆栈中创建局部变量,因此每个线程拥有独立的局部变量,不会被其他线程共享。当线程执行完execute()方法时,它的局部变量就会被销毁。
如果在Action类中定义了实例变量,那么在Action实例的整个生命周期中,这个实例变量被所有的请求线程共享。因此不能在Action类中定义代表特定客户状态的实例变量。
在Action类中定义的实例变量代表了可以被所有请求线程访问的共享资源。为了避免共享资源的竞争,在必要的情况下,需要采用Java同步机制对访问共享资源的代码块进行同步。
(d)Action类的安全
在某些情况下,如果Action类执行的功能非常重要,则只允许具有特定权限的用户才能访问该Action。为了防止未授权的用户来访问Action,可以在配置Action时指定安全角色。
<action>元素的roles属性指定访问这个Action的用户必须具备的安全角色,多个角色之间以逗号隔开。
RequestProcessor类在预处理请求时会调用自身的processRoles()方法,该方法先检查在配置文件中是否未Action配置了安全角色,如果配置了安全角色,就调用HttpServletRequest的isUserInRole()方法,来判断用户是否具备了必要的安全角色。如果不具备,就直接向客户端返回错误。
二 使用内置的StrutsAction类
Struts提供了一些现成的Action类,在Struts应用中直接使用这些Action类可以大大节省开发时间。
1 org.apache.struts.actions.ForwardAction类
在JSP网页中,尽管可以直接通过<jsp:forward>标签把请求转发给其他Web组件,但是Struts框架提倡先把请求转发给控制器,再由控制器来负责请求转发。
有控制器来负责请求转发有以下一些优点:
·控制器具有预处理请求功能,它能够选择正确的子应用模块来处理请求,并且把子应用模块的ModuleConfig和MessageResources对象存放在request范围内。这样,请求转发的目标Web组件就可以正常地访问ModuleConfig和MessageResources对象。
·如果JSP页面中包含HTML表单,那么控制器能够创建和这个表单对应的ActionForm对象,把用户输入表单数据组装到ActionForm中。如果<action>元素的validate属性为true,那么还会调用ActionForm的表单验证方法。控制器把ActionForm对象存放在request或session范围内,这样请求转发的目标Web组件也可以访问ActionForm。
·JSP网页之间直接相互转发违背了MVC的分层原则,按照MVC设计思想,控制器负责处理所有请求,然后选择适当的视图组件返回给用户,如果直接让JSP相互调用,控制器就失去了流程控制作用。
对于用户自定义的Action类,既可以负责请求转发,还可以充当客户端的业务代理,如果仅仅需要Action类提供请求转发功能,则可以使用org.apache.struts.actions.ForwardAction类。ForwardAction类专门用于转发请求,不执行任何其他业务操作。
ActionServlet把请求转发给ForwardAction,ForwardAction再把请求转发给<action>元素中parameter属性指定的Web组件。总之,在Web组件之间通过ForwardAction类来进行请求转发,可以充分利用Struts控制器的预处理请求功能。此外,也可以通过<action>元素的forward属性来实现请求转发。
2 org.apache.struts.actions.IncludeAction类
在JSP网页中,尽管可以直接通过<include>指令包含另一个Web组件,但是Struts框架提倡先把请求转发给控制器,再由控制器来负责包含其他Web组件。IncludeAction类提供了包含其他Web组件的功能。与ForwardAction一样,Web组件通过IncludeAction类来包含另一个Web组件,可以充分利用Struts控制器的预处理功能。
<action>的paramter属性指定需要包含的Web组件。此外,也可以通过<action>元素的include属性来包含Web组件。
3 org.apache.struts.actions.DispatchAction类
通常,在一个Action类中,只能完成一种业务操作,如果希望在同一个Action类中完成一组相关的业务操作,可以使用DispatchAction类。
创建一个扩展DispatchAction类的子类,不必覆盖execute()方法,而是创建一些实现实际业务操作的方法,这些业务方法都应该和execute()方法具有同样的方法签名,即他们的参数和返回类型都应该相同,此外也应该声明抛出Exception。
在配置DispatchAction类时,需要把parameter属性设置为“method”。设置之后,当用户请求访问DispatchAction时,应该提供method请求参数。
4 org.apache.struts.actions.LookupDispatchAction类
LookupDispatchAction类是DispatchAction的子类,在LookupDispatchAction类中也可以定义多个业务方法。通常LookupDispatchAction主要应用于在一个表单中有多个提交按钮,而这些按钮又有一个共同的名字的场合,这些按钮的名字和具体的ActionMapping的parameter属性值相对应。
在Struts配置文件中配置LookUpDispatchAction:
在<action>元素中,设置parameter属性时,需要使它和<html:submit>标签的property属性保持一致。
5 org.apache.struts.actions.SwitchAction类
SwitchAction类用于子应用模块之间的切换。对于请求访问SwitchAction的URL,需要提供两个参数:
·prefix:指定子应用模块的前缀,以“/”开头,默认子应用模块的前缀为空字符串“”。
·page:指定被请求Web组件的URI,只需指定相对于被切换后的子应用模块的相对路径。
三 利用Token解决重复提交
在某些情况下,如果用户对同一个HTML表单多次提交,Web应用必需能够判断用户的重复提交行为,以作出相应的处理。
可以利用同步令牌(Token)机制来解决Web应用中重复提交的问题,Struts也给出了一个参考实现。org.apache.struuts.action.Action类中提供了一系列和Token相关的方法:
·protected boolean isTokenValid(javax.servlet.http.HttpServletRequestrequest)
判断存储在当前用户会话中的令牌值和请求参数中的令牌值是否匹配。如果匹配,就返回true,否则返回false。只要符合一下情况之一,就返回false:
·不能存在HttpSession对象。
·在session范围内没有保存令牌值。
·在请求参数中没有令牌值
·存储在当前用户session范围内的令牌值和请求参数中的令牌值不匹配。
·protected voidresetToken(javax.servlet.http.HttpServletRequest request)
从当前session范围内删除令牌属性。
·protected voidsaveToken(javax.servlet.http.HttpServletRequest request)
创建一个新的令牌,并把它保存在当前session范围内。如果HttpSession对象不存在,就首先创建一个HttpSession对象。
具体的Token处理逻辑由org.apache.struts.util.TokenProcessor类来完成,它的generateToken(request)方法根据用户会话ID和当前系统时间来生成一个唯一的令牌。
四 实用类
在创建Web应用时,有许多检索和处理HTTP请求的操作时重复的。为了提高应用代码的可重用性,减少冗余,Struts框架提供了一组提供这些通用功能的实用类,它们可以被所有的Struts应用共享。
1 RequestUtils类
org.apache.struts.util.RequestUtils为Struts控制框架提供了一些处理请求的通用方法。RequestUtils类中的所有方法都是线程安全的,在这个类中没有定义任何实例变量,所有的方法都被声明为static类型。因此,不必创建RequestUtils类的实例,可以直接通过类名来访问这些方法。
RequestUtils类的常用方法
方法 | 描述 |
absoluteURL(HttpServletRequest request,String utl) | 创建并返回绝对URL路径,参数path指定相对于上下文(context-relative)的相对路径 |
createActionForm(HttpServletRequest request, ActionMapping mapping, ModuleConfig moduleConfig, ActionServlet servlet) | 先从request或session范围内查找该ActionForm,如果存在,就直接将它返回,否则先创建它的实例,把它保存在request或session范围内,再把它返回。mapping参数包含了<action>元素的配置信息,例如它的scope属性指定ActionForm的范围 |
populate(Object bean, HttpServletRequest request) | 把HTTP请求中的参数值组装到指定的JavaBean中,请求的参数名和JavaBean的属性名匹配。当ActionServlet把用户输入的表单数据组装到ActionForm中时,就调用此方法 |
2 TagUtils类
org.apache.struts.taglib.TagUtil类为JSP标签处理类提供了许多实例方法,如果要使用TagUtils类,首先应调用TagUtils.getInstance()方法,获得TagUtils类的实例,getInstance()方法为静态方法。
TagUtils类的常用方法
方法 | 描述 |
getInstance() | 返回一个TagUtils的实例。该方法为静态的,如果要在程序中获得TagUtils的实例,可以调用TagUtils.getInstance()方法 |
getActionMessages(PageContext pageContext, String paramName) | 调用pageContext.findAttribute(paramName)方法,从page, request, session和application范围内减缩并返回ActionMessages对象,参数paramName指定检索ActionMessages对象的属性key |
getModuleConfig(PageContext pageContext) | 返回ModuleConfig对象,如果不存在,就返回null |
lookup(PageContext pageContext, String name, String scope) | 返回特定范围内的JavaBean。参数scope指定JavaBean的所在范围,name参数指定JavaBean在特定范围内的名字 |
message(PageContext pageContext, String bundle, String locale, String key) | 从指定的Resource Bundle中返回一条消息文本,参数locale指定Locale,参数key指定消息key |
write(PageContext pageContext, String text) | 向网页上输入特定的文本,参数text用于指定文本内容 |
3 ModuleUtils类
org.apache.struts.taglib.ModuleUtils类提供了处理子应用模块的实用方法,如果要使用ModuleUtils类,首先应该调用ModuleUtils.getInstance()方法,获得ModuleUtils类的实例,getInstance()方法为静态方法。
ModuleUtils类的常用方法
方法 | 描述 |
getInstance() | 返回一个ModuleUtils的实例。该方法为静态的,如果要在程序中获得ModuleUtils的实例,可以调用ModuleUtils.getInstance()方法。 |
getModuleConfig(javax.servlet.http.HttpervletRequest request) | 从request范围内检索并返回ModuleConfig对象 |
getModuleConfig(java.lang.String prefix, javax.servlet.ServletContext context) | 从application范围内检索并返回ModuleConfig对象,参数prefix指定子应用模块名的前缀 |
getModuleName(javax.servlet.http.HttpServletRequest request, javax.servlet.ServletContext context) | 返回请求访问的子应用模块的名字 |
selectModule(javax.servlet.http.HttpServletRequest request, javax.servlet.ServletContext context) | 选择请求访问的子应用模块,把和子应用模块相关的ModuleConfig和MessageResources对象存储到request范围中 |
4 Globals类
org.apache.struts.Globals类提供一组公共类型的静态常量,被用作在特定范围内存放JavaBean的属性key。
Globals类中定义的常量
方法 | 描述 |
ACTION_SERVLET_KEY | 代表在application范围内存放ActionServlet实例的属性key |
DATA_SOURCE_KEY | 代表在application范围内存放默认的DataSource实例的属性key |
ERROR_KEY | 代表在request范围内存放ActionErrors实例的属性key |
LOCALE_KEY | 代表在session范围内存放Locale实例的属性key |
MAPPING_KEY | 代表在request范围内存放ActionMapping实例的属性key |
MESSAGE_KEY | 代表在request范围内存放ActionMessages实例的属性key |
MESSAGES_KEY | 代表在application范围内存放各个子应用模块的MessageResources实例的属性key的前缀 |
MODULE_KEY | 代表在application范围内存放各个子应用模块的ModuleConfig实例的属性key的前缀 |
REQUEST_PROCESSOR_KEY | 代表在application范围内存放各个子应用模块的RequestProcessor实例的属性key的前缀 |
阅读材料:《精通Struts:基于MVC的Java Web设计与开发》