• 本专题内容主要包含两部分:Spring所使用的操作数据库的技术之一,JDBC模版的使用;另一部分则为Spring对于事务的管理。
  • Spring与Dao部分,是Spring的两大核心技术loC与AOP的经典应用体现:
  • 对于JDBC模版的使用,是loC的应用,是将JDBC模版对象注入给Dao层的实现类。
  • 对于Spring的事务管理,是AOP的应用,将事务作为切面织入到Service层的业务方法中。

    1 Spring与JDBC模版

  • 为了避免直接使用JDBC而带来的复杂且亢长的代码,Spring提供了一个强有力的模版类--JdbcTemplate来简化JDBC操作。并且,数据源DataSource对象和模版JdbcTemplate对象均可通过Bean的形式定义在配置文件中,充分发挥了依赖注入的威力。

    1.1 导入Jar包

    1、Spring的JDBC Jar包:
    SSH框架之Spring4专题4:Spring与DAO
    2、Spring的事务Jar包:
    SSH框架之Spring4专题4:Spring与DAO

    1.2 搭建测试环境

    1、定义实体类User:

    public class User {
        private Integer id;
        private String username;
        private int age;
        //setter and getter()
        @Override
        public String toString() {
            return "User [id=" + id + ", username=" + username + ", age=" + age + "]";
        }
    }

    2、定义数据库以及表:
    SSH框架之Spring4专题4:Spring与DAO
    3、定义IUserDao:

    public interface IUserDao {
    void insertUser(User user);
    void deleteUser(int id);
    void updateUser(User user); 
    String selectUsernameById(int id);
    List<String> selectUsernamesByAge(int age);
    User selectUserById(int id);
    List<User> selectAllUsers();
    }

    4、初步定义UserDaoImpl:

  • 这里仅仅定义一个UserDaoImpl类实现了IUserDao接口,但不具体写每个方法的方法实现。保持默认即可。后面会逐个通过Jdbc模版来实现。
    public class UserDaoImpl implements IUserDao{
    @Override
    public void insertUser(User user) {
    }
    //...
    }

    5、定义IUserService:

    public interface IUserService {
    void addUser(User user);
    void removeUser(int id);
    void modifyUser(User user); 
    String findUsernameById(int id);
    List<String> findUsernamesByAge(int age);
    User findUserById(int id);
    List<User> findAllUsers();
    }

    6、定义UserService:

    public class UserServiceImpl implements IUserService{
    private IUserDao dao;
    public void setDao(IUserDao dao) {
        this.dao = dao;
    }
    @Override
    public void addUser(User user) {
        dao.insertUser(user);
    }
    //...
    }

    7、定义测试类MyTest:

    public class MyTest {
    private IUserService service;
    @Before
    public void setUp() {
        String resource = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
        service = (IUserService) ac.getBean("myService");
    }
    @Test
    public void testInsert() {
        User user = new User("张三", 23);
        service.addUser(user);
    }
    }

    1.3 数据源的配置

  • 使用JDBC模版,首先需要配饰号数据源,数据源直接以Bean的形式配置在Spring配置文件中。根据数据源的不同,其配置方式也不同。下面主要讲解三种常用数据源的配置方式:
    1、Spring默认的数据源;
    2、DBCP数据源;
    3、C3P0数据源;

    1.3.1 Spring默认的数据源DriverManagerDataSource

  • Spring默认的数据源为DriverManagerDataSource,其有一个属性DriverClassName,用于接收DB驱动。
  • Ctrl + O查看类结构以及源码:
    SSH框架之Spring4专题4:Spring与DAO
  • DriverManagerDataSource类继承自AbstractDriverBasedDataSource。其有三个属性用于接收连接数据库的URL、用户名和密码。
  • Ctrl + O查看父类的类结构和源码:
    SSH框架之Spring4专题4:Spring与DAO
    <!-- 配置Spring默认数据源 -->
    <bean id="myDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql:///test"></property>
        <property name="username" value="root"></property>
        <property name="password" value="02000059"></property>
    </bean>

    1.3.2 DBCP数据源BasicDataSource

  • DBCP,DataBase Connection Pool,是apache下的项目,使用该数据源,需要导入两个Jar包。它们在Spring依赖库的解压目录的org.apache.commons目录中dbcp与pool子包中。
  • com.springsource.org.apache.commons.dbcp-1.2.2.osgi.jar;
  • com.springsource.org.apache.commons.pool-1.5.3.jar;
    SSH框架之Spring4专题4:Spring与DAO
  • DBCP数据源是BasicDataSource,Ctrl + O查看其类结构可以看到,其有driverClassName、url、username、password四个DB连接属性。
    SSH框架之Spring4专题4:Spring与DAO
    <!-- 配置DBCP数据源-->
    <bean id="myDBCPDataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql:///test"></property>
        <property name="username" value="root"></property>
        <property name="password" value="02000059"></property>
    </bean>

    1.3.3 C3P0数据源ComboPooledDataSource

  • 使用C3P0数据源,需要导入一个Jar包,在Spring依赖库的解压目录的com.mchange.c3p0目录中。
    SSH框架之Spring4专题4:Spring与DAO
  • C3P0数据源是ComboPooledDataSource,Ctrl + 0 查看其类结构可以看到,其有driverClass、jdbcUrl、user、password四个DB连接属性
    SSH框架之Spring4专题4:Spring与DAO
    <!-- 配置C3P0数据源-->
    <bean id="myC3P0DataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql:///test"></property>
        <property name="user" value="root"></property>
        <property name="password" value="02000059"></property>
    </bean>

    1.4 从属性文件中读取数据库连接信息

  • 为了便于维护,可以将数据库连接信息写入到属性文件中,使得Spring配置文件从中读取数据。
  • 属性文件名称随意,但是一般都是放在src下。
    SSH框架之Spring4专题4:Spring与DAO
  • Spring配置文件从属性文件中读取数据时,需要在<property/>的value属性中使用${},将在属性文件中定义的key括起来,以引用指定属性的值。
    <!-- 配置C3P0数据源-->
    <bean id="myC3P0DataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driver}"></property>
        <property name="jdbcUrl" value="${jdbc.url}"></property>
        <property name="user" value="${jdbc.user}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>
  • 该属性文件若要被Spring配置文件读取,其必须在配置文件中进行注册。注册方式有两种。

1、<bean/>方式 - 使用class为PropertyPlaceholdConfigurer

  • 以PropertyPlaceholdConfigurer类的bean实例的方式进行注册。该类有一个属性location,用于指定属性文件的位置。这种方式不常用。
    <!-- 注册属性文件方式一 -->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:jdbc.properties"></property>
    </bean>

2、<context:property-placehlder/>方式

  • 该方式要求在Spring配置文件头部加入context的约束,即修改配置文件头。
    SSH框架之Spring4专题4:Spring与DAO
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- bean definitions here -->
    </beans>
  • <context:property-placeholder/>标签中有一个属性location,用于指定属性文件的位置。
    <!-- 注册属性文件方式二 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    1.5 配置JDBC模版

  • JDBC模版类JdbcTemplate从其父类中JdbcAccessor继承了一个属性dataSource,用于接收数据源。
  • 查看JdbcTemplate源码,以及JdbcAccessor的类结构:
    SSH框架之Spring4专题4:Spring与DAO
    SSH框架之Spring4专题4:Spring与DAO
    <!-- 配置JDBC模版 -->
    <bean id="myJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="myC3P0DataSource"></property>
    </bean>

    1.6 Dao实现类继承JdbcDaoSupport类

  • JdbcDaoSupport类中有一个属性JdbcTemplate,用于接收JDBC模版。所以Dao实现类继承了JdbcDaoSupport类后,也就具有了JDBC模版属性。在配置文件中,只要将模版对象注入即可。
    SSH框架之Spring4专题4:Spring与DAO
    <!-- 配置JDBC模版 -->
    <bean id="myJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="myC3P0DataSource"></property>
    </bean>
    <!-- 配置Dao -->
    <bean id="myDao" class="com.eason.spring4.dao.impl.UserDaoImpl">
        <property name="jdbcTemplate" ref="myJdbcTemplate"></property>
    </bean>
  • 再仔细查看JdbcDaoSupport类,发现其有一个dataSource属性,查看setDataSource()方法体可知,若Jdbc模版为null,则自动会创建一个模版对象。
    SSH框架之Spring4专题4:Spring与DAO
  • 所以,在Spring配置文件中,对于JDBC模版对象的配置完全可以省去,而是在Dao实现类中直接注入数据源对象。这样会让系统自动创建JDBC模版对象。
    <!-- 配置Dao -->
    <bean id="myDao" class="com.eason.spring4.dao.impl.UserDaoImpl">
        <property name="dataSource" ref="myC3P0DataSource"></property>
    </bean>

    1.7 对于DB的增、删、改操作

  • JdbcTemplate类中提供了对DB进行修改、查询的方法。Dao实现类使用继承自JdbcDaoSupport的getTemplate()方法,可以获取到JDBC模版方法。
    public interface IUserDao {
    void insertUser(User user);
    void deleteUser(int id);
    void updateUser(User user); 
    String selectUsernameById(int id);
    List<String> selectUsernamesByAge(int age);
    User selectUserById(int id);
    List<User> selectAllUsers();
    }
  • 对DB的增删改都是通过update()方法实现的。该方法常用的额重载方法有两个:
  • public int update(String sql);
  • public int update(String sql, Object ...args);
  • 第一个参数为要执行的sql语句,第2个参数为要执行的sql语句中所包含的动态参数。其返回值为所影响记录的条数,一般不使用。
    public class UserDaoImpl extends JdbcDaoSupport implements IUserDao{
    @Override
    public void insertUser(User user) {
        String sql = "insert into t_user(username, age) values(?, ?)";
        this.getJdbcTemplate().update(sql, user.getUsername(), user.getAge());
    }
    @Override
    public void deleteUser(int id) {
        String sql = "delete from t_user where id=?";
        this.getJdbcTemplate().update(sql, id);
    }
    @Override
    public void updateUser(User user) {
        String sql = "update t_user set username = ?, age = ? where id = ?";
        this.getJdbcTemplate().update(sql, user.getUsername(), user.getAge(), user.getId());
    }
    }

    1.8 对DB的查询操作

  • JDBC模版的查询结果均是以对象的形式返回。根据返回对象的不同,可以将查询分为两类:简单对象查询,与自定义对象查询。
  • 简单对象查询,查询结果为String、Integer等简单对象类型,或者该类型作为元素的集合的类型,如List<String>等。
  • 自定义对象查询:查询结果为自定义类型,如User等,或者该类型作为元素的集合类型,如List<User>等。

    1.8.1 简单对象查询

  • 常用的简单对象查询方法有:查询结果为单个对象的queryForObject()与查询结果为List的queryForList()。
  • public T queryForObject(String sql, Class<T> type, Object... args);
  • public List<T> queryForList(String sql, Class<T> type, Object ...args);
    @Override
    public String selectUsernameById(int id) {
        String sql = "select username from t_user where id = ?";
        String username = this.getJdbcTemplate().queryForObject(sql, String.class, id);
        return username;
    }
    @Override
    public List<String> selectUsernamesByAge(int age) {
        String sql = "select username from t_user where age = ?";
        List<String> allUsernames = this.getJdbcTemplate().queryForList(sql, String.class, age);
        return allUsernames;
    }

    1.8.2 自定义对象查询

  • 常用的自定义对象查询方法有:查询结果为单个对象的queryForObject()与查询结果为List的query()。
  • public T queryForObject(String sql, RowMapper<T> m, Object ...args)
  • public List<T> query(String sql, RowMapper<T> m , Object ...args)
  • 注意,RowMapper为记录映射接口,用于将查询结果中每一条记录包装成指定对象。该接口中有一个方法需要实现。
  • public Object mapRow(ResultSet rs, int rowNum):参数rowNum表示总的结果集中当前行的行号,但是参数rs并不表示总的结果集,而是表示rowNum所代表的当前行的记录所定义的结果集,仅仅是当前行的结果。
  • 一般来说,该方法体中就是实现将查询结果中当前行的数据包装为一个指定对象。
    public class UserRowMapper implements RowMapper<User>{
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getInt("id"));
        user.setUsername(rs.getString("username"));
        user.setAge(rs.getInt("age"));
        return user;
    }
    }
    @Override
    public User selectUserById(int id) {
        String sql = "select * from t_user where id = ?";
        User user = this.getJdbcTemplate().queryForObject(sql, new UserRowMapper(), id);
        return user;
    }
    @Override
    public List<User> selectAllUsers() {
        String sql = "select * from t_user";
        List<User> users = this.getJdbcTemplate().query(sql, new UserRowMapper());
        return users;
    }

    1.9 注意:JDBC模版对象时多例的

  • JdbcTemplate对象是多例的,即系统会为每一个使用模版对象的线程(方法)创建一个JdbcTemplate实例,并且在该线程(方法)结束时,自动释放JdbcTemplate实例。所以在每次使用JdbcTemplate对象时,都需要通过getJdbcTemplate()方法获取。

    2 Spring的事务管理

  • 事务原来是数据库中的概念,在Dao层。但是一般情况下,需要将事务提升到业务层,即Service层。这样做是为了能够使用事务的特性来管理具体的业务。
  • 在Spring中通常可以通过以下三种方式来实现对事务的管理:
    1、使用Spring的事务代理工厂管理事务;
    2、使用Spring的事务注解管理事务;
    3、使用AspectJ的AOP配置管理事务;

    2.1 Spring事务管理API

  • Spring的事务管理,主要用到两个事务相关的接口。

    2.1.1 事务管理器接口

  • 事务管理器是PlatformTransactionManager接口对象。其主要用于完成事务的提交、回滚、以及获取事务的状态信息。查看SpringAPI帮助文档:Spring框架解压目录下的docs/javadoc-api/index.html。
    SSH框架之Spring4专题4:Spring与DAO
    1、常用的两个实现类:
  • PlatformTransactionManager接口有两个常用的实现类:
  • DataSourceTransactionManager:使用JDBC或者iBatis进行持久化数据时使用。
  • HibernateTransactionManager:使用Hibernate进行持久化数据时使用。

2、Spring的回滚方式:

  • Spring事务的默认回滚方式是:发生运行时异常时回滚,发生受查异常时提交。不过,对于受查异常,程序员也可以手工设置其回滚方式。

3、回顾错误和异常:
SSH框架之Spring4专题4:Spring与DAO

  • Throwable类是Java语言中所有错误或者异常的超类。只有当对象时此类(或者其子类之一)的实例时,才能够通过Java虚拟机或者Java的throw语句抛出。
  • Error是程序在运行过程中出现的无法处理的错误。比如OutOfMemoryError、ThreadDeath、NoSuchMethodError等。当这些错误发生时,程序是无法处理(捕获或者抛出)的,JVM一般会终止线程。
  • 程序在编译和运行时出现的另一类错误称之为异常,它是JVM通知程序员的一种方式。通过这种方式,让程序员知道已经或者可能出现错误,要求程序员对其进行处理。
  • 异常分为运行时异常和受查异常。
  • 运行时异常,是RuntimeException类或者其子类,即只有在运行时才能够出现的异常。如,NullPointerException、ArrayIndexOutOfBoundException、IllegalArgumentException等均属于运行时异常。这些异常由JVM抛出,在编译时不要求必须处理(捕获或者抛出)。但是,只要代码编写足够仔细,程序足够健壮,运行时异常是可以避免的。
  • 注意,Hibernate异常HibernateException就属于运行时异常。
  • 受查异常,也叫作编译时异常,即在代码编写时要求必须捕获或者抛出的异常。若不处理,则无法通过编译。如SQLException、ClassNotFoundException、IOException等都属于受查异常。
  • RuntimeException及其子类以外的异常,均属于受查异常。当然,用户自定义的Exception的子类,即用户自定义的异常也属于受查异常。程序员在定义异常时,只要未明确声明定义为RuntimeException的子类,那么定义的就是受查异常。

    2.1.2 事务定义接口

  • 事务定义接口TransactionDefinition中定义了事务描述相关的三类常量:事务隔离级别、事务传播行为、事务默认超时时限,以及对它们的操作。

1、定义了五个事务隔离级别常量:

  • 这些常量均是以ISOLATION_开头,即形如ISOLATION_XXX。
  • DEFAULT:采用DB默认的事务隔离级别。MySQL的默认为REPEATABLE_READ:Oracle默认为READ_COMMITTED。
  • READ_UNCOMMITTED:读未提交。未解决任何并发问题。
  • READ_COMMITTED:读已提交。解决脏读,存在不可重复读和幻读。
  • REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读。
  • SERIALIZABLE:串行化。不存在并发问题。

2、定义七个事务传播行为常量:

  • 所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A事务中的方法doSome()调用B事务中的方法doOther(),在调用执行期间事务的维护情况,就称之为事务传播行为。事务传播行为是加在方法上的。
  • 事务传播行为常量都是以PROPAGATION_开头的,形如PROPAGATION_XXX。

  • REQUIRED:指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常用的选择,也是Spring默认的事务传播行为。
  • 如该传播行为加在doOther()方法上。若doSome()方法在调用doOther()方法时就是在事务内运行的,则doOther()方法的执行也加入到该事务内执行。若doSome()方法在调用doOther()方法时没有在事务内执行,则doOther()方法会创建一个事务,并在其中执行。
    SSH框架之Spring4专题4:Spring与DAO

  • SUPPORTS:指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。
    SSH框架之Spring4专题4:Spring与DAO

  • MANDATORY:指定的方法必须在当前事务内执行,若当前没有事务,则直接抛出异常。
    SSH框架之Spring4专题4:Spring与DAO

  • REQUIRES_NEW:总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
    SSH框架之Spring4专题4:Spring与DAO

  • NOT_SUPPORTED:指定的方法不能在事务环境下执行,若当前存在事务,就将当前事务挂起。
    SSH框架之Spring4专题4:Spring与DAO

  • NEVER:指定的方法不能在事务环境下执行,若当前存在事务,就直接抛出异常。
    SSH框架之Spring4专题4:Spring与DAO

  • NESTED:指定的方法必须在事务内执行。若当前存在事务,则再嵌套事务内部执行;若当前没有事务,则新建一个新事务。
    SSH框架之Spring4专题4:Spring与DAO

3、定义了默认事务超时时限:

  • 常量TIMEOUT_DEFAULT定义了事务底层默认的超时时限,以及不支持事务超时时限设置的none值。
  • 注意,事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该值一般就使用默认值即可。

2.2 程序举例环境搭建

  • 举例:购买股票----transaction_buystock项目。
  • 本例要实现模拟购买股票。存在两个实体:银行账户Account与股票账户Stock。当要购买股票时,需要从Account中扣除相应金额的存款,然后在Stock中增加相应的股票数量。而在这个过程中,可能会抛出一个用户自定义的异常。异常的抛出,将会使得两个操作回滚。
  • 实现步骤:

    2.2.1 创建数据库表

  • 创建两个数据库表account、stock。
    SSH框架之Spring4专题4:Spring与DAOSSH框架之Spring4专题4:Spring与DAO

    2.2.2 创建实体类

  • 创建实体类Account与Stock
    public class Account {
    private Integer id;
    private String aname;
    private double balance;
    //setter and getter()
    @Override
    public String toString() {
        return "Account [id=" + id + ", aname=" + aname + ", balance=" + balance + "]";
    }
    }
    public class Stock {
    private Integer id;
    private String sname;
    private int count;
    //setter and getter()
    @Override
    public String toString() {
        return "Stock [id=" + id + ", sname=" + sname + ", count=" + count + "]";
    }   
    }

    2.2.3 定义dao接口

  • 定义两个dao的接口IAccountDao与IStockDao。
    public interface IAccountDao {
    void insertAccount(String aname, double money);
    void updateAccount(String aname, double money);
    Account selectAccount(String aname);
    }
    public interface IStockDao {
    void insertStock(String sname, int count);
    void updateStock(String snaem, int count);
    Stock selectStock(String sname);
    }

    2.2.4 定义dao实现类

  • 定义两个dao接口的实现类AccountDaoImpl与StockDaoImpl,注意,它们要继承自JdbcDaoSupport
    public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao{
    @Override
    public void insertAccount(String aname, double money) {
        String sql = "insert into t_account(aname, balance) values(?, ?)";
        this.getJdbcTemplate().update(sql, aname, money);
    }
    @Override
    public void updateAccount(String aname, double money) {
        String sql = "update t_account set balance = balance - ? where aname = ?";
        this.getJdbcTemplate().update(sql, money, aname);
    }
    @Override
    public Account selectAccount(String aname) {
        String sql = "select * from t_account where aname = ?";
        return this.getJdbcTemplate().queryForObject(sql, Account.class, aname);
    }
    }
    public class StockDaoImpl extends JdbcDaoSupport implements IStockDao{
    @Override
    public void insertStock(String sname, int count) {
        String sql = "insert into t_stock(sname, count) values(?, ?)";
        this.getJdbcTemplate().update(sql, sname, count);
    }
    @Override
    public void updateStock(String sname, int count) {
        String sql = "update t_stock set count = count + ? where sname =?";
        this.getJdbcTemplate().update(sql, count, sname);
    }
    @Override
    public Stock selectStock(String sname) {
        String sql = "select * from t_stock where sname = ?";
        return this.getJdbcTemplate().queryForObject(sql, Stock.class, sname);
    }
    }

    2.2.5 定义异常类

  • 定义service层可能会抛出的异常类StockException。
    public class StockException extends Exception {
    public StockException() {
        super();
    }
    public StockException(String message) {
        super(message);
    }
    }

    2.2.6 定义Service接口

  • 定义Service接口IStockProcessService:
    public interface IStockProcessService {
    void openAccount(String aname, double money);
    void openStock(String sname, int count);
    //aname银行账户花money元购买sname的股票amount支
    void buyStock(String aname, String sname, double money, int amount) throws StockException;
    Account findAccount(String aname);
    Stock findStock(String sname);  
    }

    2.2.7 定义service的实现类

  • 定义service层接口的实现类StockProcessServiceImpl:
    public class StockProcessServiceImpl implements IStockProcessService{
    private IAccountDao aDao;
    private IStockDao sDao;
    // setter()此处省略了
    @Override
    public void openAccount(String aname, double money) {
        aDao.insertAccount(aname, money);
    }
    @Override
    public void openStock(String sname, int count) {
        sDao.insertStock(sname, count);
    }
    @Override
    public void buyStock(String aname, String sname, double money, int amount) throws StockException {
        aDao.updateAccount(aname, money);
        if(1 == 1) {
            throw new StockException();
        }
        sDao.updateStock(aname, amount);
    }
    @Override
    public Account findAccount(String aname) {
        return aDao.selectAccount(aname);
    }
    @Override
    public Stock findStock(String sname) {
        return sDao.selectStock(sname);
    }
    }

    2.2.8 Spring配置文件中添加约束和配置

  • 本例中将使用到Spring中DI、AOP、事务等众多功能,所以将之前用过的所有约束进行了综合。综合后的约束为:(并添加了本例中Spring配置文件内容)

    <?xml version="1.0" encoding="utf-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:context="http://www.springframework.org/schema/context"
            xmlns:aop="http://www.springframework.org/schema/aop"
            xmlns:tx="http://www.springframework.org/schema/tx"
            xsi:schemaLocation="http://www.springframework.org/schema/beans
                    http://www.springframework.org/schema/beans/spring-beans.xsd
                    http://www.springframework.org/schema/context
                    http://www.springframework.org/schema/context/spring-context.xsd
                    http://www.springframework.org/schema/aop
                    http://www.springframework.org/schema/aop/spring-aop.xsd
                    http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd">
        <!-- 注册属性文件方式  -->
        <context:property-placeholder location="classpath:jdbc.properties"/>
    
        <!-- 配置C3P0数据源-->
        <bean id="myC3P0DataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <property name="driverClass" value="${jdbc.driver}"></property>
            <property name="jdbcUrl" value="${jdbc.url}"></property>
            <property name="user" value="${jdbc.user}"></property>
            <property name="password" value="${jdbc.password}"></property>
        </bean>
    
        <!-- 配置Dao -->
        <bean id="myaDao" class="com.eason.spring.dao.impl.AccountDaoImpl">
            <property name="dataSource" ref="myC3P0DataSource"></property>
        </bean>
        <bean id="mysDao" class="com.eason.spring.dao.impl.StockDaoImpl">
            <property name="dataSource" ref="myC3P0DataSource"></property>
        </bean>
    
        <!-- 配置Service -->
        <bean id="myService" class="com.eason.spring.service.impl.StockProcessServiceImpl">
            <property name="aDao" ref="myaDao"></property>
            <property name="sDao" ref="mysDao"></property>
        </bean>
    </beans>

    2.2.9 定义测试类

  • 定义测试类MyTest,现在就可以在无事务代理的情况下运行。
    public class MyTest {
        private IStockProcessService service;
        @Before
        public void setUp() {
            String recource = "applicationContext.xml";
            ApplicationContext ac = new ClassPathXmlApplicationContext(recource);
            service = (IStockProcessService) ac.getBean("myService");
        }   
        @Test
        public void test01() {
            service.openAccount("a", 2000);
            service.openStock("s", 100);
        }
        @Test
        public void test02() throws StockException {
            service.buyStock("a", "s", 500, 100);
        }
    }

    2.3 使用Spring的事务代理工厂管理事务

  • 该方式是,需要为目标类,即Service的实现类创建事务代理。事务代理使用的类是TransactionProxyFactoryBean,该类需要初始化如下一些属性:
    1、transactionManager:事务管理器;2、target:目标对象,即Service实现类对象;3、transactionAttributes:事务属性设置。
  • 对于XML配置代理方式实现事务管理时,受查异常的回滚方式,程序员可以通过以下方式进行设置:通过“-异常”方式,可使得发生指定的异常时事务回滚;通过“+异常”方式,可使得发生指定的异常时事务提交。
  • 该方式的实现步骤为:

    2.3.1 复制项目

  • 复制transaction_buystock项目,并重命名为transaction_proxy。在此基础上修改。

    2.3.2 导入Jar包

  • 这里使用到的Spring的AOP,所以需要引入AOP的两个jar包:aop联盟,及Spring对AOP实现的Jar包。
    SSH框架之Spring4专题4:Spring与DAO
    SSH框架之Spring4专题4:Spring与DAO

    2.3.3 在容器中添加事务管理器DataSourceTransactionManager

  • 由于本项目使用的是JDBC模版进行持久化,所以使用DataSourceTransactionManager类作为事务管理器。
    <!-- 配置事务管理器 -->
    <bean id="myTransactionManager"
            class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="myC3P0DataSource"></property>
    </bean>

    2.3.4 在容器中添加事务代理TransactionProxyFactoryBean

    <!-- 配置service的事务切面代理 -->
    <bean id="myServiceProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager" ref="myTransactionManager"></property>
        <property name="target" ref="myService"></property>
        <property name="transactionAttributes">
            <props>
                <prop key="open">PROPAGATION_REQUIRED</prop>
                <prop key="find">PROPAGATION_SUPPORTS,readOnly</prop>
                <prop key="buyStock">PROPAGATION_REQUIRED,-StockException</prop>
            </props>
        </property>
    </bean>

    2.3.5 修改测试类

  • 现在就可以通过事务代理来运行。
    public class MyTest {
    private IStockProcessService service;   
    @Before
    public void setUp() {
        String recource = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(recource);
        service = (IStockProcessService) ac.getBean("myServiceProxy");
    }
    @Test
    public void test01() {
        service.openAccount("a", 2000);
        service.openStock("s", 100);
    }   
    @Test
    public void test02() throws StockException {
        service.buyStock("a", "s", 500, 100);
    }
    }

    2.4 使用Spring的事务注解管理事务

  • 通过@Transactional注解方式,也可将事务织入到相应方法中。而使用注解方式,只需要在配置文件中加入一个tx标签,以告诉spring使用注解来完成事务的织入。该标签只需要指定一个属性,事务管理器。
    <!-- 开启事务注解驱动 -->
    <tx:annotation-driven transaction-manager="myTransactionManager"/>

    @Transactional的所有可选属性如下所示:
    1、propagation:用于设置事务传播属性。该属性类型为Propagation枚举,默认值为Propagation.REQUIRED。
    2、isolation:用于设置事务的隔离级别。该属性类型为Isolation枚举,默认值为Isolation.DEFAULT。
    3、readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为boolean,默认值为false。
    4、timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为int,默认值为-1,即没有时限。
    5、rollbackFor:指定需要回滚的异常类。类型为Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
    6、rollbackForClassName:指定需要回滚的异常类类名。类型为String[],默认值为空数组。当然,若只有一个异常类,可以不使用数组。
    7、noRollbackFor:指定不需要回滚的异常类。类型为String[],默认值为空数组。当然,若只有一个异常类,可以不使用数组。
    8、noRollbackForClassName:指定不需要回滚的异常类类名。类型为String[],默认值为空数组。当然,若只有一个异常类,可以不使用数组。

  • 需要注意的是,@Transactional若用在方法上,只能够用于public方法上,对于其他非public方法,如果加上了注解@Transactional,虽然Spring不会报错,但是不会将指定事务织入到该方法中。因为Spring会忽略所有非public方法上的@Transaction注解。
  • 若@Transaction注解在类上,则表示该类上所有的方法均将在执行时织入事务。

    2.4.1 step1:复制项目

  • 复制transaction_buystock项目,并重命名为transaction_annotation,在此基础上进行修改。

    2.4.2 在容器中添加事务管理器

    <!-- 配置事务管理器 -->
    <bean id="myTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="myC3P0DataSource"></property>
    </bean>

    2.4.3 在Service实现类方法上添加注解

    @Transactional(propagation=Propagation.REQUIRED)
    @Override
    public void openStock(String sname, int count) {
        sDao.insertStock(sname, count);
    }
    @Transactional(propagation=Propagation.REQUIRED, rollbackFor=StockException.class)
    @Override
    public void buyStock(String aname, String sname, double money, int amount) throws StockException {
        aDao.updateAccount(aname, money);
        if(1 == 1) {
            throw new StockException();
        }
        sDao.updateStock(aname, amount);
    }
    @Transactional(propagation=Propagation.SUPPORTS, readOnly=true)
    @Override
    public Account findAccount(String aname) {
        return aDao.selectAccount(aname);
    }

    2.4.4 添加配置文件内容

    <!-- 开启事务注解驱动 -->
    <tx:annotation-driven transaction-manager="myTransactionManager"/>

    2.4.5 修改测试类

  • 由于配置文件中已经不存在事务代理对象,所以测试类中要从容器中获取的将不再是事务代理对象,而是原来的目标对象。
    @Before
    public void setUp() {
        String recource = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(recource);
        service = (IStockProcessService) ac.getBean("myService");
    }

    2.5 使用AspectJ的AOP配置管理事务

  • 使用XML配置事务代理的方式的不足是,每个目标类都需要配置事务代理。当目标类较多时,配置文件会变得非常臃肿。
  • 使用XML配置顾问方式可以自动为每个符合切入点表达式的类生成事务代理。其用法很简单,只需要将前面代码中关于事务代理的配置删除,再替换为如下内容即可。

    2.5.1 复制项目

  • 复制transaction_buystock项目,并重命名为transaction_advisor。在此基础上修改。

    2.5.2 导入Jar包

  • 这里使用Spring的AspectJ方式将事务进行的织入,所以,这里除了前面导入的aop的两个Jar包外,还需要两个Jar包:AspectJ的Jar包,以及Spring整合AspectJ的Jar包。
    SSH框架之Spring4专题4:Spring与DAO
    SSH框架之Spring4专题4:Spring与DAO

    2.5.3 在容器中添加事务管理器

    <!-- 配置事务管理器 -->
    <bean id="myTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="myC3P0DataSource"></property>
    </bean>

    2.5.4 配置事务通知

  • 为了事务通知设置相关属性。用于指定要将事务以什么样的方式织入给哪些方法。
  • 例如,应用到buyStock方法上的事务要求是必须的,且当buyStock方法发生StockException后,要回滚。
    <!-- 配置事务通知 -->
    <tx:advice id="myAdvice" transaction-manager="myTransactionManager">
        <tx:attributes>
            <tx:method name="open" propagation="REQUIRED"/>
            <tx:method name="find" propagation="SUPPORTS" read-only="true"/>
            <tx:method name="buyStock" propagation="REQUIRED" rollback-for="StockException"/>
        </tx:attributes>
    </tx:advice>

    2.5.5 配置顾问

  • 指定将配置好的事务通知,织入给谁。
    <!-- 配置顾问(顾问 = 通知 + 切入点) -->
    <aop:config>
        <aop:pointcut expression="execution(* com.eason.spring.service.*.*(..))" id="myPointcut"/>
        <aop:advisor advice-ref="myAdvice" pointcut-ref="myPointcut"/>
    </aop:config>
  • 需要注意的是,不能够写为下面的形式,切入点表达式一定要指明切入点在Service层,否则将会抛出对数据源的循环引用异常。因为下面的写法同时会把Service层和Dao层的方法均作为切入点。Service与Dao中均注入了数据源,而Service又调用了Dao,所以就出现了循环调用的异常。
    SSH框架之Spring4专题4:Spring与DAO

    2.5.6 修改测试类

  • 测试类中要从容器中获取的将不再是事务代理对象,而是目标对象。
    @Before
    public void setUp() {
        String recource = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(recource);
        service = (IStockProcessService) ac.getBean("myService");
    }