【原創】Spring、Hibernate、Struts1整合的方式

  [i][color=olive]完整的例子源码已经作为附件上传,为了减小体积,没有包含用到的jar文件,请读者自己下载,不便处还请见谅。[/color][/i]
  [color=darkblue][b]9.1 模型概述[/b][/color]
  正如我们所知,Struts是一个单纯的web框架,因此对于其他方面的操作无能为力,例如对数据库的操作。此外它也不能管理对象之间的依赖关系,因此需要和持久化框架和IoC框架结合,才能构建一个完整健壮的系统。本节分析Struts+Spring+Hibernate结合的必要性。
  [color=blue]9.1.1 三种模型结合的必要性[/color]
  对于从事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机制。通过整合,我们可以充分利用各个框架的优势,从而达到构建完美的应用的目标。
  [color=darkblue][b]9.2 模型特征[/b][/color]
  我们分析了三种框架结合的必要性,现在我们分析一下使用三个框架的模型特性。
  [color=blue]9.2.1 模型的构成特点[/color]
  在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。
  [color=blue]9.2.2 实现模型的目的[/color]
  使用Spring+Hibernate+Struts模型的目的,实际上面已经涉及到了,这些框架的优点是使用它们的一个重要原因。还有一些可能更重要的原因,总结如下:
  去耦合 这是一个老生常谈的话题了。去耦合是如此的重要,以至于人们要将这方面的经验总结成设计模式。Spring通过IoC,实现了去耦合,这样用户可以随时替换现在的实现,而不用修改源码。这正是软件开发中开-闭原则的要求。Spring倡导面向接口编程,这是最为推崇的最佳实践之一,继承仅仅作为内部实现的一个手段。面向接口编程,不仅仅是提供了更好的抽象,实际上也更有利于去耦合。
  扩展的需要 Struts、Spring甚至Hibernate都提供了良好的扩展功能,Struts通过插件机制,可以很方便的添加功能。Spring对Bean的管理也提供了扩展,例如用户可以提供自己的作用域。Hibernate用户可以定制自己的持久化策略。
  维护的需要 这些框架都很容易维护,它们都很好的实现了开-闭原则,因此需要替换功能的时候,只需简单的修改配置文件。它们提供的一些公用功能,也能通过配置的方式提供,比如Spring的声明式事务。因此维护成本大大降低。
  还有一个原因,由于它们都有庞大的社区支持,因此寻找技术支持很容易。由于它们很流行,因此招一个开发人员也很容易。
  [color=darkblue][b]9.3 Hibernate+Spring+Struts模型应用分析[/b][/color]
  通过上面的学习,我们对三者结合的模型有了一个概念性的理解,现在我们通过一个具体的例子,演示怎样将它们整合在一起。
  [color=blue]9.3.1 应用事例[/color]
  我们考虑一个购物车的例子,相信读者或多或少都接触过这方面的例子。为了简单起见,我们不考虑库存的问题,商品的信息完全由用户填写,包括商品名称、商品介绍、商品价格、购买数量。填写完商品信息,会被暂时放入购物车内,每个购物车抽象成一个订单。当用户确认购买的商品后,就可以提交购物车,从而这些信息被存入数据库,购物车清空。在做这些操作之前,用户首先需要登录,如果用户没有注册,可以选择注册。用户不仅可以购买商品,还可以查看已经提交的订单的详细信息。一个用户可以有多个订单,一个订单只能属于一个用户。同理,一个订单可以包含多个商品,而一个商品只能属于一个订单。用户的姓名是唯一的,因为这是确认用户的唯一凭证。这就是全部的需求。
  [color=blue]9.3.2 事例分析[/color]
  好,现在我们开始构建整个应用。应用用到的类库如图9-1所示:
[img]http://zxmzfbdc.iteye.com/upload/attachment/45829/22eb2be9-8898-3b61-a224-05f374e4d782.bmp[/img]
图9-1 应用用到的jar包
  首先,我们需要建立所需的数据库表,我们用到的表有三个,每个表都有一个用于标识的id,这三个表分别是:
[list]
[*]tb_user——用户表,用来存放用户信息,包括用户名和密码;
[*]tb_order——订单表,用来存放订单信息,包括订单描述、生成时间、用户id;
[*]tb_item——商品表,用来存放商品信息,包括商品名称、商品描述、商品价格、购买数量、定单id。
[/list]
  我用的数据库是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同名。注意包名的命名,通过包,我们可以将类进行分组,便于管理。我们在开发中,应该重视包的管理,至少在一个应用中,应该有统一的命名规范。这里使用的命名习惯和放置的文件是这样的:
[list]
[*]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——应用用到的工具类。
[/list]
  这里我们只列出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。
[img]http://zxmzfbdc.iteye.com/upload/attachment/45832/daa5ac31-77f2-3ded-b81f-f80a517575e6.jpg[/img]
图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。
[img]http://zxmzfbdc.iteye.com/upload/attachment/45834/83502391-71ce-3038-b52c-c4466a7d510f.jpg[/img]
图 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>

[img]http://zxmzfbdc.iteye.com/upload/attachment/45840/bd463250-db67-3d77-b545-5ce323353475.jpg[/img]
图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"/>

[img]http://zxmzfbdc.iteye.com/upload/attachment/45842/8930cdf9-34db-37fe-ae97-d30e268fdc48.jpg[/img]
图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");
}
}

[img]http://zxmzfbdc.iteye.com/upload/attachment/45845/6861e0dd-d63f-36dc-a5ea-49dab50da60b.jpg[/img]
图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>

  [color=blue]9.3.3 预期的效果[/color]
  上面的例子虽然很简单,但是让我们感到还是很杂乱,我们暂时不必理会这个问题,我们需要做的是看看通过整合,是否达到我们预期的效果。我们整合的目的之一就是通过使用Spring的IoC解除代码的耦合,很显然,我们达到了这个目的。第二个,使用Hibernate,在页面端也能够使用延迟加载,同时在请求时保证共享Hibernate Session,以达到提高资源利用率的目的,这些通过使用OpenSessionInVew模式,也得到了很好的解决。此外,我们使用了声明式事务,以一种极为简洁的方式实现了这个平时需要硬编码才能实现的功能。统一对登录信息的判断,是应用中经常遇到的问题,在这里,通过使用拦截器,我们也提供了一种简洁的实现,而且它是可插拔的。
  在整合的过程中,通过将Action交给Spring托管,我们得以在web层使用Spring提供的优秀技术,IoC、AOP等,使得web层的功能更单一,结构更清晰,代码也变得更为简洁。当然这种简洁不仅仅限于web层,同样包括持久层,这是Hibernate的威力。通过三者的结合,我们就可以各取所长,从而开发出一个强大而又容易维护的系统。
  [color=darkblue][b]9.4 小结[/b][/color]
  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层的功能更单一,结构更清晰,代码也变得更为简洁。在持久层的代码也同样的简洁。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值