这一章,大象将详细分析web层代码,以及struts2的注解插件——struts2-convention的用法和其它相关知识。
    第四部分:透析控制层
    上一章对dao、entity、service三层进行了详细的分析,并对代码进行了测试。测试结果表明这部分功能没问题,可以正常使用。本章将对最后一个web层进行详细说明,尽可能的讲明白这些知识要点。
    数据库
    本例使用MySQL数据库,只有三张表,一张用于管理表主键的generator_table,另外两张是人员表与角色表。
    
    
    
    这里我有一点需要说明一下,在学习JPA——Hibernate Annotation使用实例一文中,我将generator_table设了一个id主键字段,其实这个字段是不需要的,直接将g_key设为主键。这样设计更好些,因为表名不可能一样,所以这个存放各个表主键的键名也不会一样。
    user与role这两张表只设了一个主键,没有建立外键关联,而且大象也很反对建立表之间的外键关联。因为这样做之后,约束太多,在实际开发中,很容易出问题,这是我亲身体会过的。所以我建议只对表设置一个流水号主键,其它的都可以根据业务关系来设计字段,这样会更灵活。
    这里对各个字段都默认将它们设置为null,因为针对不同的表,你都会实现相应的功能,你当然会知道哪些字段是不能为空的,哪些是可以为空的。而且在做数据库设计的时候,你也不可能在短时间内,面面俱到的把所有问题都考虑进去,根据需求的变化,在开发过程中,也是经常会遇到修改数据库的情况。如果之前过于强调字段的非空设置,在编写代码时,为了减少出错,脑袋里可能会不停的想,啊,这个字段是非空的吗?哪个字段不是非空的吧?然后反复对比数据库进行检查,会使人束手束脚很不舒服。因为这些全部都可以人为来控制,所以除了主键外,将其它字段都设为null有利于开发人员更好的进行工作。
    有人会说了,进行非空设置是一种约束,当程序出错时,很容易发现问题。当然,这话说得没错。大象只是建议,从没说过一定要这样做,我只是说下自己的一点经验总结,仅此而已!想怎么实现都是你的自由。
    struts2-convention
    既然说了是全注解开发,而且我们已经实现了Hibernate与Spring的注解。同样的,Struts2也能够做到用注解来代替配置文件,struts2-convention插件可以帮助我们完成这一功能。它是struts2提供的一个插件,目前网上相关的中文文档主要是一个叫石太洋的人根据官方文档翻译的,很多网站与博客都有转载。我看了原文与译文,感觉讲的不够清楚,例子也很简单。大象根据自己在项目中的实际使用情况,现将个人对这个插件的经验总结写出来与各位分享,希望与大家多交流,共同提高。
    官方文档 https://cwiki.apache.org/WW/convention-plugin.html
    请不要把地址中的两个大写W换成小写,否则是打不开页面滴!这个插件的使用其实非常简单,如果光看文档可能会觉得好像很麻烦。那么大象来告诉你怎样快速学习这个插件。
    首先你要搞清楚,这个插件它会默认扫描所有包名为struts、struts2、action、actions下面的类。然后它会对实现了Action接口以及类名以Action结尾的这些类,作为Action来进行处理。
    你可以重新定义按哪种包名进行扫描。比如本例设定,只扫描web包下面的所有类,因为我们将Action类都放在这个包下面。
    那这个插件是怎么实现原来的配置信息的呢?它的映射规则是这样的,对于以Action结尾的的类,去掉Action,取剩下的部分,将所有的字母转换为小写,如果有驼峰式的写法,则用"-"连接符来连接不同的单词,这是此插件的默认方式。最终转换之后的就是请求地址,还是用例子说明。
    com.bolo.examples.web.base.UserAction    
    按照上面的规则,请求地址就应该是UserAction去掉Action后缀,将其余部分转换为小写,所以user就是我们的请求地址。不过,这还没有完,因为这里面还有一个命名空间的路径,在通常的配置文件中,一般会将不同的功能进行划分,在package标签里加上namespace属性。使用这个插件,它会为你自动配上命名空间,默认的就是前面说到的以那四种名称为根目录的命名空间,它们之后的都将成为命名空间的名称。
    com.bolo.examples.struts.UserAction 映射为 /user.action
    com.bolo.examples.struts.base.UserAction 映射为 /base/user.action
    要是我们不以struts或其它几种默认值为包名,又该怎么办呢?没关系,插件为我们提供了一种自定义根包的配置方式
    <constant name="struts.convention.package.locators" value="web" />
    上面这段配置是写在struts.xml里面的,它指定web为根,作用就相当于那四种默认值。
    com.bolo.examples.web.base.UserAction映射为 /base/user.action
    com.bolo.examples.web.HelloAction 映射为 /hello.action
    com.bolo.examples.web.HelloWorldAction 映射为 /hello-world.action
    请一定注意驼峰写法的映射方式,假如这里不是HelloWorld,而是Helloworld,那就不会再是hello-world.action,而是helloworld.action了。
    既然已经知道了它的映射方式,接下来再看看这个插件是如何定义结果页面的。
    convention默认会到/WEB-INF/content文件夹下面查找对应的结果页面,这个文件夹的名字可以修改,需要在struts.xml中定义
    <constant name="struts.convention.result.path" value="/WEB-INF/jsp" />
    文件夹的名字改成了jsp,这样定义后,convention就会在这个文件夹下面查找结果页面。它的查找路径与映射的命名空间有关。默认规则是,在请求的命名空间下面,根据请求名称再结合方法返回的字符串生成最终的结果页面名称,再配以后缀名。convention支持以jsp、ftl、vm、html、htm等五种后缀格式的文件。这里有个比较特殊的是如果方法返回success,那么可以不用将它与请求名称拼接起来,直接使用请求名称作为返回页面的名称。还是举例子说明。
    
    比如上面这段代码,HelloAction处于我们定义的根包(web)下面,因此,它的action请求为hello.action。这时,会默认执行execute()方法,由于返回的是success字符串,所以页面的名称可以简写为hello.jsp,但是当执行welcome方法时,由于返回的字符串为welcome,这时的页面名称则为hello-welcome.jsp。convention就是遵循这样的规则来进行命名,当然这只是最基本的,我们再来看看稍微复杂点的东东。
    
    这个RoleAction类的外部,加了两种注解,它们的作用相当于配置文件中的result标签。Results是一个Result类型的数组注解,里面可以包含多个Result配置。使用Result注解来设置返回类型与返回页面,是不准备采取默认的定义方式。比如HelloAction就是我们采取的默认方式。另外对于有些特殊的返回类型,也需要显式的进行定义。
    因为我对RoleAction中的execute()方法返回结果进行了显式的定义,所以,它将不再返回默认的role.jsp,而是location指定的role-list.jsp,Result注解中的name值要与返回值对应。
    当请求路径为role!input.action时,会执行input()方法,对于这个方法来说,由于没有进行显式的定义,所以它会按照默认的命名规则返回role-input.jsp。
    而redirectUser方法的返回结果指定了一个type为redirectAction的值,这表示要对Action重定向,在location中也说明了是跳转到哪个Action。请注意这里指定的是user.action,当程序跳转到UserAction时,会默认执行execute方法。
    假如说,你想执行其它方法该怎么办呢?可以在location里面这样定义,location="user!input.action"。请记住,重定向时,如果是跳转到其它Action或本Action中的其它方法,type要写成redirectAction。
    更进一步,我还想带些参数过去,又该如何呢?请添加params属性,它是一个数组类型。可以这样定义,params={"role_id","${role_id}","role_name","超级管理员"}。convention文档中有说明,里面的参数是一个键值对,总是形如key,value,key,value。所以第一个role_id与第三个role_name都叫参数名,二和四则是参数值。另外注意下"${role_id}"的含义,这是使用的OGNL表达式取出存在于值栈中的名叫role_id的值。这是一种动态获取并赋值的方式,在采用配置文件的方式中,也可以这样运用,而role_name参数则是一个固定字符串值。需要特别注意的就是,作为参数名的role_id与role_name,一定要在指向的Action中有这两个同名的属性,并且还有set方法,这是用来给这两个属性赋值。而对于${role_id},则要在当前这个Action中,有它的get方法。用于取值。
    补充说明一下,在Action类中定义的全局变量,不是非得给它都加上set、get方法,这是根据实际情况来设置的。简单的说get()是获得值,set()是设置值。比如,你现在要在页面上显示username,那么就对这个属性设置get方法,如果只是对username设置值,从页面传值到Action,那只需要对它设置set方法就可以了。除此之外,我们也可以不采用struts2提供的值栈方式得到参数值,而是使用非常熟悉的request. getParameter()方法来获取参数。至于实际怎么使用,由各位自己决定,不知道我这样说,大家能不能明白?
    大象根据实际使用情况,发现动态参数的传递在struts2.1.6存在BUG,如果需要使用这个功能,请将struts2升级到2.1.8.1版。
    大象根据实际应用,建议大家统一在类名上面定义Results设置,这样做有利于开发与维护;不建议单独对方法使用@Action注解来重新定义它的访问地址与返回结果,因为这样做有些破坏统一性,不过可以根据实际情况进行处理,但不要过多的使用。
    struts.xml
    
    整个struts.xml的配置文件就这么多,当然你自己还可以扩展,因为采用了注解,所以以前的那些配置就再也看不到了。在这个文件中,package是继承convention-default,而没有继承struts-default,为什么呢?查看convention的struts-plugin.xml文件,我们可以发现convention-default继承了struts-default,所以这样写是没错的。另外的几个constant配置就是对convention的常量设置,请看注释。
    关于paramsPrepareParamsStack拦截器栈,我准备在第五篇,对基础框架进行扩展的时候再详细的说明。大家如果等不急想学习下,可以在网上查找这方面的资料先看看。
    web
    大象是这样想的,如果一次讲的太多太复杂不利于理解和吸收,所以对于web层,大家从前面也看到了,代码很简单,因为本篇主要是讲convention插件的知识,然后实现一部分功能用于演示它的效果。下面贴上web和WebRoot目录结构、UserAction的代码,以及jsp代码。
    
    
    
    请注意web包下面的层次结构,这与你的请求路径相关。content文件夹是插件默认指定的名字,你可以修改为别的名字。同样请注意在这个目录下面的文件与子文件夹的定义方式是和web层相同的。如果还没有理解,请再看下我对convention插件的说明。
    在web.xml文件中,设置了一个<welcome-file-list>标签,定义了一个index.jsp,这文件里就一句代码 <% response.sendRedirect("hello.action"); %> 它会去执行HelloAction的execute()方法,这方法里面什么逻辑都没有,直接返回结果页面hello.jsp
    
    ${ctx}是一个EL表达式,设置的是当前项目名称。我在文件开头加了一个静态包含,<%@ include file="/common/taglibs.jsp" %>
    
    不管是user.action还是role.action,它们默认的执行方法都是execute(),点击这两个链接,返回指定的结果页面。
    
    
    在user.jsp里面,用来循环的list,是根据getList()方法获取的,struts2会自动的分析出属性名。想一下,list的get方法是不是就是getList()呢?我之前说过,get()是获得值,set()是设置值。在这里我只是要在列表页面上得到list集合,没有其它的需求,所以不用像这样定义 private List list,再然后给它加上set()、get()方法,因为要得到list集合,所以还要在execute()方法里面写上list = userManager.getUsers(),这样做有必要么?我一直都在遵循优雅、高效、简洁的代码风格,并且一直都在朝这方面努力,也提倡大家这样做。编程是门艺术,而不是一种工作,不要把它当工作看,只想着完成任务,拼命的堆代码。这样做很难有提高。应该换一种心态去对待它,用艺术的眼光来重新审视你的代码,你会发现这很有乐趣,也会学到很多。自己的一点浅薄之见,让各位见笑了。
    这部分的内容就说到这里,下一篇将对paramsPrepareParamsStack拦截器栈进行详细说明,另外再对框架进行一下扩展,封装CRUD功能,只要没有特殊的业务逻辑,在你的实际Action中,再不会看到增删改查这些基本功能。
    本文为菠萝大象原创,如要转载请注明出处。 http://bolo.blogjava.net/