- 浏览: 47917 次
- 性别:
- 来自: 北京
文章分类
最新评论
-
e421083458:
人才啊!
【原創】Spring、Hibernate、Struts1整合的方式 -
zqzstar:
不知道怎么用,悲剧
ckfinder的java实现 -
bingqiang:
哦,只有cn.boyi.util.RequestUtil.ma ...
ckfinder的java实现 -
bingqiang:
依赖的cn.boyi.pagination包没有在里面
ckfinder的java实现 -
usb_usb:
不知道怎么配置啊?那个文件上传路径怎么是'/browser/b ...
ckfinder的java实现
完整的例子源码已经作为附件上传,为了减小体积,没有包含用到的jar文件,请读者自己下载,不便处还请见谅。
9.1 模型概述
正如我们所知,Struts是一个单纯的web框架,因此对于其他方面的操作无能为力,例如对数据库的操作。此外它也不能管理对象之间的依赖关系,因此需要和持久化框架和IoC框架结合,才能构建一个完整健壮的系统。本节分析Struts+Spring+Hibernate结合的必要性。
9.1.1 三种模型结合的必要性
对于从事web开发的人来说,使用Struts可以大大简化开发过程,对于直接使用Servlet+JSP来说,它提供了优秀的封装机制,使得用户可以严格按照MVC架构来开发应用。想想我们没有使用web框架之前,是怎样的混乱状态,需要自己将数据从请求中取出来,然后组装起来,在数据很多的情况下,这简直是一个噩梦。验证代码散落在各处,维护它们同样是一件累人的活。为了处理不同的请求,可能需要写很多的Servlet,且不说容器的负担问题,单单是web.xml文件就变得分外的臃肿,最后可能变成一个难以维护的庞然大物。这是我在实际开发中遇到过的真实情况。尽管使用Struts,在早期也会遇到配置文件(struts-config.xml)膨胀的问题,但后来的Struts通过模块化很好的解决了这个问题。
关于验证,需要多说一些,事实上,Struts的验证并不像后来的一些web框架而言,如Webwork、Spring MVC,是可插拔的,相反,它被包含在ActionForm中,这是一种耦合性较强的设计。form负担了的太多的责任,使得重用它变得困难,比如在业务层和持久层,另外,form是Struts框架的一部分,出于降低耦合性的考虑,也不能在业务层和持久层中出现。它之后的一些框架都很好的解决了这个问题:使用一个普通的pojo,仅仅用于封装数据,不包含任何的逻辑。但是,Struts是一个可扩展的框架,它的任何一部份都是可以替换的。我们可以通过它提供的插件机制,来提供自己的功能,例如通常情况下,Spring和Struts的整合的一种方式,就是通过插件实现的。通过插件,我们也可以提供可插拔的验证功能。
Struts是一个经历了无数项目考验的框架,有着为数最为庞大的社群,有着成熟的经验可供参考,所以在项目中使用Struts风险很小。而且,Struts比较容易上手,因此学习成本也相对较小。
正如我们前面所说的,Struts是一个纯粹的web框架,不涉及业务层和持久层的处理,因此,对于其他层需要借助于其他的框架。比如,在持久层使用 Hibernate。
数据库操作是一个很复杂的问题,尽管在Java中,JDBC进行了很好的抽象,但是用户需要直接操作SQL。这里也存在一个数据组装的问题,实际开发中直接从ResultSet中取出数据,然后放入一个model中,是很常见的方式。冗长的set操作令人厌烦。我们在实际开发中,都是将数据存放到model中,然后操作model,在这个意义上说,model和数据库之间有着某种内在的关系。怎样映射这种关系,正是ORM所要做的。ORM不仅仅要映射对象的属性和数据库表的字段间的关系,还要把表之间的关系映射为对象之间的关系,从而把对数据库表的操作,完全转为对对象的操作。这种处理方式,更为程序员所熟悉,而且通过工具,我们也很容易保持表关系和对象关系的同步。Hibernate正是这样一个强大的ORM工具,除了通常的O-R映射,还提供了其他的广受欢迎的功能,比如脏数据检查、延迟加载等。这些,我们在前面的章节中,已经做过介绍。Hibernate在实际开发中获得了广泛的应用,成为事实上的标准。所以,我们在开发中,持久层实现,可以选择Hibernate。
Struts同样也缺少IoC和AOP功能,因此存在一定的耦合,例如,在Action调用业务对象的时候,需要new一个实例,这是很常见的情况。当然,我们可以使用工厂模式消除这种情况,但是又需要引入工厂类。使用Spring可以有效地消除这种耦合。我们在上一章中,介绍的Spring和Struts整合的方式中,可我们在Action中通过Spring的ApplicationContext获取对象,但是这样Action中又嵌入了Spring的代码。整合的第三种方式,通过Struts的plug-in机制,将Action交由Spring托管,这样,我们就可以在Action中使用依赖注入,完全消除了这种耦合。除此之外,我们还可以使用Spring的AOP机制。通过整合,我们可以充分利用各个框架的优势,从而达到构建完美的应用的目标。
9.2 模型特征
我们分析了三种框架结合的必要性,现在我们分析一下使用三个框架的模型特性。
9.2.1 模型的构成特点
在Spring+Hibernate+Struts模型中,Hibernate负责持久化操作,Struts负责web操作,两者之间完全独立。Spring在这个模型中的地位很特殊,它提供对对象的管理,而不管这个对象是处在哪一层,完成哪些任务。Spring为用户设置对象间的依赖关系,并且通过AOP提供了统一的方式,处理横向的需求,这些需求通常很难使用OO实现,比如权限验证。这样,Spring就涉及到广泛的领域,而不管它处在哪一层。或者说,Spring是一个更为基础的框架,而不像Struts和Hibernate专注于各自的领域。
现在已经清楚了Spring、Hibernate、Struts的作用,现在从分层的角度,分析一下这个模型。
业务层,处理实际的逻辑操作,这一部分是由用户独立实现的,拥有最大的灵活性。有些时候,持久层和业务层是混合在一起的,但是数据库一旦设计完成,就很少修改,所有拥有相当的稳定性,所以持久层也拥有相当的稳定性,因此可以将它们独立出来。还有一个原因,持久化操作,很多时候很复杂,特别是涉及到事物的时候,因此,维护一个稳定的持久层,有利于分工,也减少了维护的成本。Hibernate的出现,使得持久化操作不再像以前那么复杂繁琐,所以也有将这两层合并的做法。我们下面给出的例子,会看到很多时候业务层仅仅是将请求委托给持久层,本身什么也不做。但我认为,维护一个独立的业务层,还是有好处的,至少在将来切换持久化实现的时候,比如用JDBC替换Hibernate,业务层就不用作任何的改动,虽然这种情况很少发生。
持久层,我们可以直接使用Hibernate API,更好的方式是使用Spring的Hibernate封装。Spring提供了对Hibernate的良好封装,这样我们在持久化操作中,可以使用Spring提供的封装,简化持久化的操作。Hibernate的Session是很昂贵的资源,因此,每个DAO(这里指持久层的类)方法,单独使用一个Session是一种浪费,使用Spring的封装,就可以使用统一的方式使用Session。通过使用Spring托管的Session,我们实现了OpenSessionInView模式,即在每一个请求中使用一个Session。当然我们可以提供自己的OpenSessionInView实现,最简单的就是实现一个Filter。事实上Spring提供的其中一种实现就是通过这种方式实现的。在Spring托管的环境下,我们就可以实现Hibernate的声明式管理。使用Spring封装的一个好处就是对异常的处理,Spring可以给出很明确的异常信息,不像JDBC统一抛出SQLException,Hibernate抛出HibernateException。Spring还提供了异常转换的功能,用户可以在配置文件中声明转换成什么样的异常。
web层,就是Struts的责任了,这里通常不会涉及到Hibernate和Spring,我们仅仅是由Spring管理依赖对象的注入。但是,很多时候web请求需要一些横向的需求,比如上面提到的权限管理,一个更常见的情况是用户登录和注册。当然我们可以使用Filter实现,也可以写一个顶层的Action,统一处理这些请求,需要处理这些请求的Action继承它即可。但是这是一种硬编码的实现,不能通过配置改变,因此缺少灵活性。用户是否登录的判断也是一个典型的横切应用,因此可以通过AOP来实现。我们下面的例子,会给出一个使用拦截器判断用户是否登录的实现。为了使用拦截器,我们这里给出了Spring和Struts整合的第四种方式,涉及到了SpringMVC的内容。相对来说,SpringMVC在设计上更加倚重IoC和AOP,也提供了更为开放的方式,因此更为强大,但是却过于复杂。但是相对于它提供的丰富特性,这些复杂性是可以克服的,因此在实际开发中,可以考虑用SpringMVC实现web层,即用它替换Struts。
9.2.2 实现模型的目的
使用Spring+Hibernate+Struts模型的目的,实际上面已经涉及到了,这些框架的优点是使用它们的一个重要原因。还有一些可能更重要的原因,总结如下:
去耦合 这是一个老生常谈的话题了。去耦合是如此的重要,以至于人们要将这方面的经验总结成设计模式。Spring通过IoC,实现了去耦合,这样用户可以随时替换现在的实现,而不用修改源码。这正是软件开发中开-闭原则的要求。Spring倡导面向接口编程,这是最为推崇的最佳实践之一,继承仅仅作为内部实现的一个手段。面向接口编程,不仅仅是提供了更好的抽象,实际上也更有利于去耦合。
扩展的需要 Struts、Spring甚至Hibernate都提供了良好的扩展功能,Struts通过插件机制,可以很方便的添加功能。Spring对Bean的管理也提供了扩展,例如用户可以提供自己的作用域。Hibernate用户可以定制自己的持久化策略。
维护的需要 这些框架都很容易维护,它们都很好的实现了开-闭原则,因此需要替换功能的时候,只需简单的修改配置文件。它们提供的一些公用功能,也能通过配置的方式提供,比如Spring的声明式事务。因此维护成本大大降低。
还有一个原因,由于它们都有庞大的社区支持,因此寻找技术支持很容易。由于它们很流行,因此招一个开发人员也很容易。
9.3 Hibernate+Spring+Struts模型应用分析
通过上面的学习,我们对三者结合的模型有了一个概念性的理解,现在我们通过一个具体的例子,演示怎样将它们整合在一起。
9.3.1 应用事例
我们考虑一个购物车的例子,相信读者或多或少都接触过这方面的例子。为了简单起见,我们不考虑库存的问题,商品的信息完全由用户填写,包括商品名称、商品介绍、商品价格、购买数量。填写完商品信息,会被暂时放入购物车内,每个购物车抽象成一个订单。当用户确认购买的商品后,就可以提交购物车,从而这些信息被存入数据库,购物车清空。在做这些操作之前,用户首先需要登录,如果用户没有注册,可以选择注册。用户不仅可以购买商品,还可以查看已经提交的订单的详细信息。一个用户可以有多个订单,一个订单只能属于一个用户。同理,一个订单可以包含多个商品,而一个商品只能属于一个订单。用户的姓名是唯一的,因为这是确认用户的唯一凭证。这就是全部的需求。
9.3.2 事例分析
好,现在我们开始构建整个应用。应用用到的类库如图9-1所示:
图9-1 应用用到的jar包
首先,我们需要建立所需的数据库表,我们用到的表有三个,每个表都有一个用于标识的id,这三个表分别是:
我用的数据库是Oracle9i,生成三个表用到的SQL如下:
接下来,我们生成pojo和相关的hibernate映射文件,这可以通过hibernate-tools实现,也可以通过Middlegen实现。pojo对应的名字分别是User、Order和Item,映射文件和pojo同名。注意包名的命名,通过包,我们可以将类进行分组,便于管理。我们在开发中,应该重视包的管理,至少在一个应用中,应该有统一的命名规范。这里使用的命名习惯和放置的文件是这样的:
这里我们只列出pojo的源码,省略映射文件,其他的接口和类会在后面列出:
User.java:
Order.java:
Item.java:
第三步,构建持久化接口和它们的实现。开始构建时,我们可能不知道需要哪些方法,没有关系,我们从最小需求来考虑,只定义肯定用到的方法即可,至于应用中需要新的方法,可以通过重构添加,现在的IDE很多都提供了很好的重构功能。那么重构时是先从接口开始,还是从实现开始?一般来说,从接口开始保证实现必须包含这个方法,但是从类开始,我们可能会忘记将方法上推到接口。不过,这个问题不大,因为我们实际开发时用到的是接口,所以,这个不是问题。我的建议是从实现开始重构。
针对用户的操作,我们需要根据主键获取用户信息。由于要处理用户注册,所以需要保存用户信息。要处理登录,所以需要根据用户名获取用户信息。这就是需要用到的三个方法,借口定义如下:
对应的实现我们用到了Spring的HibernateTemplate类,这个类封装了对于Hibernate的通用操作。实现如下:
UserDAOImpl:
由于HibernateTemplate在其他的DAO实现中也要用到,因此我们可以把它放到一个超类中,这样所有的DAO实现先就可以通过继承这个超类,而不在需要自己声明HibernateTemplate。超类命名为BaseDAO,代码如下:
修改UserDAOImpl,使他继承BaseDAO,代码如下:
订单操作同样需要保存操作,可能需要删除操作,因为要查看订单,显然需要一个查询方法,为了简单,这里不考虑修改操作(由于Hibernate有脏数据检查功能,实际上,通过查询方法,我们也可以实现修改操作)。代码如下:
OrderDAO的实现代码如下:
商品的操作,我们是直接放在session中,只有提交订单时才会涉及到保存操作,这里我们使用Hibernate的级联保存,因此不涉及单独操作Item的情况,ItemDAO在这个事例中不需要,这里省略。读者可以自己完善这个应用,比如删除订单和查看单个商品信息等。
持久层逻辑完成了,现在我们考虑一下逻辑层的实现。
用户操作和DAO的操作相似,只不过这里只有业务处理,不会涉及数据存取的操作,代码如下:
对应的实现如下:
订单的处理,我们需要一个获取订单信息的方法,还需要一个保存订单的方法,接口代码如下:
对应的实现如下:
我们的购物车是通过HttpSession实现的,因此我们在这里不是定义一个Service,还是通过一个工具类,实现操作购物车的功能。这里购物车中保存的是一个尚未提交的订单。由于需要向订单内保存商品,我们在Order中增加一个方法,代码如下:
工具类的代码如下:
现在我们需要将这些类配置在Spring配置文件中,需要注意的是我们这里使用了声明式事务,因此需要引入对应的schema,引入的方式在上一章已经介绍,这里不再重复。配置文件的代码如下:
这里需要说明的是,为了简化配置,我们使用了自动装载。Hibernate所需要的信息完全在这里配置,从而取代了hibernate.cfg.xml的地位。事务管理器用的是专门为Hibernate3设计的,数据源是通过JNDI读取的,这些都是需要特别注意的。
现在可以开发web层了。由于Action和Form依赖于页面,所以我们先考虑构建页面,所有的页面位于应用的order目录下。根据需求,首先需要一个登录页,只需要用户名和密码输入框。代码如下(这里只列出和输入相关部分的代码,其它部分省略):
这里使用了Struts标签,使用它们时,需要确保在页面头部正确引入。注意在下面的form类中,属性名和这里的名称要一致。相关的form类代码如下:
然后写一个Action用来处理登陆,代码如下:
针对登陆的类至此开发完成。剩下的就是配置,我们首先使用上一章讲到的Spring与Struts整合的第三种方式,即使用Struts的Plug-in机制,struts-config.xml配置如下:
在WEB-INF目录下新建一个文件struts-order-config.xml,这个文件用于配置我们现在使用的模块。Struts的模块是按照目录划分的。在加入了这个模块以后,相应的web.xml中,给ActionServlet加入如下的参数:
如果有其它的模块加入的话,只需按照这个格式添加即可。需要注意的是 confg/ 后面的参数必须是模块对应的目录名。这样应用访问特定模块的时候,Struts会自动追加模块的名称。比如现在的登录,页面端表单中action对应的值是/loginDispatcher.do,Struts会自动将它转为/order/loginDispatcher.do。而Struts本身的配置文件在改动模块名的时候,不需要作任何改动。这个应用很小,没有必要使用模块化功能,这里使用它,只是为了演示。这样的需求实际应用中很常见。
Struts-order-config.xml的配置如下:
orderList.jsp用来显示用户的所有订单。path的命名,应该有一定的约定,比如只是简单映射页面,使用页面名称+Dispatcher的形式;处理页面的请求的路径使用页面名称+Action的形式,等等,这有利于将来我们使用通配符来简化配置。这个问题我们后面会涉及到。
在Spring配置文件中加入Action的定义,bean的名字对应Struts中的path,只不过这个bean的名字包含了模块名:
现在我们可以测试登录了,我们在数据库中输入一个用户的信息,用户名和密码都是zxm。打开浏览器,在地址栏输入
http://www.zxm.net:8090/order/order/loginDispatcher.do
,页面如图9-2。
图9-2 登录
输入用户名和密码,输入不正确的用户名或者密码,应该返回登录页面,成功则进入订单列表页面,由于页面端需要用到Hibernate的延迟加载特性,第一次访问会抛出session closed的异常。
现在我们构建订单显示页,订单页的代码如下:
这里不但使用了Struts标签,还使用了JSTL,fn定义了一些有用的函数,可以在EL中使用,比如计算集合长度的length。Logic标签库是用于逻辑处理的标签库,iterator用于遍历集合,name属性用于从request、session、pageContext或者application取值,相当于调用它们的getAttribute()方法,property是对应的属性,可以使用表达式的方式,比如这里user.orders,相当于调用对象的getUser().getOrders()方法,这种形式很常见,比如在Spring和ognl中就支持这种形式的表达式。id属性是引用的名称,其作用和下面的变量order相当:
上面用到的EL语言,前面已经介绍过,这里不再赘述。
这里引人注目的是Orders集合是从User对象中取出的,而在我们的代码中并没有这样的赋值操作。事实上,这里用到了Hibernate的延迟加载特性,因此要求这里用到的User对象必须是一个持久化对象。这要求在处理页面的时候,必须保证Hibernate的Session打开。因此我们应该采用OpenSessionInView的模式,我们可以通过使用Spring提供的OpenSessionInViewFilter来实现这一模式,也可以通过使用Spring提供的OpenSessionInViewInterceptor,二者的作用相同。使用Filter的好处是开发者对它比较熟悉,使用Interceptor的方式要更为优雅。因为我们将来要借助Spring的拦截器实现对用户是否登录的判断,这里我们使用Interceptor的方式。不过这需要借助于SpringMVC来实现,稍显复杂。这就是Struts与Spring整合的第四种方式。详细介绍如下。
我们使用Spring的DispatcherServlet来分发请求,使用这个Servlet需要注册一个Listener,web.xml配置如下:
DispatcherServlet使用的配置文件,默认遵循这样的规则:Servlet的名字-servlet.xml。这里是actions-servlet,它应该位于WEB-INF下,这是一个标准的Spring配置文件。ContextLoaderListener也有需要一个默认的配置文件,名字是applicationContext.xml,也位于WEB-INFO下。我把我的配置信息都放入了applicationContext.xml中。我们需要去掉struts-config.xml中plug-in配置。好了,web.xml的信息就这么多。现在需要修改Spring配置文件applicationContext.xml,在里面添加如下的配置:
flushMode属性设置session的flush方式,这里设为0,表示永远不使用自动flush,这是为了使用声明式事务。
这里声明了一个Controller,ServletWrappingController是为了Struts专门设计的,作用相当于代理Struts的ActionServlet,下面的属性都很好理解,我们看到和在web.xml的配置差不多,只不过形式的差异。
最后我们需要把Struts的请求路径和Controller关联起来:
这样我们就可以使用Hibernate的延迟加载特性和Spring的声明式事务了。注意intercptors属性,我们可以添加自己的拦截器实现,稍后我们会给出一个具体的实例。拦截器的顺序也是要格外留意的。登录成功后页面如图9-3。
图 9-3 用户订单列表
用户注册的流程和登录一样,注册页面和登录一样,这里我们就不在列出,只列出处理注册的Action,代码如下:
注册页面是register.jsp,对应的path是/registerAction。然后我们在登录页面下加一个注册按钮,这样用户就可以从登录页转到注册页。给注册按钮注册一个响应单击的方法,代码如下:
在struts-order-config.xml添加如下的配置:
在applicationContext.xml中加入对应的Action声明:
这样就可以进行登录测试了。读者可以测试注册一个中文名,成功后,会发现用户名是乱码,这是由于Tomcat默认使用ISO-8859-1编码,而我们使用的是GBK编码。解决这个问题,针对请求方式,有不同的处理策略。比如对POST请求,我们需要使用过滤器进行重新编码,对于GET请求,需要在Tomcat端口配置中设置编码方式。下面我们分别予以介绍。
我们可以自己实现这个过滤器,不过Spring已经给我们提供了一个现成的实现,它的配置方式如下:
forceEncoding参数表示是否进行强制编码,这里当然是true。encoding表示编码方式,这里是GBK。
针对GET请求,我们只需要在监听HTTP请求的端口中加入如下的属性即可:URIEncoding="GBK"。
由于我们在应用中针对页面和Action的命名遵循了统一的规范,所以我们可以使用通配符简化配置。例如访问页面的方式可以使用如下的方式:
*就是用到的占位符,下面引用的时候使用大括号,包含一个数字,数字表示占位符的位置,注意这个值是从1开始的。例如当用户访问/loginDispatcher的时候,访问到的页面就是login.jsp。
相应的用于处理请求的Action的配置统一修改如下:
登录和查看用户订单清单的任务已经完成了,现在我们看一下怎么订购商品。同上面的处理方式一样,我们需要一个输入商品信息的页面,页面的名字是item.jsp,代码如下:
处理保存订单的Action代码如下:
提交订单后,会返回一个购物车的页面,页面的名称是order.jsp,显示用户现在放入购物车中的所有商品。由于这需要一个查询操作,所以我们需要一个新的Action,这个Action事实上会处理两种情况,一种是用户查阅订单内的商品,另一种就是查阅购物车内的商品,二者的区别不过一个是从数据库中读取数据,一个是从session中读取数据。这个Action的代码如下:
struts-order-config.xml添加如下的配置:
在applicationContext.xml中需要加入的配置如下:
现在我们可以通过/itemDispatcher.do来访问定购商品页了,如图9-4。
提交商品后,应该返回给用户购物车的页面,显示购物车内的所有商品,这个页面是order.jsp。order.jsp的代码如下:
图9-4 订购商品
logic标签用来处理逻辑的,从字面意思也很好理解,empty判断属性是否为空,notEmpty判断属性是否非空。我们把提交购物车和查询订单的页面做到了一起,为了清晰,读者可以考虑将它分为两个文件。查询订单的页面如图9-5,查询购物车的页面如图9-6。
将商品放入购物车后,下一步就是提交购物车,反映在数据库中,是生成了一个订单,同时将购物车清空。处理提交订单的Action代码如下:
由于提交订单和提交商品有着相似的形式,因此我们使用通配符,不需要再次配置,只需要在applicationContext.xml中加入下面的配置即可:
图9-5 查询订单详细信息
我们还忽略了一个问题,就是用户查询订单列表的时候,并非一定是从登录页面开始的,因此,我们还需要一个查询订单列表的Action,代码如下:
图9-6 查询购物车内的信息
struts-order-config.xml的配置信息如下:
applicationContext.xml需要加入的内容是:
为了访问方便,我们还使用全局性的配置,这里用到了三个,struts-order-config.xml中涉及的配置如下:
这样整个应用就算完成了,剩下的工作就是在页面上加入必要的导航,比如返回首页的链接等。
这个应用似乎还缺少些什么。如果session过期,怎么办?这是一个即为常见的需求,我们固然可以在访问每个action前进行判断,但是这样同样的代码会分布在不同的地方,维护起来很麻烦。重构中,有一条原则,就是"Once and only once",即所谓的相同的代码只能出现一次。这是一条很重要的原则,除了维护的需要外,还有重用的考虑。登录过期的判断,是一个典型的横切的应用,采用传统的方式实现要困难一些,我们可以使用一个基类进行判断,然后让需要这个功能的类继承这个基类,这是一个解决方案。还有一个更好的解决方案,就是使用拦截器,拦截请求。我们可以将它配置在文件中,这样就使用可配置的方式,防止了硬编码,这是一种极为灵活的方式。我们下面就介绍怎样使用拦截器,判断用户的登录信息是否过期。
为了拦截HTTP请求,我们需要实现接口HandlerInterceptor,这个接口有三个方法,boolean preHandle(...),这个方法在处理HTTP请求之前执行,void postHandle(..)在输出页面之前执行,void afterCompletion(...)在请求处理完成之后进行。很显然preHandle()正是需要我们实现的方法。这个拦截器的代码如下:
为了增加灵活性,action的后缀和需要过滤的url已经登录页面,都使用依赖注入。在appliccationContext.xml首先需要添加拦截器的声明,它和一个普通的声明没有任何区别:
接下来的工作就是在urlMapping注册拦截器:
注意拦截器是有顺序的,loginInterceptor显然应该在openSessionInViewInterceptor之前。用户可以在没有登录的情况下,访问一个合法的url,例如/search_items.do,如果能返回到登录页面,证明拦截器配置成功。
最后,为了对这个示例有更为清楚地理解,我们现在列出完整的struts-order-config.xml文件内容:
9.3.3 预期的效果
上面的例子虽然很简单,但是让我们感到还是很杂乱,我们暂时不必理会这个问题,我们需要做的是看看通过整合,是否达到我们预期的效果。我们整合的目的之一就是通过使用Spring的IoC解除代码的耦合,很显然,我们达到了这个目的。第二个,使用Hibernate,在页面端也能够使用延迟加载,同时在请求时保证共享Hibernate Session,以达到提高资源利用率的目的,这些通过使用OpenSessionInVew模式,也得到了很好的解决。此外,我们使用了声明式事务,以一种极为简洁的方式实现了这个平时需要硬编码才能实现的功能。统一对登录信息的判断,是应用中经常遇到的问题,在这里,通过使用拦截器,我们也提供了一种简洁的实现,而且它是可插拔的。
在整合的过程中,通过将Action交给Spring托管,我们得以在web层使用Spring提供的优秀技术,IoC、AOP等,使得web层的功能更单一,结构更清晰,代码也变得更为简洁。当然这种简洁不仅仅限于web层,同样包括持久层,这是Hibernate的威力。通过三者的结合,我们就可以各取所长,从而开发出一个强大而又容易维护的系统。
9.4 小结
Struts是一个单纯的web框架,没有涉及业务层和持久层的东西,因此在持久层实现中,可以自由选择优秀的持久化实现,而不用担心对web层有什么影响。Hibernate是业内顶级的ORM框架,可以作为持久层实现的基础。Struts本身没有IoC和AOP功能,而Spring正是优秀的IoC和AOP框架。因此在一个应用中,采用Spring+Hibernate+Struts的模式就有必要了。
在这个模型中,Struts负责对web请求的处理和响应,Hibernate提供持久化实现,Spring则提供bean的管理,通过AOP提供对横切需求的处理。除此之外,Spring还可以提供一些企业级的服务,例如声明式事务。
实现这个模型的目的,是去耦合的需要,也是扩展的需要,因此三者都很容易进行扩展,同时也是维护的需要,因为它们良好的设计,都很容易维护。由于它们比较流行,因此招聘一个熟悉它们的人也很容易。
我们通过一个购物车的例子演示了怎样进行整合。用户需要登录,如果没有注册,还要进行注册。一个用户需要自己填写商品信息,然后把它放入购物车,最后提交购物车,生成定单。用户可以查询自己的所有订单,也可以查询单个订单内包含的信息,比如订单内的所有商品。这是一个很常见的例子。
我们演示了怎样通过Interceptor使用Spring提供的openSessionInVew模式,从而可以在页面上使用Hibernate的延迟加载。这需要借助于SpringMVC来实现,这种整合方式不同于上一章所提到的三种方式,它要更为复杂,也更为强大。SpringMVC代理了Struts的ActionServlet,从而可以通过Spring拦截HTTP请求,这是通过Spring的拦截器实现的,我们给出了一个用拦截器判断用户是否登录的例子。
可以使用通配符简化Struts的配置,这也要求我们对页面名称和Action路径遵循一定的命名规范。这里同样演示了怎样使用Struts的模块化开发,针对中文乱码问题的处理等。
持久层实现,采用Hibernate,我们使用Spring对Hibernate的封装,简化了持久层的操作。
在整合的过程中,通过将Action交给Spring托管,我们得以在web层使用Spring提供的优秀技术,IoC、AOP等,使得web层的功能更单一,结构更清晰,代码也变得更为简洁。在持久层的代码也同样的简洁。
9.1 模型概述
正如我们所知,Struts是一个单纯的web框架,因此对于其他方面的操作无能为力,例如对数据库的操作。此外它也不能管理对象之间的依赖关系,因此需要和持久化框架和IoC框架结合,才能构建一个完整健壮的系统。本节分析Struts+Spring+Hibernate结合的必要性。
9.1.1 三种模型结合的必要性
对于从事web开发的人来说,使用Struts可以大大简化开发过程,对于直接使用Servlet+JSP来说,它提供了优秀的封装机制,使得用户可以严格按照MVC架构来开发应用。想想我们没有使用web框架之前,是怎样的混乱状态,需要自己将数据从请求中取出来,然后组装起来,在数据很多的情况下,这简直是一个噩梦。验证代码散落在各处,维护它们同样是一件累人的活。为了处理不同的请求,可能需要写很多的Servlet,且不说容器的负担问题,单单是web.xml文件就变得分外的臃肿,最后可能变成一个难以维护的庞然大物。这是我在实际开发中遇到过的真实情况。尽管使用Struts,在早期也会遇到配置文件(struts-config.xml)膨胀的问题,但后来的Struts通过模块化很好的解决了这个问题。
关于验证,需要多说一些,事实上,Struts的验证并不像后来的一些web框架而言,如Webwork、Spring MVC,是可插拔的,相反,它被包含在ActionForm中,这是一种耦合性较强的设计。form负担了的太多的责任,使得重用它变得困难,比如在业务层和持久层,另外,form是Struts框架的一部分,出于降低耦合性的考虑,也不能在业务层和持久层中出现。它之后的一些框架都很好的解决了这个问题:使用一个普通的pojo,仅仅用于封装数据,不包含任何的逻辑。但是,Struts是一个可扩展的框架,它的任何一部份都是可以替换的。我们可以通过它提供的插件机制,来提供自己的功能,例如通常情况下,Spring和Struts的整合的一种方式,就是通过插件实现的。通过插件,我们也可以提供可插拔的验证功能。
Struts是一个经历了无数项目考验的框架,有着为数最为庞大的社群,有着成熟的经验可供参考,所以在项目中使用Struts风险很小。而且,Struts比较容易上手,因此学习成本也相对较小。
正如我们前面所说的,Struts是一个纯粹的web框架,不涉及业务层和持久层的处理,因此,对于其他层需要借助于其他的框架。比如,在持久层使用 Hibernate。
数据库操作是一个很复杂的问题,尽管在Java中,JDBC进行了很好的抽象,但是用户需要直接操作SQL。这里也存在一个数据组装的问题,实际开发中直接从ResultSet中取出数据,然后放入一个model中,是很常见的方式。冗长的set操作令人厌烦。我们在实际开发中,都是将数据存放到model中,然后操作model,在这个意义上说,model和数据库之间有着某种内在的关系。怎样映射这种关系,正是ORM所要做的。ORM不仅仅要映射对象的属性和数据库表的字段间的关系,还要把表之间的关系映射为对象之间的关系,从而把对数据库表的操作,完全转为对对象的操作。这种处理方式,更为程序员所熟悉,而且通过工具,我们也很容易保持表关系和对象关系的同步。Hibernate正是这样一个强大的ORM工具,除了通常的O-R映射,还提供了其他的广受欢迎的功能,比如脏数据检查、延迟加载等。这些,我们在前面的章节中,已经做过介绍。Hibernate在实际开发中获得了广泛的应用,成为事实上的标准。所以,我们在开发中,持久层实现,可以选择Hibernate。
Struts同样也缺少IoC和AOP功能,因此存在一定的耦合,例如,在Action调用业务对象的时候,需要new一个实例,这是很常见的情况。当然,我们可以使用工厂模式消除这种情况,但是又需要引入工厂类。使用Spring可以有效地消除这种耦合。我们在上一章中,介绍的Spring和Struts整合的方式中,可我们在Action中通过Spring的ApplicationContext获取对象,但是这样Action中又嵌入了Spring的代码。整合的第三种方式,通过Struts的plug-in机制,将Action交由Spring托管,这样,我们就可以在Action中使用依赖注入,完全消除了这种耦合。除此之外,我们还可以使用Spring的AOP机制。通过整合,我们可以充分利用各个框架的优势,从而达到构建完美的应用的目标。
9.2 模型特征
我们分析了三种框架结合的必要性,现在我们分析一下使用三个框架的模型特性。
9.2.1 模型的构成特点
在Spring+Hibernate+Struts模型中,Hibernate负责持久化操作,Struts负责web操作,两者之间完全独立。Spring在这个模型中的地位很特殊,它提供对对象的管理,而不管这个对象是处在哪一层,完成哪些任务。Spring为用户设置对象间的依赖关系,并且通过AOP提供了统一的方式,处理横向的需求,这些需求通常很难使用OO实现,比如权限验证。这样,Spring就涉及到广泛的领域,而不管它处在哪一层。或者说,Spring是一个更为基础的框架,而不像Struts和Hibernate专注于各自的领域。
现在已经清楚了Spring、Hibernate、Struts的作用,现在从分层的角度,分析一下这个模型。
业务层,处理实际的逻辑操作,这一部分是由用户独立实现的,拥有最大的灵活性。有些时候,持久层和业务层是混合在一起的,但是数据库一旦设计完成,就很少修改,所有拥有相当的稳定性,所以持久层也拥有相当的稳定性,因此可以将它们独立出来。还有一个原因,持久化操作,很多时候很复杂,特别是涉及到事物的时候,因此,维护一个稳定的持久层,有利于分工,也减少了维护的成本。Hibernate的出现,使得持久化操作不再像以前那么复杂繁琐,所以也有将这两层合并的做法。我们下面给出的例子,会看到很多时候业务层仅仅是将请求委托给持久层,本身什么也不做。但我认为,维护一个独立的业务层,还是有好处的,至少在将来切换持久化实现的时候,比如用JDBC替换Hibernate,业务层就不用作任何的改动,虽然这种情况很少发生。
持久层,我们可以直接使用Hibernate API,更好的方式是使用Spring的Hibernate封装。Spring提供了对Hibernate的良好封装,这样我们在持久化操作中,可以使用Spring提供的封装,简化持久化的操作。Hibernate的Session是很昂贵的资源,因此,每个DAO(这里指持久层的类)方法,单独使用一个Session是一种浪费,使用Spring的封装,就可以使用统一的方式使用Session。通过使用Spring托管的Session,我们实现了OpenSessionInView模式,即在每一个请求中使用一个Session。当然我们可以提供自己的OpenSessionInView实现,最简单的就是实现一个Filter。事实上Spring提供的其中一种实现就是通过这种方式实现的。在Spring托管的环境下,我们就可以实现Hibernate的声明式管理。使用Spring封装的一个好处就是对异常的处理,Spring可以给出很明确的异常信息,不像JDBC统一抛出SQLException,Hibernate抛出HibernateException。Spring还提供了异常转换的功能,用户可以在配置文件中声明转换成什么样的异常。
web层,就是Struts的责任了,这里通常不会涉及到Hibernate和Spring,我们仅仅是由Spring管理依赖对象的注入。但是,很多时候web请求需要一些横向的需求,比如上面提到的权限管理,一个更常见的情况是用户登录和注册。当然我们可以使用Filter实现,也可以写一个顶层的Action,统一处理这些请求,需要处理这些请求的Action继承它即可。但是这是一种硬编码的实现,不能通过配置改变,因此缺少灵活性。用户是否登录的判断也是一个典型的横切应用,因此可以通过AOP来实现。我们下面的例子,会给出一个使用拦截器判断用户是否登录的实现。为了使用拦截器,我们这里给出了Spring和Struts整合的第四种方式,涉及到了SpringMVC的内容。相对来说,SpringMVC在设计上更加倚重IoC和AOP,也提供了更为开放的方式,因此更为强大,但是却过于复杂。但是相对于它提供的丰富特性,这些复杂性是可以克服的,因此在实际开发中,可以考虑用SpringMVC实现web层,即用它替换Struts。
9.2.2 实现模型的目的
使用Spring+Hibernate+Struts模型的目的,实际上面已经涉及到了,这些框架的优点是使用它们的一个重要原因。还有一些可能更重要的原因,总结如下:
去耦合 这是一个老生常谈的话题了。去耦合是如此的重要,以至于人们要将这方面的经验总结成设计模式。Spring通过IoC,实现了去耦合,这样用户可以随时替换现在的实现,而不用修改源码。这正是软件开发中开-闭原则的要求。Spring倡导面向接口编程,这是最为推崇的最佳实践之一,继承仅仅作为内部实现的一个手段。面向接口编程,不仅仅是提供了更好的抽象,实际上也更有利于去耦合。
扩展的需要 Struts、Spring甚至Hibernate都提供了良好的扩展功能,Struts通过插件机制,可以很方便的添加功能。Spring对Bean的管理也提供了扩展,例如用户可以提供自己的作用域。Hibernate用户可以定制自己的持久化策略。
维护的需要 这些框架都很容易维护,它们都很好的实现了开-闭原则,因此需要替换功能的时候,只需简单的修改配置文件。它们提供的一些公用功能,也能通过配置的方式提供,比如Spring的声明式事务。因此维护成本大大降低。
还有一个原因,由于它们都有庞大的社区支持,因此寻找技术支持很容易。由于它们很流行,因此招一个开发人员也很容易。
9.3 Hibernate+Spring+Struts模型应用分析
通过上面的学习,我们对三者结合的模型有了一个概念性的理解,现在我们通过一个具体的例子,演示怎样将它们整合在一起。
9.3.1 应用事例
我们考虑一个购物车的例子,相信读者或多或少都接触过这方面的例子。为了简单起见,我们不考虑库存的问题,商品的信息完全由用户填写,包括商品名称、商品介绍、商品价格、购买数量。填写完商品信息,会被暂时放入购物车内,每个购物车抽象成一个订单。当用户确认购买的商品后,就可以提交购物车,从而这些信息被存入数据库,购物车清空。在做这些操作之前,用户首先需要登录,如果用户没有注册,可以选择注册。用户不仅可以购买商品,还可以查看已经提交的订单的详细信息。一个用户可以有多个订单,一个订单只能属于一个用户。同理,一个订单可以包含多个商品,而一个商品只能属于一个订单。用户的姓名是唯一的,因为这是确认用户的唯一凭证。这就是全部的需求。
9.3.2 事例分析
好,现在我们开始构建整个应用。应用用到的类库如图9-1所示:
图9-1 应用用到的jar包
首先,我们需要建立所需的数据库表,我们用到的表有三个,每个表都有一个用于标识的id,这三个表分别是:
- tb_user——用户表,用来存放用户信息,包括用户名和密码;
- tb_order——订单表,用来存放订单信息,包括订单描述、生成时间、用户id;
- tb_item——商品表,用来存放商品信息,包括商品名称、商品描述、商品价格、购买数量、定单id。
我用的数据库是Oracle9i,生成三个表用到的SQL如下:
/*商品表*/ create table tb_item ( id number(10,0) not null, orderid number(10,0) not null, /*订单id*/ name varchar2(20 char) not null, /*商品名称*/ description varchar2(800 char), /*商品描述*/ price number(18,2), /*商品价格*/ count number(10,0), /*购买数量*/ primary key (id) ) /*订单表*/ create table tb_order ( id number(10,0) not null, userid number(10,0) not null, /*用户id*/ description varchar2(800 char), /*订单描述*/ createTime timestamp, /*生成时间*/ primary key (id) ) /*用户表*/ create table tb_user ( id number(10,0) not null, name varchar2(20 char) not null, /*用户名*/ password varchar2(20 char) not null, /*密码*/ primary key (id) ) alter table tb_item add constraint FKA4F9FA443A36B48F foreign key (orderid) references tb_order alter table tb_order add constraint FKFA98EE3D6705FE99 foreign key (userid) references zxm.tb_user create sequence hibernate_sequence
接下来,我们生成pojo和相关的hibernate映射文件,这可以通过hibernate-tools实现,也可以通过Middlegen实现。pojo对应的名字分别是User、Order和Item,映射文件和pojo同名。注意包名的命名,通过包,我们可以将类进行分组,便于管理。我们在开发中,应该重视包的管理,至少在一个应用中,应该有统一的命名规范。这里使用的命名习惯和放置的文件是这样的:
- cn.zxm.order.pojo——pojo和相关的映射文件;
- cn.zxm.order.dao——持久化接口;
- cn.zxm.order.dao.impl——持久化接口的实现类;
- cn.zxm.order.service——业务接口;
- cn.zxm.order.service.impl——业务接口的实现类;
- cn.zxm.order.form——应用用到的ActionForm;
- cn.zxm.order.action——应用用到的Action;
- cn.zxm.order.util——应用用到的工具类。
这里我们只列出pojo的源码,省略映射文件,其他的接口和类会在后面列出:
User.java:
public class User implements java.io.Serializable { private Integer id; private String name; private String password; private Set<Order> orders = new HashSet<Order>(0); ……对应的setter和getter方法…… }
Order.java:
public class Order implements java.io.Serializable { private Integer id; private User user; private String description; private Date createTime; private Set<Item> items = new HashSet<Item>(0); ……对应的setter和getter方法…… }
Item.java:
public class Item implements java.io.Serializable { private Integer id; private Order order; private String name; private String description; private BigDecimal price; private Integer count; ……对应的setter和getter方法…… }
第三步,构建持久化接口和它们的实现。开始构建时,我们可能不知道需要哪些方法,没有关系,我们从最小需求来考虑,只定义肯定用到的方法即可,至于应用中需要新的方法,可以通过重构添加,现在的IDE很多都提供了很好的重构功能。那么重构时是先从接口开始,还是从实现开始?一般来说,从接口开始保证实现必须包含这个方法,但是从类开始,我们可能会忘记将方法上推到接口。不过,这个问题不大,因为我们实际开发时用到的是接口,所以,这个不是问题。我的建议是从实现开始重构。
针对用户的操作,我们需要根据主键获取用户信息。由于要处理用户注册,所以需要保存用户信息。要处理登录,所以需要根据用户名获取用户信息。这就是需要用到的三个方法,借口定义如下:
public interface UserDAO { /** * 保存用户 * @param user */ void save(User user); /** * 根据用户id获取用户信息 * @param id * @return */ User getUser(int id); /** * 根据用户名获取用户信息 * @param name * @return */ User getUserByName(String name); }
对应的实现我们用到了Spring的HibernateTemplate类,这个类封装了对于Hibernate的通用操作。实现如下:
UserDAOImpl:
public class UserDAOImpl implements UserDAO { protected HibernateTemplate hibernate; public void setHibernate(HibernateTemplate hibernate) { this.hibernate = hibernate; } public User getUser(int id) { return (User)hibernate.load(User.class, id); } public User getUserByName(String name) { String hql = "from User where name=?"; User user = null; //使用iterate方法,第二个参数可以是一个数组,用来给hql中的参数赋值 Iterator<User> iterator = hibernate.iterate(hql, name); //获取用户信息,没有将返回null if(iterator.hasNext()){ user = iterator.next(); } return user; } public void save(User user) { hibernate.saveOrUpdate(user); } }
由于HibernateTemplate在其他的DAO实现中也要用到,因此我们可以把它放到一个超类中,这样所有的DAO实现先就可以通过继承这个超类,而不在需要自己声明HibernateTemplate。超类命名为BaseDAO,代码如下:
public class BaseDAO { protected HibernateTemplate hibernate; public BaseDAO() { super(); } public void setHibernate(HibernateTemplate hibernate) { this.hibernate = hibernate; } }
修改UserDAOImpl,使他继承BaseDAO,代码如下:
public class UserDAOImpl extends BaseDAO implements UserDAO { public User getUser(int id) { …… } public User getUserByName(String name) { …… } public void save(User user) { …… } }
订单操作同样需要保存操作,可能需要删除操作,因为要查看订单,显然需要一个查询方法,为了简单,这里不考虑修改操作(由于Hibernate有脏数据检查功能,实际上,通过查询方法,我们也可以实现修改操作)。代码如下:
public interface OrderDAO { /** * 保存订单 * @param order */ void save(Order order); /** * 根据订单id获取订单信息 * @param id * @return */ Order getOrder(int id); /** * 删除订单 * @param order */ void delete(Order order); }
OrderDAO的实现代码如下:
public class OrderDAOImpl extends BaseDAO implements OrderDAO { public Order getOrder(int id) { return (Order)hibernate.load(Order.class, id); } public void save(Order order) { hibernate.save(order); } public void delete(Order order) { hibernate.delete(order); } }
商品的操作,我们是直接放在session中,只有提交订单时才会涉及到保存操作,这里我们使用Hibernate的级联保存,因此不涉及单独操作Item的情况,ItemDAO在这个事例中不需要,这里省略。读者可以自己完善这个应用,比如删除订单和查看单个商品信息等。
持久层逻辑完成了,现在我们考虑一下逻辑层的实现。
用户操作和DAO的操作相似,只不过这里只有业务处理,不会涉及数据存取的操作,代码如下:
public interface UserService { /** * 保存用户 * @param user */ void save(User user); /** * 根据用户id获取用户信息 * @param id * @return */ User getUser(int id); /** * 根据传入的用户数据获取完整的用户信息 * @param user * @return */ User getUser(User user); }
对应的实现如下:
public class UserServiceImpl implements UserService { private final Log log = LogFactory.getLog(UserServiceImpl.class); private UserDAO userDAO; public void setUserDAO(UserDAO userDAO) { this.userDAO = userDAO; } public User getUser(User user) { log.debug("验证用户"); return userDAO.getUserByName(user.getName()); } public User getUser(int id) { return userDAO.getUser(id); } /** * 这里使用了事务标注,这样可以使用声明式事务 * 如果用户名为空,将不会保存 */ @Transactional public void save(User user) { log.debug("保存用户"); if(userDAO.getUserByName(user.getName())!=null) return; userDAO.save(user); } }
订单的处理,我们需要一个获取订单信息的方法,还需要一个保存订单的方法,接口代码如下:
public interface OrderService { /** * 获取订单详细信息 * @param id * @return */ Order getOrder(int id); /** * 保存订单 * @param order */ void save(Order order); }
对应的实现如下:
public class OrderServiceImpl implements OrderService { private OrderDAO orderDAO; public void setOrderDAO(OrderDAO orderDAO) { this.orderDAO = orderDAO; } public Order getOrder(int id) { return orderDAO.getOrder(id); } /** * 保存订单,我们在这里给订单设置了生成时间 */ @Transactional public void save(Order order) { order.setCreateTime(new Timestamp(System.currentTimeMillis())); orderDAO.save(order); } }
我们的购物车是通过HttpSession实现的,因此我们在这里不是定义一个Service,还是通过一个工具类,实现操作购物车的功能。这里购物车中保存的是一个尚未提交的订单。由于需要向订单内保存商品,我们在Order中增加一个方法,代码如下:
/** * 添加商品,注意我们手动为商品添加了订单关联, * 这是为了使用Hibernate的级联保存 * @param item */ public void addItem(Item item){ item.setOrder(this); items.add(item); }
工具类的代码如下:
public final class OrderUtils { /** * 将商品放入购物车 * @param request * @param item */ public static void saveItem(HttpServletRequest request, Item item) { HttpSession session = request.getSession(); User user = (User)session.getAttribute("user"); Order order = getOrder(request); order.addItem(item); session.setAttribute(user.getName(), order); } /** * 获取购物车信息 * @param request * @return */ public static Order getOrder(HttpServletRequest request) { HttpSession session = request.getSession(); User user = (User)session.getAttribute("user"); Order order = (Order)session.getAttribute(user.getName()); //如果session没有Order对象,那么就是实例化一个 if(order==null) { order = new Order(); } return order; } /** * 清空购物车 * @param request */ public static void clearCart(HttpServletRequest request){ HttpSession session = request.getSession(); User user = (User)session.getAttribute("user"); session.removeAttribute(user.getName()); } }
现在我们需要将这些类配置在Spring配置文件中,需要注意的是我们这里使用了声明式事务,因此需要引入对应的schema,引入的方式在上一章已经介绍,这里不再重复。配置文件的代码如下:
<beans default-autowire="byName"> <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName" value="java:/comp/env/jdbc/order"/> </bean> <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="dataSource" ref="dataSource"/> <property name="sessionFactory" ref="sessionFactory"/> </bean> <tx:annotation-driven /> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mappingResources"> <list> <value>cn/zxm/order/pojo/User.hbm.xml</value> <value>cn/zxm/order/pojo/Order.hbm.xml</value> <value>cn/zxm/order/pojo/Item.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.Oracle9Dialect</prop> <prop key="hibernate.show_sql">false</prop> <prop key="hibernate.format_sql">true</prop> </props> </property> </bean> <bean id="hibernate" class="org.springframework.orm.hibernate3.HibernateTemplate"/> <bean id="baseDAO" class="cn.zxm.order.dao.impl.BaseDAO" abstract="true"/> <bean id="user" class="cn.zxm.order.pojo.User"/> <bean id="userDAO" class="cn.zxm.order.dao.impl.UserDAOImpl" parent="baseDAO"/> <bean id="orderDAO" class="cn.zxm.order.dao.impl.OrderDAOImpl" parent="baseDAO"/> <bean id="itemDAO" class="cn.zxm.order.dao.impl.ItemDAOImpl" parent="baseDAO"/> <bean id="userService" class="cn.zxm.order.service.impl.UserServiceImpl"/> <bean id="orderService" class="cn.zxm.order.service.impl.OrderServiceImpl"/> <bean id="itemService" class="cn.zxm.order.service.impl.ItemServiceImpl"/> </beans>
这里需要说明的是,为了简化配置,我们使用了自动装载。Hibernate所需要的信息完全在这里配置,从而取代了hibernate.cfg.xml的地位。事务管理器用的是专门为Hibernate3设计的,数据源是通过JNDI读取的,这些都是需要特别注意的。
现在可以开发web层了。由于Action和Form依赖于页面,所以我们先考虑构建页面,所有的页面位于应用的order目录下。根据需求,首先需要一个登录页,只需要用户名和密码输入框。代码如下(这里只列出和输入相关部分的代码,其它部分省略):
<html:form action="/loginAction.do" method="get"> <table width="400" border="0" align="center" cellpadding="10" cellspacing="1" class="table-bgcolor"> <tr class="table-body"> <td class="table-head">帐号</td><td><html:text property="name" style="width:98%;"/></td> </tr> <tr class="table-body"> <td class="table-head">密码</td><td><html:password property="password" style="width:98%;"/></td> </tr> <tr class="table-body"><td></td><td><input type="submit" class="button" value="登录"/></td></tr> </table> </html:form>
这里使用了Struts标签,使用它们时,需要确保在页面头部正确引入。注意在下面的form类中,属性名和这里的名称要一致。相关的form类代码如下:
public class UserForm extends ActionForm { private String name; private String password; private User user; //setter和getter方法 }
然后写一个Action用来处理登陆,代码如下:
public class LoginAction extends Action { private final Log log = LogFactory.getLog(LoginAction.class); private UserService userService; private User user; public void setUserService(UserService userService) { this.userService = userService; } public void setUser(User user) { this.user = user; } /** @see org.apache.struts.action.Action#execute(org.apache.struts.action.ActionMapping, org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ @Override public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { UserForm uf = (UserForm)form; //克隆一份User对象,以此保证登录信息的独立性 User user2 = (User)BeanUtils.cloneBean(user); //将用户发来的数据放入User对象中 copyProperties(user2, uf); //重新初始化表单 uf.reset(mapping, request); //查询用户信息 User u = userService.getUser(user2); //如果用户不存在或者输入信息不正确,就返回指定的页面 if(u==null || !u.getPassword().equals(user2.getPassword())) return mapping.findForward("fail"); user2.setId(u.getId()); user2.setPassword(u.getPassword()); //将查询获得的User对象放入form中,这样页面可以使用Hibernate的延迟加载 uf.setUser(u); //将用户信息存入session request.getSession().setAttribute("user", user2); //登录成功,返回指定页面 return mapping.findForward("welcome"); } }
针对登陆的类至此开发完成。剩下的就是配置,我们首先使用上一章讲到的Spring与Struts整合的第三种方式,即使用Struts的Plug-in机制,struts-config.xml配置如下:
<?xml version="1.0"?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.3//EN" "http://struts.apache.org/dtds/struts-config_1_3.dtd"> <struts-config> <plug-in className="org.springframework.web.struts.ContextLoaderPlugIn"> <set-property property="contextConfigLocation" value="/WEB-INF/classes/beans.xml" /> </plug-in> </struts-config>
在WEB-INF目录下新建一个文件struts-order-config.xml,这个文件用于配置我们现在使用的模块。Struts的模块是按照目录划分的。在加入了这个模块以后,相应的web.xml中,给ActionServlet加入如下的参数:
<init-param> <param-name>config/order</param-name> <param-value>struts-order-config.xml</param-value> </init-param>
如果有其它的模块加入的话,只需按照这个格式添加即可。需要注意的是 confg/ 后面的参数必须是模块对应的目录名。这样应用访问特定模块的时候,Struts会自动追加模块的名称。比如现在的登录,页面端表单中action对应的值是/loginDispatcher.do,Struts会自动将它转为/order/loginDispatcher.do。而Struts本身的配置文件在改动模块名的时候,不需要作任何改动。这个应用很小,没有必要使用模块化功能,这里使用它,只是为了演示。这样的需求实际应用中很常见。
Struts-order-config.xml的配置如下:
<struts-config> <form-beans> <form-bean name="userForm" type="cn.zxm.order.form.UserForm"/> </form-beans> <action-mappings> <action path="/loginDispatcher" forward="/login.jsp"/> <action path="/loginAction" input="/login.jsp" type="org.springframework.web.struts.DelegatingActionProxy" name="userForm"> <forward name="fail" path="/loginDispatcher.do"/> <forward name="welcome" path="/orderList.jsp"/> </action> </action-mappings> </struts-config>
orderList.jsp用来显示用户的所有订单。path的命名,应该有一定的约定,比如只是简单映射页面,使用页面名称+Dispatcher的形式;处理页面的请求的路径使用页面名称+Action的形式,等等,这有利于将来我们使用通配符来简化配置。这个问题我们后面会涉及到。
在Spring配置文件中加入Action的定义,bean的名字对应Struts中的path,只不过这个bean的名字包含了模块名:
<bean name="/order/loginAction" class="cn.zxm.order.action.LoginAction"/>
现在我们可以测试登录了,我们在数据库中输入一个用户的信息,用户名和密码都是zxm。打开浏览器,在地址栏输入
http://www.zxm.net:8090/order/order/loginDispatcher.do
,页面如图9-2。
图9-2 登录
输入用户名和密码,输入不正确的用户名或者密码,应该返回登录页面,成功则进入订单列表页面,由于页面端需要用到Hibernate的延迟加载特性,第一次访问会抛出session closed的异常。
现在我们构建订单显示页,订单页的代码如下:
<%@ page language="java" contentType="text/html; charset=GBK" pageEncoding="GBK"%> <%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %> <%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %> <%@ taglib uri="http://struts.apache.org/tags-logic" prefix="logic" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=GBK"> <link rel="stylesheet" href="style.css" /> <title>用户订单列表</title> </head> <body> ${userForm.user.name},您好,您共提交了${fn:length(userForm.user.orders)}个订单<br/> <table width="100%" border="0" align="center" cellpadding="10" cellspacing="1" class="table-bgcolor"> <tr class="table-head"> <th>id</th><th>订单名称</th><th>订单描述</th><th>订单生成日期</th> </tr> <logic:iterate id="order" property="user.orders" name="userForm"> <tr class="table-body"> <td><html:link action="/search_items.do?id=${order.id}">${order.id}</html:link></td> <td>无</td> <td>${order.description}</td> <td>${order.createTime}</td> </tr> </logic:iterate> </table> <html:link action="/search_items.do">购物车</html:link><br/> <html:link action="/itemDispatcher.do" style="button">订购商品</html:link> </body> </html>
这里不但使用了Struts标签,还使用了JSTL,fn定义了一些有用的函数,可以在EL中使用,比如计算集合长度的length。Logic标签库是用于逻辑处理的标签库,iterator用于遍历集合,name属性用于从request、session、pageContext或者application取值,相当于调用它们的getAttribute()方法,property是对应的属性,可以使用表达式的方式,比如这里user.orders,相当于调用对象的getUser().getOrders()方法,这种形式很常见,比如在Spring和ognl中就支持这种形式的表达式。id属性是引用的名称,其作用和下面的变量order相当:
for(Order order: 包含Order对象的集合){ …… }
上面用到的EL语言,前面已经介绍过,这里不再赘述。
这里引人注目的是Orders集合是从User对象中取出的,而在我们的代码中并没有这样的赋值操作。事实上,这里用到了Hibernate的延迟加载特性,因此要求这里用到的User对象必须是一个持久化对象。这要求在处理页面的时候,必须保证Hibernate的Session打开。因此我们应该采用OpenSessionInView的模式,我们可以通过使用Spring提供的OpenSessionInViewFilter来实现这一模式,也可以通过使用Spring提供的OpenSessionInViewInterceptor,二者的作用相同。使用Filter的好处是开发者对它比较熟悉,使用Interceptor的方式要更为优雅。因为我们将来要借助Spring的拦截器实现对用户是否登录的判断,这里我们使用Interceptor的方式。不过这需要借助于SpringMVC来实现,稍显复杂。这就是Struts与Spring整合的第四种方式。详细介绍如下。
我们使用Spring的DispatcherServlet来分发请求,使用这个Servlet需要注册一个Listener,web.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?> <web-app id="WebApp_ID" version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <servlet> <servlet-name>actions</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>actions</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> </web-app>
DispatcherServlet使用的配置文件,默认遵循这样的规则:Servlet的名字-servlet.xml。这里是actions-servlet,它应该位于WEB-INF下,这是一个标准的Spring配置文件。ContextLoaderListener也有需要一个默认的配置文件,名字是applicationContext.xml,也位于WEB-INFO下。我把我的配置信息都放入了applicationContext.xml中。我们需要去掉struts-config.xml中plug-in配置。好了,web.xml的信息就这么多。现在需要修改Spring配置文件applicationContext.xml,在里面添加如下的配置:
<bean id="openSessionInViewInterceptor" class="org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor"> <property name="flushMode" value="0"/> </bean>
flushMode属性设置session的flush方式,这里设为0,表示永远不使用自动flush,这是为了使用声明式事务。
<bean id="strutsWrappingController" class="org.springframework.web.servlet.mvc.ServletWrappingController"> <property name="servletClass"> <value>org.apache.struts.action.ActionServlet</value> </property> <property name="servletName" value="action"/> <property name="initParameters"> <props> <prop key="config">/WEB-INF/struts-config.xml</prop> <prop key="config/order">/WEB-INF/struts-order-config.xml</prop> <prop key="debug">true</prop> </props> </property> </bean>
这里声明了一个Controller,ServletWrappingController是为了Struts专门设计的,作用相当于代理Struts的ActionServlet,下面的属性都很好理解,我们看到和在web.xml的配置差不多,只不过形式的差异。
最后我们需要把Struts的请求路径和Controller关联起来:
<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="interceptors"> <list> <ref bean="openSessionInViewInterceptor" /> </list> </property> <property name="mappings"> <props> <prop key="/*/*.do">strutsWrappingController</prop> </props> </property> </bean>
这样我们就可以使用Hibernate的延迟加载特性和Spring的声明式事务了。注意intercptors属性,我们可以添加自己的拦截器实现,稍后我们会给出一个具体的实例。拦截器的顺序也是要格外留意的。登录成功后页面如图9-3。
图 9-3 用户订单列表
用户注册的流程和登录一样,注册页面和登录一样,这里我们就不在列出,只列出处理注册的Action,代码如下:
public class RegisterAction extends Action { protected final Log log = LogFactory.getLog (RegisterAction.class); private UserService userService; private User user; public void setUserService(UserService userService) { this.userService = userService; } public void setUser(User user) { this.user = user; } /** * @see org.apache.struts.action.Action#execute(org.apache.struts.action.ActionMapping, org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ @Override public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { UserForm uf = (UserForm)form; User u = (User)cloneBean(user); copyProperties(u, uf); uf.reset(mapping, request); userService.save(u); //用户注册失败,则返回失败页面 if(u.getId()==null || u.getId()==0) return mapping.findForward("fail"); //注册成功,就返回订单列表页 uf.setUser(u); request.getSession().setAttribute("user", u); return mapping.findForward("welcome"); } }
注册页面是register.jsp,对应的path是/registerAction。然后我们在登录页面下加一个注册按钮,这样用户就可以从登录页转到注册页。给注册按钮注册一个响应单击的方法,代码如下:
<script language="javascript"> function register(){ window.location="registerDispatcher.do"; } </script>
在struts-order-config.xml添加如下的配置:
<action path="/registerDispatcher" forward="/register.jsp"/> <action path="/registerAction" input="/register.jsp" type="org.springframework.web.struts.DelegatingActionProxy" name="userForm"> <forward name="fail" path="/registerDispatcher.do"/> <forward name="welcome" path="/orderList.jsp"/> </action>
在applicationContext.xml中加入对应的Action声明:
<bean name="/order/registerAction" class="cn.zxm.order.action.RegisterAction"/>
这样就可以进行登录测试了。读者可以测试注册一个中文名,成功后,会发现用户名是乱码,这是由于Tomcat默认使用ISO-8859-1编码,而我们使用的是GBK编码。解决这个问题,针对请求方式,有不同的处理策略。比如对POST请求,我们需要使用过滤器进行重新编码,对于GET请求,需要在Tomcat端口配置中设置编码方式。下面我们分别予以介绍。
我们可以自己实现这个过滤器,不过Spring已经给我们提供了一个现成的实现,它的配置方式如下:
<filter> <filter-name>encodeFilter</filter-name> <filter-class> org.springframework.web.filter.CharacterEncodingFilter </filter-class> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>encoding</param-name> <param-value>GBK</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodeFilter</filter-name> <url-pattern>*.do</url-pattern> </filter-mapping>
forceEncoding参数表示是否进行强制编码,这里当然是true。encoding表示编码方式,这里是GBK。
针对GET请求,我们只需要在监听HTTP请求的端口中加入如下的属性即可:URIEncoding="GBK"。
由于我们在应用中针对页面和Action的命名遵循了统一的规范,所以我们可以使用通配符简化配置。例如访问页面的方式可以使用如下的方式:
<action path="/*Dispatcher" forward="/{1}.jsp"/>
*就是用到的占位符,下面引用的时候使用大括号,包含一个数字,数字表示占位符的位置,注意这个值是从1开始的。例如当用户访问/loginDispatcher的时候,访问到的页面就是login.jsp。
相应的用于处理请求的Action的配置统一修改如下:
<action path="/*Action" input="/{1}.jsp" type="org.springframework.web.struts.DelegatingActionProxy" name="userForm"> <forward name="fail" path="/{1}Dispatcher.do"/> </action>
登录和查看用户订单清单的任务已经完成了,现在我们看一下怎么订购商品。同上面的处理方式一样,我们需要一个输入商品信息的页面,页面的名字是item.jsp,代码如下:
<html:form action="/commit_item.do" method="post"> <table width="400" border="0" align="center" cellpadding="10" cellspacing="1" class="table-bgcolor"> <tr class="table-body"> <td class="table-head">名称</td> <td><html:text property="name" style="width:98%;"/></td> </tr> <tr class="table-body"> <td class="table-head">简介</td> <td><html:textarea property="description" style="width:98%;"/></td> </tr> <tr class="table-body"> <td class="table-head">价格</td> <td><html:text property="price" style="width:98%;"/></td> </tr> <tr class="table-body"> <td class="table-head">数量</td> <td><html:text property="count" style="width:98%;"/></td> </tr> <tr class="table-body"><td></td><td><input type="submit" class="button" value="购买"/></td></tr> </table> </html:form>
处理保存订单的Action代码如下:
public class BuyItemAction extends org.apache.struts.action.Action{ private ItemService itemService; public void setItemService(ItemService itemService) { this.itemService = itemService; } @Override public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { ItemForm itemForm = (ItemForm)form; Item item = new Item(); copyProperties(item, itemForm); //这里将Item的id设为null,是为了使用Hibernate根据id是否为null, //选择是进行保存,还是进行修改 item.setId(null); saveItem(request, item); itemForm.reset(mapping, request); return mapping.findForward("success"); } }
提交订单后,会返回一个购物车的页面,页面的名称是order.jsp,显示用户现在放入购物车中的所有商品。由于这需要一个查询操作,所以我们需要一个新的Action,这个Action事实上会处理两种情况,一种是用户查阅订单内的商品,另一种就是查阅购物车内的商品,二者的区别不过一个是从数据库中读取数据,一个是从session中读取数据。这个Action的代码如下:
public class ViewOrderItemAction extends Action { private OrderService orderService; public void setOrderService(OrderService orderService) { this.orderService = orderService; } @Override public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { OrderForm orderForm = (OrderForm) form; //如果不能获取的id不是0,表明这是一个订单查询 if (orderForm.getId() > 0) { Order order = orderService.getOrder(orderForm.getId()); orderForm.setId(0); orderForm.setOrder(order); return mapping.findForward("viewOrderItem"); } //如果获取的id是0,表明这是查询购物车内的商品 orderForm.setOrder(getOrder(request)); return mapping.findForward("viewOrderItem"); } }
struts-order-config.xml添加如下的配置:
<action path="/commit_*" type="org.springframework.web.struts.DelegatingActionProxy" input="/{1}.jsp" name="{1}Form"> <forward name="success" path="/search_{1}s.do"/> </action> <action path="/search_items" type="org.springframework.web.struts.DelegatingActionProxy" name="orderForm"/>
在applicationContext.xml中需要加入的配置如下:
<bean name="/order/commit_item" class="cn.zxm.order.action.BuyItemAction"/> <bean name="/order/search_items" class="cn.zxm.order.action.ViewOrderItemAction"/>
现在我们可以通过/itemDispatcher.do来访问定购商品页了,如图9-4。
提交商品后,应该返回给用户购物车的页面,显示购物车内的所有商品,这个页面是order.jsp。order.jsp的代码如下:
<table width="400" align="center"> <tr> <td class="title"> <logic:notEmpty name="orderForm" property="order.id"> 查看订单包含的商品信息 </logic:notEmpty> <logic:empty name="orderForm" property="order.id"> 查看购物车内商品信息 </logic:empty> </td> </tr> </table> <table width="100%" border="0" align="center" cellpadding="10" cellspacing="1" class="table-bgcolor"> <tr class="table-head"> <th>商品名称</th><th>商品描述</th><th>商品价格(单位:人民币元)</th><th>购买数量</th> </tr> <logic:iterate id="item" property="order.items" name="orderForm"> <tr class="table-body"> <td>${item.name}</td> <td> ${item.description}</td> <td>${item.price}</td> <td>${item.count}</td> </tr> </logic:iterate> </table> <html:form action="/commit_order.do" method="post"> <table width="100%" border="0" align="center" cellpadding="10" cellspacing="1"> <tr class="table-title"><td class="title">订单描述</td></tr> <tr class="table-body"> <td align="left"> <logic:notEmpty name="orderForm" property="order.id"> ${orderForm.order.description} </logic:notEmpty> <logic:empty name="orderForm" property="order.id"> <html:textarea property="description" style="width:98%;" value="${orderForm.order.description}"/> </logic:empty> </td> </tr> <logic:empty name="orderForm" property="order.id"> <tr> <td><html:submit style="center" value="提交订单"/></td> </tr> </logic:empty> </table> </html:form> <logic:empty name="orderForm" property="order.id"> <html:link action="/itemDispatcher.do"> <logic:empty name="orderForm" property="order.items"> 订购商品 </logic:empty> <logic:notEmpty name="orderForm" property="order.items"> 继续订购 </logic:notEmpty> </html:link><br/> </logic:empty> <html:link action="/search_orders.do">返回首页</html:link>
图9-4 订购商品
logic标签用来处理逻辑的,从字面意思也很好理解,empty判断属性是否为空,notEmpty判断属性是否非空。我们把提交购物车和查询订单的页面做到了一起,为了清晰,读者可以考虑将它分为两个文件。查询订单的页面如图9-5,查询购物车的页面如图9-6。
将商品放入购物车后,下一步就是提交购物车,反映在数据库中,是生成了一个订单,同时将购物车清空。处理提交订单的Action代码如下:
public class CommitOrderAction extends Action { private OrderService orderService; public void setOrderService(OrderService orderService) { this.orderService = orderService; } @Override public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { Order order = getOrder(request); copyProperties(order, form); //设置订单所属的用户 order.setUser((User)request.getSession().getAttribute("user")); //如果购物车内没有商品,不会进行保存操作 if(order.getItems().size()>0)orderService.save(order); form.reset(mapping, request); //清空购物车 clearCart(request); return mapping.findForward("success"); } }
由于提交订单和提交商品有着相似的形式,因此我们使用通配符,不需要再次配置,只需要在applicationContext.xml中加入下面的配置即可:
<bean name="/order/commit_order" class="cn.zxm.order.action.CommitOrderAction"/>
图9-5 查询订单详细信息
我们还忽略了一个问题,就是用户查询订单列表的时候,并非一定是从登录页面开始的,因此,我们还需要一个查询订单列表的Action,代码如下:
public class ViewOrdersAction extends Action { private UserService userService; public void setUserService(UserService userService) { this.userService = userService; } @Override public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { UserForm userForm = (UserForm) form; User u = (User) request.getSession().getAttribute("user"); User user = userService.getUser(u.getId()); userForm.setUser(user); return mapping.findForward("welcome"); } }
图9-6 查询购物车内的信息
struts-order-config.xml的配置信息如下:
<action path="/search_orders" type="org.springframework.web.struts.DelegatingActionProxy" name="userForm"/>
applicationContext.xml需要加入的内容是:
<bean name="/order/search_orders" class="cn.zxm.order.action.ViewOrdersAction"/>
为了访问方便,我们还使用全局性的配置,这里用到了三个,struts-order-config.xml中涉及的配置如下:
<global-forwards> <forward name="login" path="/loginDispatcher.do"/> <forward name="welcome" path="/orderListDispatcher.do"/> <forward name="viewOrderItem" path="/orderDispatcher.do"/> </global-forwards>
这样整个应用就算完成了,剩下的工作就是在页面上加入必要的导航,比如返回首页的链接等。
这个应用似乎还缺少些什么。如果session过期,怎么办?这是一个即为常见的需求,我们固然可以在访问每个action前进行判断,但是这样同样的代码会分布在不同的地方,维护起来很麻烦。重构中,有一条原则,就是"Once and only once",即所谓的相同的代码只能出现一次。这是一条很重要的原则,除了维护的需要外,还有重用的考虑。登录过期的判断,是一个典型的横切的应用,采用传统的方式实现要困难一些,我们可以使用一个基类进行判断,然后让需要这个功能的类继承这个基类,这是一个解决方案。还有一个更好的解决方案,就是使用拦截器,拦截请求。我们可以将它配置在文件中,这样就使用可配置的方式,防止了硬编码,这是一种极为灵活的方式。我们下面就介绍怎样使用拦截器,判断用户的登录信息是否过期。
为了拦截HTTP请求,我们需要实现接口HandlerInterceptor,这个接口有三个方法,boolean preHandle(...),这个方法在处理HTTP请求之前执行,void postHandle(..)在输出页面之前执行,void afterCompletion(...)在请求处理完成之后进行。很显然preHandle()正是需要我们实现的方法。这个拦截器的代码如下:
public class LoginInterceptor implements HandlerInterceptor { private Properties urls = new Properties(); private String suffix; public void setUrls(Properties urls){ this.urls = urls; } public void setSuffix(String suffix){ this.suffix = suffix; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // TODO Auto-generated method stub } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //获取用户请求的URL StringBuffer url = request.getRequestURL(); //取出请求中的action名 String action = url.substring(url.lastIndexOf("/")+1, url.lastIndexOf(".")); //判断这个action是否是不需要进行登录验证, //如果不需要,就返回true,处理流程照常进行 if(matchUrl(action.toLowerCase())){ return true; } //进行登录验证,如果没有登录或session过期,就转到登录页 if(request.getSession().getAttribute("user")==null){ request.getRequestDispatcher(urls.getProperty("login") + "." + suffix).forward(request, response); return false; } return true; } /** * 用来判断url是否需要添加登录验证 */ private boolean matchUrl(String action){ Pattern p = Pattern.compile(urls.getProperty("urlMapping")); Matcher m = p.matcher(action); return m.matches(); } }
为了增加灵活性,action的后缀和需要过滤的url已经登录页面,都使用依赖注入。在appliccationContext.xml首先需要添加拦截器的声明,它和一个普通的声明没有任何区别:
<bean id="loginInterceptor" class="cn.zxm.interceptor.LoginInterceptor"> <property name="urls"> <props> <prop key="login">/order/loginDispatcher</prop> <prop key="urlMapping">[0-9a-zA-Z]*(login|register)[0-9a-zA-Z]*</prop> </props> </property> <property name="suffix" value="do"/> </bean>
接下来的工作就是在urlMapping注册拦截器:
<property name="interceptors"> <list> <ref bean="loginInterceptor"/> <ref bean="openSessionInViewInterceptor" /> </list> </property>
注意拦截器是有顺序的,loginInterceptor显然应该在openSessionInViewInterceptor之前。用户可以在没有登录的情况下,访问一个合法的url,例如/search_items.do,如果能返回到登录页面,证明拦截器配置成功。
最后,为了对这个示例有更为清楚地理解,我们现在列出完整的struts-order-config.xml文件内容:
<?xml version="1.0"?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.3//EN" "http://struts.apache.org/dtds/struts-config_1_3.dtd"> <struts-config> <form-beans> <form-bean name="userForm" type="cn.zxm.order.form.UserForm"/> <form-bean name="orderForm" type="cn.zxm.order.form.OrderForm"/> <form-bean name="itemForm" type="cn.zxm.order.form.ItemForm"/> </form-beans> <global-forwards> <forward name="login" path="/loginDispatcher.do"/> <forward name="welcome" path="/orderListDispatcher.do"/> <forward name="viewOrderItem" path="/orderDispatcher.do"/> </global-forwards> <action-mappings> <action path="/*Dispatcher" forward="/{1}.jsp"/> <action path="/*Action" input="/{1}.jsp" type="org.springframework.web.struts.DelegatingActionProxy" name="userForm"> <forward name="fail" path="/{1}Dispatcher.do"/> </action> <action path="/commit_*" type="org.springframework.web.struts.DelegatingActionProxy" input="/{1}.jsp" name="{1}Form"> <forward name="success" path="/search_{1}s.do"/> </action> <action path="/search_items" type="org.springframework.web.struts.DelegatingActionProxy" name="orderForm"/> <action path="/search_orders" type="org.springframework.web.struts.DelegatingActionProxy" name="userForm"/> </action-mappings> </struts-config>
9.3.3 预期的效果
上面的例子虽然很简单,但是让我们感到还是很杂乱,我们暂时不必理会这个问题,我们需要做的是看看通过整合,是否达到我们预期的效果。我们整合的目的之一就是通过使用Spring的IoC解除代码的耦合,很显然,我们达到了这个目的。第二个,使用Hibernate,在页面端也能够使用延迟加载,同时在请求时保证共享Hibernate Session,以达到提高资源利用率的目的,这些通过使用OpenSessionInVew模式,也得到了很好的解决。此外,我们使用了声明式事务,以一种极为简洁的方式实现了这个平时需要硬编码才能实现的功能。统一对登录信息的判断,是应用中经常遇到的问题,在这里,通过使用拦截器,我们也提供了一种简洁的实现,而且它是可插拔的。
在整合的过程中,通过将Action交给Spring托管,我们得以在web层使用Spring提供的优秀技术,IoC、AOP等,使得web层的功能更单一,结构更清晰,代码也变得更为简洁。当然这种简洁不仅仅限于web层,同样包括持久层,这是Hibernate的威力。通过三者的结合,我们就可以各取所长,从而开发出一个强大而又容易维护的系统。
9.4 小结
Struts是一个单纯的web框架,没有涉及业务层和持久层的东西,因此在持久层实现中,可以自由选择优秀的持久化实现,而不用担心对web层有什么影响。Hibernate是业内顶级的ORM框架,可以作为持久层实现的基础。Struts本身没有IoC和AOP功能,而Spring正是优秀的IoC和AOP框架。因此在一个应用中,采用Spring+Hibernate+Struts的模式就有必要了。
在这个模型中,Struts负责对web请求的处理和响应,Hibernate提供持久化实现,Spring则提供bean的管理,通过AOP提供对横切需求的处理。除此之外,Spring还可以提供一些企业级的服务,例如声明式事务。
实现这个模型的目的,是去耦合的需要,也是扩展的需要,因此三者都很容易进行扩展,同时也是维护的需要,因为它们良好的设计,都很容易维护。由于它们比较流行,因此招聘一个熟悉它们的人也很容易。
我们通过一个购物车的例子演示了怎样进行整合。用户需要登录,如果没有注册,还要进行注册。一个用户需要自己填写商品信息,然后把它放入购物车,最后提交购物车,生成定单。用户可以查询自己的所有订单,也可以查询单个订单内包含的信息,比如订单内的所有商品。这是一个很常见的例子。
我们演示了怎样通过Interceptor使用Spring提供的openSessionInVew模式,从而可以在页面上使用Hibernate的延迟加载。这需要借助于SpringMVC来实现,这种整合方式不同于上一章所提到的三种方式,它要更为复杂,也更为强大。SpringMVC代理了Struts的ActionServlet,从而可以通过Spring拦截HTTP请求,这是通过Spring的拦截器实现的,我们给出了一个用拦截器判断用户是否登录的例子。
可以使用通配符简化Struts的配置,这也要求我们对页面名称和Action路径遵循一定的命名规范。这里同样演示了怎样使用Struts的模块化开发,针对中文乱码问题的处理等。
持久层实现,采用Hibernate,我们使用Spring对Hibernate的封装,简化了持久层的操作。
在整合的过程中,通过将Action交给Spring托管,我们得以在web层使用Spring提供的优秀技术,IoC、AOP等,使得web层的功能更单一,结构更清晰,代码也变得更为简洁。在持久层的代码也同样的简洁。
- strutsOrder.rar (28.1 KB)
- 下载次数: 196
评论
6 楼
e421083458
2013-04-23
人才啊!
5 楼
wxq594808632
2009-04-02
自从学了ssh之后一直没用。现在都快忘完了。赶紧复习下。。
4 楼
慕容轩
2008-11-04
此贴要浮上来!
3 楼
ALLEN仔
2008-10-29
不错,学习下
2 楼
heshencao
2008-10-29
能把源码共享一下吗?
1 楼
avanry
2008-10-29
高手,讲的非常细
不过现在都是struts2.0了
不过现在都是struts2.0了
相关推荐
整合的四大框架项目 spring hibernate struts ajax整合项目源代码 spring hibernate struts ajax整合项目源代码
struts spring hibernate 登陆 SSH整合分页功能 SSH整合分页struts spring hibernate 登陆 SSH整合分页功能 SSH整合分页struts spring hibernate 登陆 SSH整合分页功能 SSH整合分页
《Spring+Struts+Hibernate整合开发》是一本深入讲解企业级Java应用开发的书籍,它主要聚焦于三大著名开源框架——Spring、Struts和Hibernate的集成与应用。这些框架是Java Web开发中的基石,广泛应用于各种复杂的...
标题中的"spring hibernate struts2 整合jar包"指的是一个用于构建Java Web应用程序的集成框架,这个框架集成了Spring、Hibernate和Struts2这三个关键的开源技术。Spring是全面的企业级应用开发框架,提供了依赖注入...
在IT行业中,SSH(Spring、Struts2、Hibernate)是一个经典的Java Web开发框架组合,而Redis则是一个高性能的键值存储系统,常用于缓存和数据持久化。将SSH与Redis整合,可以提升应用程序的性能和响应速度。下面将...
整合Struts1、Spring2和Hibernate2的过程主要包括以下步骤: 1. **配置环境**:确保所有依赖库已添加到项目的类路径中,如struts-core.jar、spring-framework.jar和hibernate-core.jar等。 2. **配置Struts1**:...
【标题】:“Hibernate、Struts2与Spring的整合项目” 【描述】:“这是一个将Hibernate、Struts2和Spring三大框架集成在一起的项目示例。它展示了如何在实际开发中有效地结合这三个强大的Java技术,实现数据持久层...
整合使用最新版本的三大框架(即Struts2、Spring4和Hibernate4),搭建项目架构原型。 项目架构原型:Struts2.3.16 + Spring4.1.1 + Hibernate4.3.6。 此外,还有:log4j、slf4j、junit4、ehcache等知识点。 项目...
在Spring和Struts整合时,Spring可以作为Action的依赖注入容器,提供业务对象给Struts,从而减少代码耦合。 4. **整合过程**: - **配置Spring**:创建Spring的配置文件,如`applicationContext.xml`,定义Bean的...
2. **Struts整合** - 配置Struts的`struts-config.xml`文件,声明Action和ActionForm,设置Action的Forward规则。 - 创建ActionForm类,如`LoginForm`,定义表单字段,与JSP页面中的表单元素对应。 - 创建Action...
1. **Spring 与 Struts 整合**: - **ActionSupport 方式**:将 Struts Action 类继承自 Spring 的 ActionSupport 类,但这样会导致 Struts Action 与 Spring 紧耦合,不利于后期扩展或更换框架。 - **...
在本整合中,Spring 2.5.6版本主要作为业务逻辑的管理和协调者,它可以通过配置文件管理Bean的生命周期,同时与Hibernate和Struts进行无缝集成。 Hibernate 3.2是一个流行的ORM(对象关系映射)工具,它消除了...
在IT行业中,SSH(Struts2、Hibernate、Spring)是一个非常经典的Java Web开发框架整合,被誉为"企业级应用开发的黄金组合"。本教程将详细阐述如何将这三个组件结合在一起,构建一个完整的CRUD(创建、读取、更新、...
整合Spring、Hibernate和Struts,可以创建出高效、模块化且易于维护的企业级Web应用。这个过程通常被称为SSH整合,它是Java开发中的经典组合,尤其是在早期的Java EE项目中非常流行。 首先,Spring作为核心框架,...
SSH整合,全称为Struts、Spring和Hibernate的集成,是一种常见的Java Web开发框架组合,用于构建高效、可维护的企业级应用程序。在这个例子中,我们看到的是一个基于SSH的用户注册功能的实现,使用的开发工具是...
【Spring、Hibernate、Struts整合开发】 在Java应用开发中,Spring、Hibernate和Struts是三个非常重要的框架,它们各自负责不同的职责。Spring作为一个轻量级的容器,提供了依赖注入和面向切面编程(AOP)等功能;...
Spring整合Struts主要有三种方式: 1. **使用Spring的ActionSupport**:Action类直接继承自Spring的ActionSupport,通过`super.getWebApplicationContext()`获取Spring上下文,然后通过`ApplicationContext.getBean...
Struts2、Spring和Hibernate是Java Web开发中的三大框架,它们各自负责不同的职责:Struts2作为MVC框架处理请求和展示,Spring提供依赖注入和事务管理,Hibernate则作为ORM框架处理数据库操作。将这三个框架整合在...
在Struts2.1.6、Spring2.5.6和Hibernate3.3.1的版本中,整合这三个框架时,开发者通常会使用全注解的方式来简化配置。全注解开发意味着在类和方法上使用注解来声明和配置组件,而不是编写XML配置文件。 首先,开发...
集成Hibernate + Spring + Struts + Mybatis,jdk 32的版本