UML软件工程组织

Learn Spring in spring
作者: Bromon

   当前的形势是,非常多的Java程序员言必称Spring,如此大面积的程序员集体叫”春”,体现了Spring框架的威力。

 Spring的设计目的是简化J2EE开发,所以如果我们学习、使用它的时候还需要抓破头皮口吐白沫的话,岂不是个笑话?就我的经验来说,Spring在这方面做得很好,的确是一个很牛叉易用的框架。

 之前我曾经设计过一个J2EE的考试系统,大量使用了EJB(详见http://blog.csdn.net/bromon/archive/2004/08/27/86291.aspx),我打算同样使用一个考试系统做例子,便于比较。两个系统的大致结构都差不多,不过新的版本采用了轻量级的方案,使用Hibernate作为ORM框架,所有的对象都交给spring来管理。

一、IoC与DI

 首先想说说IoC(Inversion of Control,控制倒转)。这是spring的核心,贯穿始终。所谓IoC,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。这是什么意思呢,举个简单的例子,我们是如何找女朋友的?常见的情况是,我们到处去看哪里有长得漂亮身材又好的mm,然后打听她们的兴趣爱好、qq号、电话号、ip号、iq号………,想办法认识她们,投其所好送其所要,然后嘿嘿……这个过程是复杂深奥的,我们必须自己设计和面对每个环节。传统的程序开发也是如此,在一个对象中,如果要使用另外的对象,就必须得到它(自己new一个,或者从JNDI中查询一个),使用完之后还要将对象销毁(比如Connection等),对象始终会和其他的接口或类藕合起来。

 那么IoC是如何做的呢?有点像通过婚介找女朋友,在我和女朋友之间引入了一个第三者:婚姻介绍所。婚介管理了很多男男女女的资料,我可以向婚介提出一个列表,告诉它我想找个什么样的女朋友,比如长得像李嘉欣,身材像林熙雷,唱歌像周杰伦,速度像卡洛斯,技术像齐达内之类的,然后婚介就会按照我们的要求,提供一个mm,我们只需要去和她谈恋爱、结婚就行了。简单明了,如果婚介给我们的人选不符合要求,我们就会抛出异常。整个过程不再由我自己控制,而是有婚介这样一个类似容器的机构来控制。Spring所倡导的开发方式就是如此,所有的类都会在spring容器中登记,告诉spring你是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。如果你还不明白的话,我决定放弃。

 IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后像打针一样,注射到A当中,这样就完成了对各个对象之间关系的控制。A需要依赖Connection才能正常运行,而这个Connection是由spring注入到A中的,依赖注入的名字就这么来的。那么DI是如何实现的呢?Java 1.3之后一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。关于反射的相关资料请查阅java doc。

 理解了IoC和DI的概念后,一切都将变得简单明了,剩下的工作只是在spring的框架中堆积木而已。

二、spring管理对象的简单例子

 任何需要交给spring管理的对象,都必须在配置文件中注册,这个过程被称为wiring,下面做一个最简单的Hello world演示,我们将要注册的类如下:


/*
* 创建日期 2005-3-22
*/
package org.bromon.spring.test;

/**
* @author Bromon
*/
public class HelloTalker
{
public String greeting()
{
return "hello world";
}
}

 然后我们来编写一个spring配置文件,文件名任意,在我这里它是springConfig.xml,需要注意的是这个文件应该存放在classpath所包含的路径中:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
 <bean id=”helloTalker” class=” org.bromon.spring.test.HelloTalker”>
 </bean>
</beans>

 通过使用bean标签,注册了一个HelloTalker对象,它的名字叫做helloTalker。然后我们编写一个测试类,它的工作是利用spring框架提供的接口,加载配置文件,通过指定对象的id,获得一个对象。它的代码如下:

/*
* 创建日期 2005-3-17
*/
package org.bromon.spring.test.junit;

import java.io.FileInputStream;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import org.bromon.spring.test;

/**
* @author Bromon
*/
public class TestStudentManager extends TestCase {

public void testHelloTalker()
{
try
{
ApplicationContext context =new ClassPathXmlApplicationContext("springConfig.xml");

HelloTalker ht=(HelloTalker)context.getBean(“helloTalker”);
System.out.println(ht.greeting());
}catch(Exception e)
{
e.printStackTrace();
}
}

}

 这个程序就完成了,因为只有一个对象HelloTalker被注册到了spring中,所以不存在对象间的依赖,当然也就不涉及依赖注入。下面演示一个简单的依赖注入:

 第一步是修改HelloTalker,增加一个String name属性:

public String name;


 为该属性编写set方法,该方法必须严格遵守javabean的命名规则:

public void setName(String name)
{
  this.name=name;
}

  修改greeting方法:


public String greeting()
{
  return "hello "+name;
}

 如你所见,name属性没有初试化,因为它的值将在运行过程中被spring动态注射入。

 第二步,修改springConfig.xml中唯一的这个bean配置:


<bean id=”helloTalker” class=” org.bromon.spring.test.HelloTalker”>
 <property name=”name”>
  <value>bromon</value>
 </property>
</bean>


 修改完成。我们将一个名字”bromon”写死在springConfig.xml中,它会被动态的注入到HelloTalker的name属性中,greeting方法将会把它打印出来。重新运行刚才的junit类,可以看到结果。

 我们只演示了如何注入一个最简单的String,实际上我们可以注入任何值类型,也可以注入任何类的实例,也可以注入List、Map、Properties。配置文件管理了所有的对象和对象间的关系,而对象则只负责执行自己的功能,他们的职责越少,藕合度越低,系统就越容易测试,管理维护也更容易。

 <bean>标签还有很多属性,用于指定对象如何被实例化,它也有很多子标签用于配置对象的属性,请大家参考相关的DTD和文档,能够很快的掌握。本系列文章不是spring手册,spring的基础知识请参考spring in action,足够详细准确。后面的章节更多的讨论系统设计、开发的一些细节和高级特性。

三、spring中的hibernate开发

 spring中对hibernate的支持是非常强大的,从一个简单的例子就看得出来,从这个例子中我们还将对所谓的轻量级容器做一些讨论。

 首先需要配置数据源,通常我们有两种方式获得Connection,一是自己编写代码获得连接,二是从JNDI环境中得到DataSource,然后产生一个Connection。无论怎样,既然是spring下面的对象,就应该注册到配置文件中。假设我们需要一个连接mysql下面一个叫做examer的数据库,手动方式的配置是:


<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
  <property name="driverClassName">
    <value>com.mysql.jdbc.Driver</value>
  </property>
  <property name="url">
    <value>jdbc:mysql://localhost/examer</value>
  </property>
  <property name="username">
    <value>root</value>
  </property>
  <property name="password">
    <value></value>
  </property>
</bean>


 很好读是不是?假如我们使用JNDI数据源,那么dataSource的声明就应该是:


<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
  <property name="jndiName">
    <value>java:compenvjdbcspringExamer</value>
  </property>
</bean>

 你需要在JNDI环境中绑定一个名为jdbc/springExamer的东西,这段代码才有实际意义。另外需要提醒的是,所有的bean声明,它的id必须是唯一的。

 在本系统中,数据库操作是被hibernate封装起来的,所以dataSource是不需要注入到具体的逻辑类中,它只会被注给hibernate的sessionFactory。

 按照常规思路,我们需要在spring中注册hibernate的sessionFactory,它应该是我们自己编写的一个类,获得dataSource,返回sessionFactory,其他的逻辑类通过这个sessionFactory获得session进行数据库操作。

 但是我们有另外一种选择,spring直接提供了对sessionFactory的封装,你只需要注册一个spring自己的类,给它提供必须的属性,它会返回一个org.springframework.orm.hibernate.HibernateTemplate,这个类封装了add、del等操作,它的封装程度相当高,通过它来编写hibernate应用非常简单。但是问题出来了,我们该如何选择?

 表面上看,使用spring自己的库无疑更加简单,但是请注意,spring是一个轻量级的框架,所谓轻量级,一个重要特征就是无侵入性,也就是你使用这套框架,不会被它绑定,被spring管理的类,应该不需要使用它的接口和抽象类,这样你的系统不会对spring产生依赖。但是如果你使用了spring封装的方式去操作hibernate,就必须继承org.springframework.orm.hibernate.support.HibernateDaoSupport类,这导致了绑定。所以做这样的选择是有点痛苦的,如果有一天spring框架不存在了,你的代码怎么升级维护?具体问题只能具体分析,在我们的应用中,完全使用了spring封装的HibernateTemplate,它太好用了,所以容易上瘾。

 假设我们有一张student表,结构很简单:

  
id      自动增长
  name     varchar(40)
  password   varchar(32)
  grade     int(4)      年级
  sex     Boolean      性别(true为男,false为女)

 设计一个Student类来映射这张表:


/*
* 创建日期 2005-3-17
*/
package net.bromon.spring.examer.pojo;

/**
* @author Bromon
*/
public class Student
{
private int id;
private String name;
private String password;
private int grade;//年级
private boolean sex;

getset方法……….
}

 编写Student.hbm.xml,让hibernate知道如何去关联student表和Student类,该文件和Student.java在同一目录:


<hibernate-mapping>
  <class name="net.bromon.spring.examer.pojo.Student" table="student">
    <id name="id" column="id">
      <generator class="identity"/>
    </id>

    <property name="name" column="name" />
    <property name="password" column="password" />
    <property name="grade" column="grade" />
    <property name="sex" column="sex" />
  </class>
</hibernate-mapping>

 然后我们可以在spring中配置sessionFactory:


<bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
  <property name="dataSource">
    <ref bean="dataSource"/>
  </property>

  <property name="hibernateProperties">
    <props>
      <prop key="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</prop>
    </props>
  </property>

  <property name="mappingDirectoryLocations">
    <list>
      <value>classpath:/netbromonspringexamerpojo</value>
    </list>
  </property>
</bean>

 其中引用了我们之前注册过的dataSource,mappingDirectoryLocations属性指明了.hbm.xml文件在哪里路径,该文件夹下面的.hbm.xml文件会被全部加载。

 一切都准备就绪,现在我们要加入一个StudentManager类,来进行增删查改的操作:

/*
* 创建日期 2005-3-17
*/
package net.bromon.spring.examer.student;

import net.bromon.spring.examer.pojo.Student;

import org.springframework.orm.hibernate.HibernateTemplate;
import org.springframework.orm.hibernate.LocalSessionFactoryBean;
import org.springframework.orm.hibernate.support.HibernateDaoSupport;

/**
* @author Bromon
*/
public class StudentManager extends HibernateDaoSupport
{
private LocalSessionFactoryBean sessionFactory;
private HibernateTemplate ht;
public StudentManager()
{
this.ht=super.getHibernateTemplate();
}

public void add(Student s)
{
ht.save(s);//插入一条数据只需要这一行代码
}
}

 该类只演示了如何增加一个Student,HibernateTemplate还封装了很多有用的方法,请查阅spring文档。StudentManager中的sessionFactory是由spring注入的,但是StudentManager并没有对sessionFactory做任何的处理,这是因为所有的处理都被HibernateDaoSupport.getHibernateTemplate()封装。整个StudentManager中也看不到任何的异常处理,他们也都被基类封装了。

 最后一个步骤就是在spring中注册StudentManger,然后向它注入sessionFactory:


<bean id="studentManager" class="net.bromon.spring.examer.student.StudentManager">
  <property name="sessionFactory">
    <ref bean="sessionFactory"/>
  </property>
</bean>


 所有的配置都完成了,下面做单元测试:


/*
* 创建日期 2005-3-17
*/
package net.bromon.spring.examer.student.test;

import java.io.FileInputStream;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import net.bromon.spring.examer.pojo.Student;
import net.bromon.spring.examer.student.StudentManager;
import junit.framework.TestCase;

/**
* @author Bromon
*/
public class TestStudentManager extends TestCase {

public void testAdd()
{
try
{
ApplicationContext context =new ClassPathXmlApplicationContext("springConfig.xml");

Student s=new Student();
s.setName("bromon");
s.setPassword("123");
s.setGrade(3);
s.setSex(true);

((StudentManager)context.getBean("studentManager")).add(s);
}catch(Exception e)
{
e.printStackTrace();
}
}

}

 Spring已经将hibernate的操作简化到了非常高的程度,最关键的是整个开发可以由设计来驱动,如果一个团队对spring有足够的熟悉,那么完全可以由设计师规划所有的类,整理清楚类之间的关系,写成配置文件,然后编写hibernate映射文件,将数据表与pojo关联,成员就可以完全在设计方案内工作,利用spring封装好的hibernate模版,开发起来速度非常快,调试也很容易。它能够解决如何在团队内贯彻设计方案的问题。

 由于本文不讲解hibernate的使用,所以相关内容请查阅hibernate文档。

四、Spring中的事务控制

 Spring和EJB一样,提供了两种事务管理方式:编程式和声明式。在考试系统中我们将使用声明式的事务管理,这是spring推荐的做法。使用这种方式可以体验到spring的强大便捷,而且我们无须在Dao类中编写任何特殊的代码,只需要通过配置文件就可以让普通的java类加载到事务管理中,这个意义是很重大的。

 Spring中进行事务管理的通常方式是利用AOP(面向切片编程)的方式,为普通java类封装事务控制,它是通过动态代理实现的,由于接口是延迟实例化的,spring在这段时间内通过拦截器,加载事务切片。原理就是这样,具体细节请参考jdk中有关动态代理的文档。本文主要讲解如何在spring中进行事务控制。

 动态代理的一个重要特征是,它是针对接口的,所以我们的dao要通过动态代理来让spring接管事务,就必须在dao前面抽象出一个接口,当然如果没有这样的接口,那么spring会使用CGLIB来解决问题,但这不是spring推荐的方式,我们也不做讨论。

 参照前面的例子,我们为StudentManager.java定义一个接口,它的内容如下:


/*
* 创建日期 2005-3-25
*/
package org.bromon.spring.examer.student;

import java.util.List;

import org.bromon.spring.examer.pojo.Student;

/**
* @author Bromon
*/
public interface StudentManagerInterface
{
public void add(Student s);
public void del(Student s);
public void update(Student s);

public List loadAll();
public Student loadById(int id);
}


 StudentManager也应该做出修改,实现该接口:


public class StudentManager extends HibernateDaoSupport implements StudentManagerInterface

 现在需要修改配置文件,用于定义Hibrenate适用的事务管理器,并且把sessionFactory注入进去,同时还需要通过注册一个DefaultTransactionAttribute对象,来指出事务策略。其中sessionFactory的定义已经在本文的第三章中说明。

 首先定义一个Hibernate的事务管理器,让它来管理sessionFactory:

<bean id="transactionManager" class="org.springframework.orm.hibernate.HibernateTransactionManager">
 <property name="sessionFactory">
  <ref bean="sessionFactory"/>
 </property>
</bean>


 下面定义事务管理策略,我们希望把策略定义在方法这个级别上,提供最大的灵活性,本例中将add方法定义为:PROPAGATION_REQUIRES_NEW,这可以保证它将始终运行在一个事务中。


<bean id="transactionAttributeSource" class="org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource">
 <property name="properties">
  <props>
   <prop key="add">
    PROPAGATION_REQUIRES_NEW
   </prop>
  </props>
 </property>
</bean>


 我们不仅可以为add方法定义事务策略,还可以定义事务隔离程度和回滚策略,他们以逗号隔开,比如我们的add事务可以定义为:


<prop key="add">
PROPAGATION_REQUIRES_NEW,-ExamerException
</prop>


 这个事务策略表示add方法将会独占一个事务,当事务过程中产生ExamerException异常,事务会回滚。

 Add/update/del都是写入方法,对于select(读取)方法,我们可以指定较为复杂的事务策略,比如对于loadAll()方法:

 
<prop key=”loadAll”>
 PROPAGATION_SUPPORTS,ISOLATION_READ_COMMITED,readOnly
 </prop>


 该事务的含义为,loadAll方法支持事务,不会读取未提交的数据,它的数据为只读(可提高执行速度)。

 如你所见,我们的StudentManagerInterface接口中还有一个loadById(int id)方法,也许我们将来还会有很多的loadByXXXX的方法,难道要一一为他们指定事务策略?太烦人了,他们应该和loadAll()一样,所以我们可以使用通配符,定义所有的loadXXXX方法:


<prop key=”load*”>
PROPAGATION_SUPPORTS,ISOLATION_READ_COMMITED,readOnly
</prop>


 现在可以定义事务管理器:

<bean id="studentManager" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
 <property name="target">
  <ref bean="studentManager"/>
 </property>
 <property name="transactionManager">
  <ref bean="transactionManager"/>
 </property>
 <property name="transactionAttributeSource">
  <ref bean="transactionAttributeSource"/>
 </property>
</bean>

 这个bean的外观是一个接口(StudentManagerInterface),我们指出了它的具体实现(studentManager),而且为它绑定了事务策略。在客户端使用的时候,获得对象是StudentManagerInterface,所有的操作都是针对这个接口的。测试代码并没有改变,我们虽然修改了很多地方,加入了事务控制,但是客户端并没有受到影响,这也体现了spring的一些优势。测试代码如下:

  
public void testAdd()
{
ApplicationContext ctx=new ClassPathXmlApplicationContext("springConfig.xml");
StudentManager sm=(StudentManager)ctx.getBean("studentManager");

Student s=new Student();
s.setId(1);
s.setName("bromon");
s.setPassword("123");
s.setGrade(1);
s.setSex(0);

sm.add(s);
}

通过以上的代码可以看出,spring可以简单的把普通的java class纳入事务管理,声明性的事务操作起来也很容易。有了spring之后,声明性事务不再是EJB独有,我们不必为了获得声明性事务的功能而去忍受EJB带来的种种不便。

我所使用的mysql是不支持事务的,你可以更换使用PostgreSQL,有了spring+hibernate,更换db并不像以前那样恐怖了,步骤很简单:

1、 添加PostgreSQL的jdbc驱动
2、 修改dataSource配置,包括驱动名称、url、帐号、密码
3、 修改sessionFactory的数据库dailet为net.sf.hibernate.dialect.PostgreSQLDialect
4、 修改hbm.xml中的主键生成策略为increment

所有的修改都在配置文件中完成,业务代码不需要任何修改,我很满意,How about u?

附A  pring中的所有事务策略

    PROPAGATION_MANDATORY
    PROPAGATION_NESTED
    PROPAGATION_NEVER
    PROPAGATION_NOT_SUPPORTED
    PROPAGATION_REQUIRED
    PROPAGATION_REQUIRED_NEW
    PROPAGATION_SUPPORTS

附B  Spring中所有的隔离策略:

    ISOLATION_DEFAULT
   ISOLATION_READ_UNCOMMITED
    ISOLATION_COMMITED
    ISOLATION_REPEATABLE_READ
    ISOLATION_SERIALIZABLE

 

 

版权所有:UML软件工程组织