这一章,大象将详细分析 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 中,再不会看到增删改查这些基本功能。