我们已经从较高层解释了整个框架的结构,请求流程的基础,配置方式和Struts2和Struts1的不同之处。了解这些后从Struts 应用 迁移到 Struts 2 不再是难事。
- Q7 `6 n3 S8 j/ K0 v忽悠,忽悠社区,忽悠论坛. 在这篇文章中,我们将会更详细地讲述如何由Struts 的action转为Struts 2的action。
! i4 f* J% a$ x& J
0 `& x/ W1 w! A \5 r! O+ p. B一个应用的例子www.huuoo.com$ D: k0 S; D) X
这个例子选择了大家都熟悉的 - weblog. 简单地介绍下这例子的功能需求:( l$ W i# B. |' S
- 增加一个新的日志
- 察看一个日志
- 修改一个日志
- 删除一个日志
- 列出所有日至
增删修改(CRUD),是项目中最为普遍的应用。
2 q0 y6 A9 j2 i8 z# G3 `9 X忽悠社区 业务逻辑类在Struts 和 Struts2 应用都是可共用的。如:- public class BlogService{
- private static List<Blog> blogs = new ArrayList<Blog>();
- public List<Blog> list() { ... }
- public Blog create(Blog blog) { ... }
- public void update(Blog blog) { ... }
- public void delete(int id){ ... }
- public Blog findById(int id){ ... }
- }
复制代码 BlogService 只是个简单的业务逻辑类,并不是接口,Struts 和 Struts2 的action皆可调用其实例。虽然这样设计在实际项目中会带来不必要的耦合,但我们的例子只是集中在讨论web层上,所以无关重要。
& Z1 x) q& U8 @* k9 p忽悠社区是综合性社区网站,将最新、最快、最专业的资讯、新闻,图片,视频奉献给所有爱好者。
# [9 F" n/ v0 T& w" o- W忽悠,忽悠社区,忽悠论坛.工具箱: 在第一篇文章中,我们谈论了在Struts2 actions中的依赖注入的接口注入方式。这个是servlet 相关类(HttpServletRequest, HttpServletResponse, PrincipalProxy, 等.)的主要注入方式,但这并不是唯一的方式。
" p p. `+ y% S. R+ ?0 r! b# W
: ?6 u M7 J3 l) P忽悠社区是综合性社区网站,将最新、最快、最专业的资讯、新闻,图片,视频奉献给所有爱好者。Struts2 可以使用Spring框架作为默认的容器时,依赖注入的setter方法就可用了。通过在action中加入setter方法(如下演示), Struts2 框架将能从Spring框架中获得正确的信息,并通过setter加载在action中。- public void setBlogService(BlogService service){
- this.blogService = service;
- }
复制代码 和接口注入方式类似,我们需要一个拦截器来帮助我们完成任务,这就是 ActionAutowiringInterceptor 拦截器。这样我们的业务逻辑类就通过Spring框架管理自动在action被调用之前注入到Struts2得action中。有多种的配置参数(如by name, by type 或 automatically)可供选择,可以让对象和setter匹配的注入的方式根据你的需要而定。
6 d z: k% A: h! z: T' }4 F: \) k! R/ a$ q) w j+ _6 U
Struts 应用中的代码忽悠社区& l( {$ ^8 d" g' W8 d5 M
我们首先从Struts讲起。在Struts中,最普遍的做法是,对于每个需求用例(如save,update,remove,list)来说都会有对应的action类,同时也会有相应的action form类。在我们的应用中的这个方式或许不是最佳的实现方式(其他的解决方案包括使用dynamic form或者使用request来分发action),但我们例子中的做法是所有Struts开发者最熟悉的一种形式。了解了这种简单的实现方法,你有能力在迁移到Struts2时,使用更为优秀的方法。
' {, z; J% m# x5 e8 ]+ a- s在第一篇文章中我们谈过Struts 和 Struts2 中action的区别。现在我们从UML中再次看看他们的差别。一般来说form在Struts action中的表现形式是:www.huuoo.com' Q+ f' x3 n9 U7 E# X7 C
忽悠社区是综合性社区网站,将最新、最快、最专业的资讯、新闻,图片,视频奉献给所有爱好者。1 `" ~/ L4 D& c+ c8 [( d
/ u0 m, J; F" S: h1 c" M
+ E! ~2 e2 k3 J6 m0 Fwww.huuoo.com这action form将会在多个action中使用,让我们来看看它:- public class BlogForm extends ActionForm {
- private String id;
- private String title;
- private String entry;
- private String created;
- // public setters and getters for all properties
- }
复制代码 如UML中展示的那样,其中一个限制就是必须继承ActionForm类,另外一个限制就是form中所有属性都必须是String类型,所以所有的getter和setter都必须只能接受String参数和返回String结果。' ?% `! O Z& n+ R1 \
- J: p1 I; D" P/ A0 Q1 y8 n& j忽悠社区是综合性社区网站,将最新、最快、最专业的资讯、新闻,图片,视频奉献给所有爱好者。然后我们来看看action。我们这个例子中的action有view, create 和 update action。忽悠,忽悠社区,忽悠论坛.9 M1 G4 s( M" B+ m B4 h% z% x
The View Action:www.huuoo.com+ e8 {6 _; g$ z1 a0 w
The Create Action:- public class ViewBlogAction extends Action{
- public ActionForward execute(ActionMapping mapping,
- ActionForm form,
- HttpServletRequest request,
- HttpServletResponse response)
- throws Exception {
- BlogService service = new BlogService();
- String id = request.getParameter("id");
- request.setAttribute("blog",service.findById(Integer.parseInt(id)));
- return (mapping.findForward("success"));
- }
- }
复制代码- public class SaveBlogEntryAction extends Action {
- public ActionForward execute(ActionMapping mapping,
- ActionForm form,
- HttpServletRequest request,
- HttpServletResponse response)
- throws Exception{
- BlogService service = new BlogService();
- BlogForm blogForm = (BlogForm) form;
- Blog blog = new Blog();
- BeanUtils.copyProperties( blog, blogForm );
- service.create( blog );
- return (mapping.findForward("success"));
- }
- }
复制代码 The Update Action:- public class UpdateBlogEntryAction extends Action{
- public ActionForward execute(ActionMapping mapping,
- ActionForm form,
- HttpServletRequest request,
- HttpServletResponse response)
- throws Exception{
- BlogService service = new BlogService();
- BlogForm blogForm = (BlogForm) form;
- Blog blog = service.findById( Integer.parseInt(blogForm.getId()));
- BeanUtils.copyProperties( blog, blogForm );
- service.update( blog );
- request.setAttribute("blog",blog);
- return (mapping.findForward("success"));
- }
- }
复制代码 这三个action都跟随着同一个模式: 忽悠社区$ i1 E" G! D% O2 r3 P* h7 c
忽悠社区是综合性社区网站,将最新、最快、最专业的资讯、新闻,图片,视频奉献给所有爱好者。4 L: N6 O% M% E) u
- 产生一个新的业务逻辑对象实例 - 如前面所提到的,我们使用最直接的方式在action中使用业务逻辑对象,这表示在每个action中都会产生新的业务逻辑对象实例。
- 从请求中获得数据 - 这是两种形式之一。在view action中,"id"是从HttpServletRequest 对象中直接获取的。而在create 和 update action 中,则从ActionForm 中取值。ActionForm 与 HttpServletRequest 的调用方式其实很相似,唯一不同的ActionForm 是bean的从field中取值。
- 调用业务逻辑- 现在开始生成调用业务逻辑所需的参数并调用逻辑。如果参数(在view action中)是一个简单对象类型,则转换值时会自动转为正确的类型(如从String转到Integer)。如果参数是复杂的对象类型,,则ActionForm 需要通过BeanUtil 来帮忙转成相应的对象。
- 设定返回的数据 - 如果需要把数据返回显示给用户,那则要把这个数据设在HttpServletRequest 的attribute 中返回。
- 返回一个 ActionForward - 所有 Struts action的最后都需要找到并返回其相应的 ActionForward 对象.
最后的两个action,remove和list action, 只有很少的差别。remove action如下所示,没有用BlogForm类. 通过从request的attribute中获取"id"(和view action相似),就能调用业务逻辑完成其需要的工作。在下面我们介绍配置时,你可以看到它并没有返回任何数据,因为它的"success"返回结果其实是执行remove后再执行了list action来返回信息的。- public class RemoveBlogEntryAction extends Action {
- public ActionForward execute(ActionMapping mapping,
- ActionForm form,
- HttpServletRequest request,
- HttpServletResponse response)
- throws Exception {
- BlogService service = new BlogService();
- String id = request.getParameter("id");
- service.delete(Integer.parseInt(id));
- return (mapping.findForward("success"));
- }
- }
复制代码 list action并不需要任何的用户输入,它只是简单地调用了业务逻辑的无参方法,同时返回所有的Blog对象。- public class ListBlogsAction extends Action {
- public ActionForward execute(ActionMapping mapping,
- ActionForm form,
- HttpServletRequest request,
- HttpServletResponse response)
- throws Exception{
- BlogService service = new BlogService();
- request.setAttribute("bloglist",service.list());
- return (mapping.findForward("success"));
- }
- }
复制代码 向 Struts2 迁移忽悠社区; a, j! z7 K+ }- A( V9 k; n; o
在Struts2中,可选的实现方式有很多,可以像Struts那样每个需求用例对应一个action,也可以用一个action对应所有需求用例。但在我们的例子中,使用的方法是我认为最佳的解决方案 - 在一个action类中实现整套CRUD功能。! j$ ?) A$ I# f$ V7 i6 r
也许你人为把list需求用例也同样地整合到同一个action类里会比较好,而我认为把list的功能分到另外一个action中,会减少容易产生的混淆,因为list用例中并不需要Blog这个类作为属性,而在其他用例中则需要。3 f# N- r# s# N. U8 D
. l8 h8 y2 ^- [$ Z% [/ H7 k对于 Struts2的例子, 它的UML模型展示如下:
: t7 E4 u0 u0 |7 A+ M+ I( m& M8 t忽悠,忽悠社区,忽悠论坛.
% l5 f9 ^0 z1 z5 z! f7 ]6 a1 x0 y& h* J5 J+ I/ i+ i6 e
7 D. K: J8 ?; O$ N7 q$ Q
( g$ G Z1 d4 j: t* w) E忽悠社区 每个用例在action中都有自己所对应的方法。从上图中我们可以看到,在BlogAction 中我们有save, update 和 remove三个方法。而ListBlogAction中,没有list这个方法,因为ListBlogAction继承了ActionSupport 类,实际上就是在默认的execute 方法中实现list功能。www.huuoo.com# _- M+ |3 i. h* d% x" I( R% o
为了更容易看,图中的BlogAction并没有画出它所实现了的三个接口。它们分别是ServletRequestAware 接口, Prepareable 接口和 ModelDriven 接口。忽悠社区是综合性社区网站,将最新、最快、最专业的资讯、新闻,图片,视频奉献给所有爱好者。7 _; U0 j5 k' g+ A" o0 i
首先回顾一下ServletRequestAware, 我们在第一篇文章中已经详细介绍它了。这个ParametersInterceptor 拦截器提供了把HttpServletRequest 自动set到action中的功能,让我们能通过request, 把所需的值传回到JSPs。忽悠,忽悠社区,忽悠论坛.2 L! @! z3 g' v0 u0 n7 H
接着看看Preparable 接口, 它会联合PrepareInterceptor拦截器一起工作,让action在执行execute() 方法前, 执行一个prepare()方法,实现在执行前设定,配置或预设一些值在action中。 在我们的例子里,prepare方法会检查blogId 属性,如果为零则这是一个新日志,非零则该日志已经存在,根据blogId取出日志。
; P/ Y* f( a# J( W( N9 U1 F忽悠,忽悠社区,忽悠论坛. 最后我们说说ModelDriven 接口,在上一篇文章中,我们已经了解到 Struts action的很大的不同在于它是需要线程安全的,而在Struts2中则没有这个限制,因为每次的请求都会有一次action对象的初始化和调用。没有了这个限制,能允许Struts2使用类级别的属性变量(特别是getters和setters),从而获得更多编码优势。! p7 Q8 p3 D( @3 L3 I' w
' j+ D% O9 H2 k5 P和拦截器的功能结合起来, 把HttpServletRequest 中的attribute 注入action中的流程如下所示:忽悠社区( _7 w2 {# s9 Y' }8 o! B' q# }9 u
7 M" e& L7 P( ^0 ?- X) R1 i: o" M- 循环读取HTTP request中的attribute
- 查找当前request attribute中是否有和action中的setter中的属性匹配的
- 有则根据attribute从HttpServletRequest 里取出其值
- 把取出来的值由String转成setter中相应的类型
- 调用setter把该转换后的值注入action中
提示: 当调用action时,如果发现不明原因使不能正确地通过setter注入值情况下,第一步最好是先检查下各拦截器,确保它们都已作用于该action。因为这些意外通常有时由拦截器设置不当形成的,检查是否各个拦截器都已起作用,并看看它们作用的顺序,因为有些情况下它们间会相互影响而产生错误。 现在我们已经有基于String类型的form bean中取值的方法或者是自动把request的attributes 注入到action的方法,那下一步就是如何把值传入 domain object 或 value / transfer object的属性中去。其实这很简单,你只需要实现ModelDriven 接口(即实现getModel()方法)就可以了,确保ModelDrivenInterceptor 拦截器已作用于action。
* @+ b$ x( C: M$ _& w8 o$ Jwww.huuoo.com 除了会调用action中的setter外,model 首先检查是否有和setter可以匹配当前的attribute名。如果在model中没有这个attribute相应的setter,则会再在action上找相应的setter来设值。忽悠社区3 ]0 L9 A K6 L) H( c0 T
在BlogAction 的例子中我们可以看到如何很灵活地使用这些方法,首先通过prepare() 方法根据Id获取相应的 Blog model object 或新建一个instance, 然后再根据把request中相应的属性注入Blog instance中和action中。
A9 ?8 _& T! t3 vwww.huuoo.com 以上的两个功能使得现在调用action那么简单 - 调用具体的业务逻辑,和把数据设在HttpServletRequest供返回用。- public class BlogAction extends ActionSupport
- implements ModelDriven, Preparable, ServletRequestAware{
- private int blogId;
- private Blog blog;
- private BlogService service = new BlogService();
- private HttpServletRequest request;
- public void setServletRequest(HttpServletRequest httpServletRequest){
- this.request = httpServletRequest;
- }
- public void setId(int blogId){
- this.blogId = blogId;
- }
- public void prepare() throws Exception{
- if( blogId==0 ){
- blog = new Blog();
- } else{
- blog = service.findById(blogId);
- }
- }
- public Object getModel() {
- return blog;
- }
- public String save(){
- service.create(blog);
- return SUCCESS;
- }
- public String update() {
- service.update(blog);
- request.setAttribute("blog",blog);
- return SUCCESS;
- }
- public String remove() {
- service.delete(blogId);
- return SUCCESS;
- }
- public String execute() {
- request.setAttribute("blog",blog);
- return SUCCESS;
- }
- }
复制代码 最后就是说说 list这个用例了。它同样需要访问HttpServletRequest对象去返回数据给JSP,所以也需要实现ServletRequestAware 接口。但是,因为它并不需要任何输入值,所以就不需要实现其他的接口了。以下是它的具体实现:- public class ListBlogsAction extends ActionSupport implements ServletRequestAware{
- private BlogService service = new BlogService();
- private HttpServletRequest request;
- public void setServletRequest(HttpServletRequest httpServletRequest){
- this.request = httpServletRequest;
- }
- public String execute() ...{
- request.setAttribute("bloglist",service.list());
- return SUCCESS;
- }
- }
复制代码 这样就完成了我们该实现的action代码了。 在下一篇文章中,当我们新的Struts2用户界面结合时,我们还会进一步简化action的代码。
! ~! E3 x2 H4 E# ^( U# b3 Q忽悠社区
. _- P) i( F' N! F* o
% h/ ^9 m! ~3 S" F! I忽悠,忽悠社区,忽悠论坛.配置Actions忽悠社区是综合性社区网站,将最新、最快、最专业的资讯、新闻,图片,视频奉献给所有爱好者。9 g, I$ i1 j; r1 _
在我们调用action之前,我们必须通过XML配置文件去配置它们。
, q- W* o: g% j5 y" x/ g, B& Z, H忽悠社区 在Struts中, 我们习惯用在WEB-INF 目录的"struts-config.xml"配置文件,在这里我们需要配置action form和action属性。在Struts2中, 用的是在classpath中的"struts.xml"配置文件, 它看起来好象会稍微复杂一些,因为它需要在配置action的同时也配置其拦截器。忽悠社区1 |! o. T1 x$ W
忽悠社区是综合性社区网站,将最新、最快、最专业的资讯、新闻,图片,视频奉献给所有爱好者。6 q6 r8 d0 \! X
在Struts中配置 form-beans 节点很容易, 只需要一个唯一的名字,还有就是继承ActionForm类的class作为type。- <struts-config>
- <form-beans>
- <form-bean name="blogForm" type="com.fdar.articles.infoq.conversion.struts.BlogForm"/>
- </form-beans>
- ...
- </struts-config>
复制代码 在我们的例子中,我们的配置文件有3点不相同:忽悠社区是综合性社区网站,将最新、最快、最专业的资讯、新闻,图片,视频奉献给所有爱好者。$ [7 F/ w* J& Y! D5 h
1. 重定向配置www.huuoo.com3 y% P/ b* Y/ ^. A( U
在Struts的配置中,每个mapping 都需要提供调用action时所需要对应的路径,Struts默认为".do", 例如paht是"/struts/add"对应于URL"/struts/add.do"。同时也需要一个forward 属性来提供给URL去转向,如"/struts/add.jsp".- <struts-config>
- ...
- <action-mappings>
- <action path="/struts/add" forward="/struts/add.jsp"/>
- ...
- </action-mappings>
- </struts-config>
复制代码 而Struts2的需要更多的一些配置,如:- <struts>
- <include file="struts-default.xml"/>
- <package name="struts2" extends="struts-default" namespace="/struts2">
- <action name="add" >
- <result>/struts2/add.jsp</result>
- </action>
- ...
- </package>
- </struts>
复制代码 首先你会注意到的是,代替action-mappings 节点的是include 和package 节点。Struts2可以把配置细分到任意数目的配置文件中,来实现配置可模块化管理。每个配置文件的结构其实都是一样的,不同的只是文件名。
. ]; |: Z/ z( }) X% m( [www.huuoo.com include 节点中,以文件名作为file 属性,可把所include的文件内容包含到当前文件中。
9 Y, j- S' }- j$ j, vwww.huuoo.com package 节点把actions组成一组,其name 属性的值必须是唯一的。www.huuoo.com0 V6 k4 _. @- m R$ G
在 Struts action的配置中, paht属性需要指定完整的URL路径。而在Struts2中,URL是通过package节点中的namespace属性,还有在action 节点中的name 属性, 和action扩展(默认是".action")共同起作用的。在上面的例子中,则URL为"/struts2/add.action"时会调用action。. S/ L# k0 W- L; E
package节点除了可以分离命名空间外, package 节点中的 extends 属性,还提供了某种可复合的组成结构。通过继承另外一个package节点,你就能继承那个节点的配置,包括其actions, results, interceptors, exception,等值。在我们的例子中,"struts2" package节点继承了 "struts-default" package 节点(在"struts-default.xml" 文件里定义了该节点) ,注意这个是主要的include文件,所以必须在所有配置之前的第一行中写出。 这个功能有助于大大减少你重复性输入默认配置所浪费的时间。忽悠社区1 H7 T- Y2 m$ t4 k
最后是result 节点, 它只是存放你这个action所需要转向的URL. 在这里我们没有提及name 和 type 属性。如果你不想改变它们的默认属性的话,你能忽略不写它们,让你的配置文件看起来更清晰。从action返回的 "success" 的结果将组成这个JSP显示给用户。忽悠,忽悠社区,忽悠论坛.9 [% x" r% G) W! c4 u5 _6 w
U' F- V; o: d0 w忽悠社区是综合性社区网站,将最新、最快、最专业的资讯、新闻,图片,视频奉献给所有爱好者。忽悠,忽悠社区,忽悠论坛.* Y* J# g$ D- R3 d/ [% O, }
2. Action 配置忽悠社区是综合性社区网站,将最新、最快、最专业的资讯、新闻,图片,视频奉献给所有爱好者。: R$ \% a4 B0 s; Z$ U" |
在Struts 中forward 节点指定了action处理后,结果将重定向到哪个相应的页面。type属性指定了action的类,scope 属性保证了form beans只在request范围内。- <struts-config>
- ...
- <action-mappings>
- <action path="/struts/list" scope="request"
- type="com.fdar.articles.infoq.conversion.struts.ListBlogsAction" >
- <forward name="success" path="/struts/list.jsp"/>
- </action>
- ...
- </action-mappings>
- </struts-config>
复制代码 Struts2 的 XML配置和上面提到的基本相同。唯一不同的就是通过class属性为action节点提供了它所需要调用的类的完整路径- <struts>
- ...
- <package name="struts2" extends="struts-default" namespace="/struts2">
- <default-interceptor-ref name="defaultStack"/>
- <action name="list" class="com.fdar.articles.infoq.conversion.struts2.ListBlogsAction">
- <result>/struts2/list.jsp</result>
- <interceptor-ref name="basicStack"/>
- </action>
- ...
- </package>
- </struts>
复制代码 如果是用其他的方法而不是用默认的execute 方法去调用action(在BlogAction 类中大多数方法如此), 则需要在action节点的 method 属性里加入方法名,下面就是个例子,这时候update方法将会被调用。- <action name="update" method="update" class="com.fdar.articles.infoq.conversion.struts2.BlogAction" >
- ...
- </action>
复制代码 default-interceptor-ref 和 interceptor-ref 节点有几点不同。在第一篇文章中,我们看到在action被调用之前必须通过一系列的拦截器,而这两个节点就是用来配置拦截器组的。default-interceptor-ref 节点为该package提供了默认的拦截器组。当在action节点中提供 interceptor-ref节点时 ,它就会覆盖默认的拦截器(interceptor-ref 节点能够和单独一个拦截器相关联,或者跟一个拦截器组相关联),在action节点中可以存在多个interceptor-ref节点,处理拦截器组的顺序会和该节点列出的顺序一致。
) O% k8 m& _; A9 B2 j. hwww.huuoo.com
$ x8 e! `. u5 c" Q% o+ v6 k
! W- x2 o8 f. awww.huuoo.com3. 再重定向配置
9 b4 C$ Z: y9 k' M& a. Z2 r 当我们提交表格的时候,我们需要重定向到更新后的结果页面。这个通常称为 "post-redirect pattern" 或, 最近出现的, "flash scope."( \8 C3 [2 z$ C1 W/ _: X' l
由于这是一个form, 所以在Struts中我们需要为Struts指定一个ActionForm。需要在name属性中提供form的名称,同样地,我们也需要在forward 节点中举加入redirect属性为true。- <struts-config>
- ...
- <action-mappings>
- <action path="/struts/save" type="com.fdar.articles.infoq.conversion.struts.SaveBlogEntryAction"
- name="blogForm" scope="request">
- <forward name="success" redirect="true" path="/struts/list.do"/>
- </action>
- ...
- </action-mappings>
- </struts-config>
复制代码 Struts2 在result 节点里提供了type 属性, 默认情况下是"dispatch", 如果需要重定向,则需要设为 "redirect"。- <struts>
- ...
- <package name="struts2" extends="struts-default" namespace="/struts2">
- <action name="save" method="save" class="com.fdar.articles.infoq.conversion.struts2.BlogAction" >
- <result type="redirect">list.action</result>
- <interceptor-ref name="defaultStack"/>
- </action>
- ...
- </package>
- </struts>
复制代码 总结www.huuoo.com6 S: y0 q: B3 P2 t0 v
我们并不可能在这篇文章中覆盖所有的内容,如果你需要更好的了解整个框架,还有其他的实现方式和选项,这里有几点可以供你参考:0 O2 {5 Q5 j* F
5 ^8 ?- w6 `8 M2 e, J
忽悠社区& A; M/ y8 a3 r! H: {
- 配置拦截器和拦截器组 - 以Struts2-core JAR 包里的"struts-default.xml" 文件作为例子。"struts-default.xml" 演示了如何配置你自己的拦截器组,包含新的拦截器,你可以尝试实现自己的拦截器。
- 配置文件中的通配符模式 - 你可以选择使用Struts2中的通配符模式来简化你的配置。
- 通过 ParameterAware 接口把form值传入maps中 - 你可以在Struct2中配置,让所有request的form属性都存于action的一个map中,这样就不需要专门再为action指定model / transfer / value object了。这和Struts的dynamic form特点很相似。
也许到现在为,也许你有个疑问,"迁移后我们的界面是否可以完全重用呢?",答案是yes。你能从这里, 下载到我这篇文章中的完整源代码,你可以自己尝试把URL的扩展名由".do" 改为 ".action",使用的页面时一样的。除此之外,其实用JSTL来代替Struts taglib也是很容易的。5 `4 ^2 c8 e8 N+ ] i$ ?
在下一篇文章中,我们将讲述用户界面,讨论themes 和 tags; 如何做validation; 如何重用UI控件。 |