Spring学习笔记第四天,Spring中的JDBCTemplate、Spring基于AOP的事务控制、Spring中的事务控制

9 篇文章 0 订阅
5 篇文章 0 订阅

1.Spring中的JDBCTemplate

1.1 Spring中JDBCTemplate的概述

在这里插入图片描述
spring中JDBCTemplate是对JDBC简单封装和 commons-dbutils 类似,在使用起来也很相似。它是 spring 框架中提供的一个对象,是对原始 Jdbc API 对象的简单封装。 spring 框架为我们提供了很多的操作模板类。

功能模板类
操作关系型数据的JdbcTemplate、HibernateTemplate
操作 nosql 数据库的RedisTemplate
操作消息队列的JmsTemplate

我们今天的主角在 spring-jdbc-5.0.2.RELEASE.jar 中,我们在导包的时候,除了要导入这个 jar 包外,还需要导入一个 spring-tx-5.0.2.RELEASE.jar(它是和事务相关的)。
我们先创建一个新的Module命名为day04_01jdbcTemplate,首先修改pom.xml导入项目需要的jar包。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.itheima</groupId>
    <artifactId>day04_01jdbcTemplate</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency> <!--连接数据库-->
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency> <!--事务控制-->
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
    </dependencies>
</project>

然后我们准备一张数据库表,这个表我们之前的学习中已经创建过了,

DROP TABLE IF EXISTS `account`;
CREATE TABLE `account`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `money` float NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `account` VALUES (1, 'aaa', 800);
INSERT INTO `account` VALUES (2, 'bbb', 2100);

然后在创建com.itheima.domain包并创建这个表的实体类Account.java

public class Account implements Serializable {
    private Integer id;
    private String name;
    private Float money;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Float getMoney() {
        return money;
    }
    public void setMoney(Float money) {
        this.money = money;
    }
    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", money=" + money +
                '}';
    }
}

下面就来演示一下Spring中JDBCTemplate最简单的使用方式,在创建一个com.itheima.jdbctemplate包,并且在其中创建一个JdbcTemplateDemo1.java。

/**
 * JdbcTemplate 的最基本用法
 */
public class JdbcTemplateDemo1 {
    public static void main(String[] args) {
        // 准备数据源:Spring的内置数据源
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/spring");
        ds.setUsername("root");
        ds.setPassword("123456");

        //1.创建JdbcTemplate对象
        JdbcTemplate jt = new JdbcTemplate();
        //给jt设置数据源
        jt.setDataSource(ds);
        //2.执行操作
        jt.execute("insert into account(name, money) values ('ddd', 1000)");
    }
}

这个Module的完整结构如下。
在这里插入图片描述

我们先来插查看一下数据库中的数据如下。
在这里插入图片描述
然后我们来运行下这个main方法。运行完成之后我们在来看一下数据库表的变化。数据库表如下,说名运行成功了
在这里插入图片描述
我们在来看一下这个main方法里的代码,发现数据库连接的参数都是写死的,而且对象是new出来的,如果你学过前面的知识你会马上想到使用ioc来配置

1.2 JDBCTemplate在Spring IOC中的使用:

通过对上面的代码分析,我们可以对上面的代码进行一定的优化,下面我们就使用IOC来重新配置一下。首先在resources里新建一个bean.xml文件,来配置jdbcTemplate对象

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--配置数据源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/spring"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
</beans>

然后我们把JdbcTemplateDemo1.java拷贝一份,复制到和JdbcTemplateDemo1.java同一个包下命名为JdbcTemplateDemo2.java。

public class JdbcTemplateDemo2 {
    public static void main(String[] args) {
        //1.获取IOC容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取bean对象
        JdbcTemplate jt = (JdbcTemplate) ac.getBean("jdbcTemplate");
        //3.执行操作
        jt.execute("insert into account(name, money) values ('ccc', 2222)");
    }
}

然后我们来执行一个这个main方法,通过查看数据库表的变化,发现这样也是可以正常运行的。

1.3 JDBCTemplate的CRUD操作

下面我们就来学习一下JDBCTemplate的CRUD操作。我们把JdbcTemplateDemo2.java复制一份放到和JdbcTemplateDemo2.java相同的目录下命名为JdbcTemplateDemo3.java。CRUD的具体操作请看下面的代码。

/**
 * JdbcTemplate 的CRUD操作
 */
public class JdbcTemplateDemo3 {
    public static void main(String[] args) {
        //1.获取IOC容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取bean对象
        JdbcTemplate jt = (JdbcTemplate) ac.getBean("jdbcTemplate");
        //3.执行操作
        //保存
        // jt.update("insert into account(name, money) values (?,?)", "eee", 3333f);
        //更新
        //jt.update("update account set name = ?, money = ? where id = ?", "test", 4567f, 7);
        //删除
        //jt.update("delete from account where id = ?", 5);
        //查询所有,使用 我们自定义Account的封住策略
//        List<Account> accounts = jt.query("select * from account where money > ?;", new AccountRowMapper(), 1000f);
        //使用Spring为我们提供的封住策略
//        List<Account> accounts = jt.query("select * from account where money > ?;", new BeanPropertyRowMapper<Account>(Account.class), 1000f);
//        System.out.println(Arrays.toString(accounts));
        //查询一个
//        List<Account> accounts = jt.query("select * from account where id = ?;", new BeanPropertyRowMapper<Account>(Account.class), 1);
//        System.out.println(accounts.isEmpty() ? "没有内容" : accounts.get(0));
        //查询返回一行一列(使用聚合函数,但不加group by句子)
        Long count = jt.queryForObject("select count(*) from account where money > ?", Long.class, 1000f); // 避免返回值大于Integer的最大值,所以使用Long
        System.out.println(count);
    }
}

/**
 * 定义Account的封住策略
 */
class AccountRowMapper implements RowMapper<Account> {

    /**
     * 把结果集中的数据封装到Account中,然后Spring把每个Account加到集合中
     *
     * @param resultSet
     * @param i
     * @return
     * @throws SQLException
     */
    public Account mapRow(ResultSet resultSet, int i) throws SQLException {
        Account account = new Account();
//        account.setId(resultSet.getInt("id"));
//        account.setName(resultSet.getString("name"));
//        account.setMoney(resultSet.getFloat("money"));

        //使用反射设置属性值
        ResultSetMetaData rsmd = resultSet.getMetaData();
        // 通过ResultSetMetaData获取结果集中的列数
        int columnCount = rsmd.getColumnCount();
        for (int j = 0; j < columnCount; j++) {
            try {
                // 获取每个列的列名
                String columName = rsmd.getColumnName(j + 1);
                Field field = Account.class.getDeclaredField(columName);
                field.setAccessible(true);

                // 获取每个列的列值
                Object columValue = resultSet.getObject(j + 1);
                // field.set(account, columValue);
                field.set(account, resultSet.getObject(columName));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return account;
    }
}

1.4 JDBCTemplate在DAO中的使用

既然我们要说 JDBCTemplate在Dao中的使用,那么首先我们要在com.itheima.dao包里创建一个IAccountDao.java的接口

/**
 * 账户的持久层接口
 */
public interface IAccountDao {
    /**
     * 根据id查询账户
     *
     * @param accountId
     * @return
     */
    Account findAccountById(Integer accountId);

    /**
     * 根据名称查询账户
     *
     * @param accountName
     * @return
     */
    Account findAccountByName(String accountName);

    /**
     * 更新账户信息
     *
     * @param account
     */
    void updateAccount(Account account);
}

然后在com.itheima.dao.impl包里创建一个IAccountDao的实现类AccountDaoImpl.java

/**
 * 账户的持久层实现类
 */
public class AccountDaoImpl implements IAccountDao {
    private JdbcTemplate jdbcTemplate;

    // 用于使用Spring进行数据注入
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public Account findAccountById(Integer accountId) {
        List<Account> accounts = jdbcTemplate.query("select * from account where id = ?", new BeanPropertyRowMapper<Account>(Account.class), accountId);
        return accounts.isEmpty() ? null : accounts.get(0);
    }

    public Account findAccountByName(String accountName) {
        List<Account> accounts = jdbcTemplate.query("select * from account where name = ?", new BeanPropertyRowMapper<Account>(Account.class), accountName);
        if (accounts.isEmpty()) {
            return null;
        }
        if (accounts.size() > 1) {
            throw new RuntimeException("结果集不唯一");
        }
        return accounts.get(0);
    }

    public void updateAccount(Account account) {
        jdbcTemplate.update("update account set name = ?, money = ? where id = ?", account.getName(), account.getMoney(), account.getId());
    }
}

还有就是我们需要配置一下bean.xml。配置账户的持久层

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--配置账户的持久层-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"></property>
    </bean>
   ...
</beans>

下面我们就可以来创建测试类写测试方法来测试我们的代码了,和上面一样=简单起见我们复制一下JdbcTemplateDemo2.java,复制到相同的包下命名为JdbcTemplateDemo4.java。

/**
 * JdbcTemplate 的最基本用法
 */
public class JdbcTemplateDemo4 {
    public static void main(String[] args) {
        //1.获取IOC容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取bean对象
        IAccountDao accountDao = (IAccountDao) ac.getBean("accountDao");
        //3.执行操作
        Account account = accountDao.findAccountById(1);
        System.out.println(account);

        account.setMoney(10000f);
        //执行更新操作
        accountDao.updateAccount(account);
    }
}

我们运行一下上面的代码可以正常运行,但是如果我们还有其他的IAccountDao实现类呢?每个类都要写 private JdbcTemplate jdbcTemplate; 这样就会有重复代码。怎么办呢?往下看。

1.4 JdbcDaoSupport的使用以及Dao的两种编写方式

对于上面提到的多个Dao的实现类都要写 private JdbcTemplate jdbcTemplate; 的问题,我们可以把重复的代码提取出来放到父类中,然后各个Dao的实现类都基础自实现类。下面我们就来写一个Dao实现类的父类JdbcDaoSupport.java

/**
 * 此类用于抽取 dao 中的重复代码
 */
public class JdbcDaoSupport {
    private JdbcTemplate jdbcTemplate;

    // 用于使用Spring进行数据注入
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public JdbcTemplate getJdbcTemplate() {
        return jdbcTemplate;
    }

    public void setDataSource(DataSource dataSource) {
        if (jdbcTemplate == null) {
            jdbcTemplate = createJdbcTemplate(dataSource);
        }
    }

    private JdbcTemplate createJdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

然后我们在复制一个AccountDaoImpl.java到相同的包下命名为AccountDaoImpl2.java
让其集成自JdbcDaoSupport

/**
 * 账户的持久层实现类
 * 这个版本适合使用xml配置,因为继承了 JdbcDaoSupport 这个是Spring提供的不方便使用注解
 */
public class AccountDaoImpl2 extends JdbcDaoSupport implements IAccountDao {
    public Account findAccountById(Integer accountId) {
    	// 使用父类中的JdbcTemplate
        List<Account> accounts = super.getJdbcTemplate().query("select * from account where id = ?", new BeanPropertyRowMapper<Account>(Account.class), accountId);
        return accounts.isEmpty() ? null : accounts.get(0);
    }

    public Account findAccountByName(String accountName) {
        List<Account> accounts = super.getJdbcTemplate().query("select * from account where name = ?", new BeanPropertyRowMapper<Account>(Account.class), accountName);
        if (accounts.isEmpty()) {
            return null;
        }
        if (accounts.size() > 1) {
            throw new RuntimeException("结果集不唯一");
        }
        return accounts.get(0);
    }

    public void updateAccount(Account account) {
        super.getJdbcTemplate().update("update account set name = ?, money = ? where id = ?", account.getName(), account.getMoney(), account.getId());
    }
}

然后修改一下bean.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--配置账户的持久层 改成 AccountDaoImpl2-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl2">
        <!--<property name="jdbcTemplate" ref="jdbcTemplate"></property>-->
        <!--只需要注入DataSource就行了,因位会触发创建JdbcTemplate的方法-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置数据源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/spring"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
</beans>

然后创建一个测试类JdbcTemplateDemo4.java

/**
 * JdbcTemplate 的最基本用法
 */
public class JdbcTemplateDemo4 {
    public static void main(String[] args) {
        //1.获取IOC容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根据id获取bean对象
        IAccountDao accountDao = (IAccountDao) ac.getBean("accountDao");
        //3.执行操作
        Account account = accountDao.findAccountById(1);
        System.out.println(account);

        account.setMoney(30000f);
        //执行更新操作
        accountDao.updateAccount(account);
    }
}

运行一下测试类JdbcTemplateDemo4中的main方法,可以发现仍然可以正常运行。这时我们把我们写的这个JdbcDaoSupport.java删除掉,会发现AccountDaoImpl2.java中会自动导入 org.springframework.jdbc.core.support.JdbcDaoSupport 因为我们写的这个父类Spring都已经给我准备好了。我们在运行一下测试方法,可以发现仍然可以正常运行。
这两种Dao的实现方法都可以正常使用,第一种 不继承JdbcDaoSupport的Dao实现类适合使用注解配置IOC第二种基础自JdbcDaoSupport的Dao实现类适合使用XML配置IOC

2.Spring基于AOP的事务控制

2.1 Spring基于XML的AOP事务控制

我们新一个Module命名为day04_02account_aop_xml,我们来根据昨天的一个Module的代码来改造,我们把day03_01account这个Module里src下的所有文件都拷贝到我们新建的Module里src下。并且根据day03_01account这个Module来编写我们新建的Module里的pom.xml文件。因为我们使用Spring的AOP来实现动态代理,所以我们现在就不再需要factory这个包里的内容,所以把这个包整体都删除掉,然后我们只是来演示Spring使用AOP来进行事务控制,所以在service里AccountServiceImpl_OLD这个类也就不需要了。最后整个Module的结构如下。
在这里插入图片描述
然后我们来修改一下bean.xml文件

<?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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置Service -->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <!-- 使用set方法注入 dao对象-->
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!--配置Dao对象-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
        <!-- 使用set方法注入 QueryRunner -->
        <property name="runner" ref="runner"></property>
        <!-- 注入ConnectionUtils -->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置 QueryRunner 对象,因为 bean 对象默认是单例对象,所以有可能发生线程安全问题。所以让scope设置为prototype为多例-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--注入数据源, 因为我们使用的是绑定在线程上的连接,所以这个不用传数据源-->
        <!--<constructor-arg name="ds" ref="dataSource"></constructor-arg>-->
    </bean>

    <!--配置数据源,使用数据库连接池-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--注入链接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>

    <!-- 配置Connection的工具类 ConnectionUtils -->
    <bean id="connectionUtils" class="com.itheima.utils.ConnectionUtils">
        <!-- 注入数据源-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 配置事务管理器-->
    <bean id="txManager" class="com.itheima.utils.TransactionManager">
        <!-- 注入ConnectionUtils -->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置aop-->
    <aop:config>
        <!--配置通用切入点表达式-->
        <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
        <aop:aspect id="txAdvice" ref="txManager">
            <!--配置前置通知:开启事务-->
            <aop:before method="beginTransaction" pointcut-ref="pt1"></aop:before>
            <!--配置后置通知:提交事务-->
            <aop:after-returning method="commit" pointcut-ref="pt1"></aop:after-returning>
            <!--配置异常通知:回滚事务-->
            <aop:after-throwing method="rollback" pointcut-ref="pt1"></aop:after-throwing>
            <!--配置最终通知:释放连接-->
            <aop:after method="release" pointcut-ref="pt1"></aop:after>
        </aop:aspect>
    </aop:config>
</beans>

然后我们恢复一下数据库表里的数据
在这里插入图片描述
然后我们把AccountServiceImpl.java这个类的转账方法transfer中的 1/0取消注释

public class AccountServiceImpl implements IAccountService {
	...
    public void transfer(String sourceName, String targetName, Float money) {
        System.out.println("transfer....");
        //2.1.根据名称查询转出账户
        Account source = accountDao.findAccountByName(sourceName);
        //2.2.根据名称查询转入账户
        Account target = accountDao.findAccountByName(targetName);
        //2.3.转出账户的存款减去转账金额
        source.setMoney(source.getMoney() - money);
        //2.4.转入账户的存款加转账金额
        target.setMoney(target.getMoney() + money);
        //2.5.更新转出账户
        accountDao.updateAccount(source);

        int i = 1 / 0;

        //2.6.更新转入账户
        accountDao.updateAccount(target);
    }
}

最后修改一下我们的测试类AccountServiceTest中的测试方法

/**
 * 使用Junit单元测试,测试我们的配置
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {

    @Autowired
    IAccountService as;

    @Test
    public void testTransfer() {
        as.transfer("bbb", "aaa", 100f);
    }
}

然后执行这个测试方法testTransfer,控制台输入如下。
在这里插入图片描述
我们在看一下数据库里表的情况如下,可以看出数据没有发生变化,说明事务控制成功了。
在这里插入图片描述

2.2 Spring基于注解的AOP事务控制

我们在新建一个Module命名为day04_03account_aoptx_anno,然后把Module名为day04_02account_aoptx_xml里src下的文件拷贝到我们新建的Module里src下,并修改pom.xml文件。首先要配置一下IOC的注解,所以要在xml配置要扫描的包,所以要导入对应的依赖。

<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:sop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--配置Spring创建IOC容器时要扫描的包-->
    <context:component-scan base-package="com.itheima"></context:component-scan>
	...
</beans>

然后配置service,给service加上注解修改一下AccountServiceImpl.java在bean.xml中也就不在需要service的配置了。

@Service("accountService")
public class AccountServiceImpl implements IAccountService {
    @Autowired // 因为只有一个dao所以可以直接使用Autowired进行自动类型注入,所以也不在需要set方法了
    private IAccountDao accountDao;
    ...
}

下面配置一下dao,修改一下AccountDaoImpl.java在bean.xml中也就不在需要dao的配置了。

@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
    @Autowired
    private QueryRunner runner;
    @Autowired
    private ConnectionUtils connectionUtils;
    ...
}

下面修改一下ConnectionUtils.java,给该类加上注解,在bean.xml对ConnectionUtils的ioc配置就不需要了

@Component("connectionUtils")
public class ConnectionUtils {
    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
    @Autowired//因为这个Module中只有一个DataSource对象,所以可以使用Autowired进行自动类型注入
    private DataSource dataSource;
    ...
}

下面就来配置一下事务管理的类TransactionManager.java,配置好后就可以把bea.xml中aop的配置删除掉了

@Component("txManager")
@Aspect // 表明当前类是一个切面
public class TransactionManager {
    @Autowired
    private ConnectionUtils connectionUtils;

    /**
     * 这个方法用于表示切入点表达式
     */
    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    private void pt1() {
    }

    /**
     * 开始事务
     */
    @Before("pt1()") // 引用切入点表达式时必须要写pt1后面的()
    public void beginTransaction() {
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 提交事务
     */
    @AfterReturning("pt1()")
    public void commit() {
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 回滚事务
     */
    @AfterThrowing("pt1()")
    public void rollback() {
        try {
            connectionUtils.getThreadConnection().rollback();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 释放连接
     */
    @After("pt1()")
    public void release() {
        try {
            connectionUtils.getThreadConnection().close(); // 把连接返回连接池中
            connectionUtils.removeConnection();//把连接和线程进行解绑
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

最后在bean.xml中国配置开启注解的aop。最终的bean.xml文件如下,

<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:sop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--配置Spring创建IOC容器时要扫描的包-->
    <context:component-scan base-package="com.itheima"></context:component-scan>

    <!--配置 QueryRunner 对象,因为 bean 对象默认是单例对象,所以有可能发生线程安全问题。所以让scope设置为prototype为多例-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--注入数据源, 因为我们使用的是绑定在线程上的连接,所以这个不用传数据源-->
        <!--<constructor-arg name="ds" ref="dataSource"></constructor-arg>-->
    </bean>

    <!--配置数据源,使用数据库连接池-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--注入链接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>

    <!--开启Spring对注解AOP的支持-->
    <sop:aspectj-autoproxy></sop:aspectj-autoproxy>
</beans>

现在我们把数据库中的表数据恢复成如下:
在这里插入图片描述
首先我们把AccountServiceImpl.java中的transfer方法里的1/0注释掉下面我们来运行下测试类AccountServiceTest中的测试方法testTransfer,控制台输出如下,通过查看数据库表的数据发现数据没有变化,但是为啥报错了呢,通过查询资料发现这个是Spring的一个 bug,据说在高版本中已经解决了,这个bug就是在使用注解配置Spring的aop时调用顺序是有问题的,它会先调用最终通知后调用后置通知。
在这里插入图片描述
这个问题怎么解决呢?我们可以使用环绕通知来解决,我们自己写调用的代码,自己来控制调用的顺序。下面就来改造一下TransactionManager.java,因为要我们自己写代码来实现环绕通知,所以要前置通知、后置通知、异常通知、最终通知的注解注释掉或者删除掉。创建一个新的方法 aroundAdvice 并加上环绕通知的注解 Around 代码如下。

@Component("txManager")
@Aspect // 表明当前类是一个切面
public class TransactionManager {
    @Autowired
    private ConnectionUtils connectionUtils;

    /**
     * 这个方法用于表示切入点表达式
     */
    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    private void pt1() {
    }

    /**
     * 开始事务
     */
    //@Before("pt1()") // 引用切入点表达式时必须要写pt1后面的()
    public void beginTransaction() {
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 提交事务
     */
    //@AfterReturning("pt1()")
    public void commit() {
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 回滚事务
     */
    //@AfterThrowing("pt1()")
    public void rollback() {
        try {
            connectionUtils.getThreadConnection().rollback();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 释放连接
     */
    //@After("pt1()")
    public void release() {
        try {
            connectionUtils.getThreadConnection().close(); // 把连接返回连接池中
            connectionUtils.removeConnection();//把连接和线程进行解绑
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Around("pt1()") // 环绕通知
    public Object aroundAdvice(ProceedingJoinPoint pjp) {
        Object rtValue = null;
        try {
            //1.获取参数
            Object[] args = pjp.getArgs();
            //2.开启事务
            this.beginTransaction();
            //3.执行方法
            rtValue = pjp.proceed(args);
            //4.提交事务
            this.commit();

            return rtValue;

        } catch (Throwable e) { // Exception 不能捕获到这个异常
            //5.回滚
            this.rollback();
            throw new RuntimeException(e);
        } finally {
            //6.释放事务
            this.release();
        }
    }
}

然后我们在来运行一下测试类AccountServiceTest中的测试方法testTransfer,通过观察控制台发现程序正常执行,并且数据库表的数据也都正常更改。然后我们把AccountServiceImpl.java里的方法transfer里的1/0打开注释,在一次运行测试方法,发现程序报错如下,但是通过查看数据库表里的数据发现现在可以正常的进行事务控制了。
在这里插入图片描述

3.Spring中的事务控制

3.1 Spring 中事务控制的 API 介绍

3.1.1 PlatformTransactionManager 接口

此接口是 spring 的事务管理器,它里面提供了我们常用的操作事务的方法,如下图:
在这里插入图片描述
我们在开发中都是使用它的实现类,如下图:
在这里插入图片描述

3.1.2 TransactionDefinition 接口

它是事务的定义信息对象,里面有如下方法:
在这里插入图片描述

3.1.2.1 事务的隔离级别

在这里插入图片描述

3.1.2.2 事务的传播行为

在这里插入图片描述

3.1.2.3 超时时间

默认值是-1,没有超时限制。如果有,以秒为单位进行设置。

3.1.2.4 是否是只读事务

建议查询时设置为只读。

3.1.3 TransactionStatus 接口

此接口提供的是事务具体的运行状态,方法介绍如下图
在这里插入图片描述

3.2 Spring中事务控制的代码准备

我们先来新建一个Module命名为dayday04_04tx。我们现在创建的这个Module是为后面的代码演示做准备

  • 首先我们需要修改pom.xml导入我们需要的坐标
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.itheima</groupId>
    <artifactId>day04_04tx</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <dependency><!-- 支持切入点表达式 -->
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.7</version>
        </dependency>
        <dependency><!-- spring 的测试支持 -->
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
  • 编写实体类Account.java
public class Account implements Serializable {
    private Integer id;
    private String name;
    private Float money;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Float getMoney() {
        return money;
    }
    public void setMoney(Float money) {
        this.money = money;
    }
    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", money=" + money +
                '}';
    }
}
  • 添加IAccountdao接口和其实现类AccountDaoImpl.java
/**
 * 账户的持久层接口
 */
public interface IAccountDao {
    /**
     * 根据id查询账户
     */
    Account findAccountById(Integer accountId);
    /**
     * 根据名称查询账户
     */
    Account findAccountByName(String accountName);
    /**
     * 更新账户信息
     */
    void updateAccount(Account account);
}
/**
 * 账户的持久层实现类
 * 这个版本适合使用xml配置,因为继承了 JdbcDaoSupport Spring IOC不方便提供注解注入数据,但是适合使用XML进行配置
 */
public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {
    public Account findAccountById(Integer accountId) {
        List<Account> accounts = super.getJdbcTemplate().query("select * from account where id = ?", new BeanPropertyRowMapper<Account>(Account.class), accountId);
        return accounts.isEmpty() ? null : accounts.get(0);
    }
    public Account findAccountByName(String accountName) {
        List<Account> accounts = super.getJdbcTemplate().query("select * from account where name = ?", new BeanPropertyRowMapper<Account>(Account.class), accountName);
        if (accounts.isEmpty()) {
            return null;
        }
        if (accounts.size() > 1) {
            throw new RuntimeException("结果集不唯一");
        }
        return accounts.get(0);
    }
    public void updateAccount(Account account) {
        super.getJdbcTemplate().update("update account set name = ?, money = ? where id = ?", account.getName(), account.getMoney(), account.getId());
    }
}
  • 编写IAccountService接口和其实现类AccountServiceImpl.java
/**
 * 账户的业务层接口
 */
public interface IAccountService {
    /**
     * 根据id查询账户信息
     */
    Account findAccountBuId(Integer id);
    /**
     * 转账
     * @param sourceName 转出账户名称
     * @param targetName 转入账户名称
     * @param money      转账金额
     */
    void transfer(String sourceName, String targetName, float money);
}
public class AccountServiceImpl implements IAccountService {
    private IAccountDao accountDao;
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }
    public Account findAccountBuId(Integer id) {
        return accountDao.findAccountById(id);
    }
    public void transfer(String sourceName, String targetName, float money) {
        System.out.println("transfer....");
        //根据账户名查询转出账户
        Account sourceAccount = accountDao.findAccountByName(sourceName);
        //根据账户名查询转入账户
        Account targetAccount = accountDao.findAccountByName(targetName);
        //转出账户减少money
        sourceAccount.setMoney(sourceAccount.getMoney() - money);
        //转入账户增加money
        targetAccount.setMoney(targetAccount.getMoney() + money);

        //更新账户
        accountDao.updateAccount(sourceAccount);

        int i = 1 / 0;

        accountDao.updateAccount(targetAccount);
    }
}
  • 然后我们还需要编写spring配置文件bean.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置业务层-->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"></property>
    </bean>
    <!--配置账户的持久层-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
        <!--只需要注入DataSource就行了,因位会触发创建JdbcTemplate的方法-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--配置数据源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/spring"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
</beans>
  • 最后们在编写一个测试类AccountServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {
    @Autowired
    private IAccountService as;
    @Test
    public void testTransfer() {
        as.transfer("aaa", "bbb", 100f);
    }
}

最后我们整个的准备项目结构如下
在这里插入图片描述

3.3 Spring 基于 XML 的声明式事务控制(配置方式)

首先我们新建一个Module命名为day04_05_tx_xml,然后我们把我们上面创建一个名为day04_04tx的准备项目的src里的内容拷贝到我们新建的Module的src下。不要忘记填写pom.xml文件哟,可以根据day04_04tx的pom.xml文件进行填写。
在这个Module里我们不需要修改太多的内容,我们主要修改spring的配置文件bean.xml。
因为我们要进行事务的控制所以在修改bean.xml文件时首先要导入事务控制的约束和aop的约束。我们可以在spring的官方文档中找到,我们这里使用的spring的版本为5.0.2.RELEASE
在这里插入图片描述
然后点击上图中红框中的连接Data Access。然后在这个页面搜索 xmlns:tx ,找到如下内容,替换掉bean.xml文件里原来的约束。我们会发现这个约束里已经包含了aop的约束。
在这里插入图片描述

  • spring中基于XML的声明式事务控制配置步骤
    在这里插入图片描述
  • 最后完整的bean.xml如下
<?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: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/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--配置业务层-->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"></property>
    </bean>
    <!--配置账户的持久层-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
        <!--只需要注入DataSource就行了,因位会触发创建JdbcTemplate的方法-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--配置数据源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/spring"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
    <!--spring中基于XML的声明式事务控制配置步骤
        1、配置事务管理器
        2、配置事务的通知
            此时我们需要导入事务的约束 tx名称空间,同时也需要aop的约束
            使用tx:advice 标签配置事务通知
                属性:
                    id:个事务通知起一个唯一的标识
                    transaction——manager:给事务通知提供一个事务管理器引用
         3、配置AOP中的通用切入点表达式
         4、建立事务通知和切入点表达式的对应关系
         5、事务的属性
            实在事务的通知tx:advice标签的内部配置
    -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--配置事务的通知-->
    <tx:advice id="txAvice" transaction-manager="transactionManager">
        <!--配置事务的属性
            isolation:用于指定事务的隔离级别,默认值是DEFAULT,表示使用数据库的默认隔离级别。
            propagation:用于指定事务的传播行为,默认值是 REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择 SUPPORTS
            read-only:用于指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写。
            timeout:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。

            no-rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值,表示任何异常都回滚
            rollback-for:用于指定一个异常,当产生该异常时,事务不回滚。产生其他异常时事务回滚。没有默认值,表示任何异常都回滚
        -->
        <tx:attributes>
            <!--transfer 表示一个方法-->
            <tx:method name="transfer" propagation="REQUIRED" read-only="false"/>
            <!--表示所有的方法-->
            <tx:method name="*" propagation="REQUIRED" read-only="false"/>
            <!--find* 表示方法名以find开头的方法,例如查询方法-->
            <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
        </tx:attributes>
    </tx:advice>
    <!--配置AOP-->
    <aop:config>
        <!--配置切入点表达式-->
        <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/>
        <!--建立切入点表达式和事务通知的对应关系-->
        <aop:advisor advice-ref="txAvice" pointcut-ref="pt1"></aop:advisor>
    </aop:config>
</beans>

下面我运行一下测试类中的测试方法,就会发现事务控制就可以啦。

3.4 Spring 基于 注解 的声明式事务控制(配置方式)

首先创建一个新的Module命名为day04_06tx_anno,然后把上一个名为day04_05_tx_xml的Module的src下的文件拷贝到我们新建的Module的src下,记得填写pom.xml文件可以根据上一个Module进行填写。
首先我们把spring IOC配置改成注解的形式。
-我们先把dao接口的实现类AccountDaoImpl.java改成注解的形式

/**
 * 账户的持久层实现类
 * 因为使用注解进行开发,所以不能再继承 JdbcDaoSupport 这个类啦,
 */
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    public Account findAccountById(Integer accountId) {
        List<Account> accounts = jdbcTemplate.query("select * from account where id = ?", new BeanPropertyRowMapper<Account>(Account.class), accountId);
        return accounts.isEmpty() ? null : accounts.get(0);
    }
    public Account findAccountByName(String accountName) {
        List<Account> accounts = jdbcTemplate.query("select * from account where name = ?", new BeanPropertyRowMapper<Account>(Account.class), accountName);
        if (accounts.isEmpty()) {
            return null;
        }
        if (accounts.size() > 1) {
            throw new RuntimeException("结果集不唯一");
        }
        return accounts.get(0);
    }
    public void updateAccount(Account account) {
        jdbcTemplate.update("update account set name = ?, money = ? where id = ?", account.getName(), account.getMoney(), account.getId());
    }
}
  • 然后修改一下bean.xml,因为要配置spring IOC要扫描的包所以要添加 xmlns:context 的约束。
<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:scantext="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/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <!--配置spring IOC要扫描的包-->
    <scantext:component-scan base-package="com.itheima"></scantext:component-scan>
    <!--配置jdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--只需要注入DataSource就行了,因位会触发创建JdbcTemplate的方法-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--配置数据源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/spring"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
    <!--spring中基于注解的声明式事务控制配置步骤
        1、配置事务管理器
        2、开始spring对注解事务的支持
        3、在需要事务支持的地方使用 @Transcational注解
    -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--开启spring对注解事务的支持-->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>
  • 最后修改service的实现类AccountServiceImpl.java配置ico同时添加事务控制的注解
@Service("accountService")
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true) // 参数和使用xml配置时一样
public class AccountServiceImpl implements IAccountService {
    @Autowired
    private IAccountDao accountDao;

    public Account findAccountBuId(Integer id) {
        return accountDao.findAccountById(id);
    }

    //因为这里的事务读写都需要,所以需要单独配置
    @Transactional(propagation = Propagation.REQUIRED, readOnly = false)
    public void transfer(String sourceName, String targetName, float money) {
        System.out.println("transfer....");
        //2.1根据账户名查询转出账户
        Account sourceAccount = accountDao.findAccountByName(sourceName);
        //2.2根据账户名查询转入账户
        Account targetAccount = accountDao.findAccountByName(targetName);
        //2.3转出账户减少money
        sourceAccount.setMoney(sourceAccount.getMoney() - money);
        //2.3转入账户增加money
        targetAccount.setMoney(targetAccount.getMoney() + money);

        //2.4更新账户
        accountDao.updateAccount(sourceAccount);

        int i = 1 / 0;

        accountDao.updateAccount(targetAccount);
    }
}

至此Spring 基于 注解 的声明式事务控制配置就完成了。下面我们可以用个运行测试方法来查看是否成功配置了事务控制。
我们在开发的时候建议使用xml进行配置spring的事务控制,因为如果一个service中有多个方法他们要求的数据库隔离级别不同,就需要写很多个Transactional注解,而使用xml进行配置相对来说比较简单。

3.4 Spring 基于 纯注解 的声明式事务控制

我们先来创建一个新Module命名day04_07anno_tx_withoutxml。首先我们可以根据上一个名为day04_06tx_anno的Module的pom.xml来修改我们新建Module的pom.xml文件。然后把day04_06tx_anno里src下的文件拷贝到我们新建的Module的src下。下面我们一步一步的来去除bean.xml里的内容。新建一个包com.config,用于存放我们的配置类。

  • 首先为了除去bean.xml文件中的jdbcTemplate对象和dataSource对象,我们在config包下,新建一个JdbcConfig.java
/**
 * 连接数据库相关的配置类
 */
public class JdbcConfig {
    @Value("${jdbc.driver}") // 使用spring的er表达式
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;
    /**
     * 创建 JdbcTemplate 对象
     * Qualifier注解 在给方法参数修饰时可以单独使用,参数 value 表示bean的id
     */
    @Bean(name = "jdbcTemplate")
    public JdbcTemplate createJdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
    /**
     * 创建一个数据源对象
     */
    @Bean(name = "dataSource")
    public DataSource createDataSource() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);
        return ds;
    }
}
  • 为了除去bea.xml中的事务控制有关的配置,我们在config包里新建一个TransactionConfig.java
/**
 * 和事务相关的配置类
 */
public class TransactionConfig {
    /**
     * 用于创建事务管理器对象
     * PlatformTransactionManager:事务管理器接口,DataSourceTransactionManager为其实现类
     */
    @Bean(name = "transactionManager")
    public PlatformTransactionManager createTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
        DataSourceTransactionManager dstm = new DataSourceTransactionManager(dataSource);
        return dstm;
    }
}
  • 最后为了我们的Module可以删除整个bean.xml,我们在config包下新建一个SpringConfiguration.java相当于xml配置时的bean.xml
/**
 * spring 的配置类相当于 bean.xml
 */
@Configuration // 如果这个类以字节码的形式传给 AnnotationContext 对象,那么这个注解就可以不用写
@ComponentScan("com.itheima") //用于配置要扫描的包
@Import({JdbcConfig.class, TransactionConfig.class}) // 导入其他的配置类
@PropertySource("jdbcConfig.properties") // 加载jdbc的配置文件
@EnableTransactionManagement // 实现 开启spring对注解事务的支持
public class SpringConfiguration {
}
  • 最后我们还需要修改一下我们的测试类AccountServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class)
//@ContextConfiguration(locations = "classpath:bean.xml") // 用于使用xml配置
@ContextConfiguration(classes = SpringConfiguration.class) //用于使用注解配置
public class AccountServiceTest {
    @Autowired
    private IAccountService as;

    @Test
    public void testTransfer() {
        as.transfer("aaa", "bbb", 100f);
    }
}

至此整个纯注解配置spring事务管理的Module编写完成。
在这个Module中我们基本回顾了前三题天所学的所有spring注解。

3.5 Spring 基于 编程式 的事务控制

我们在来新建一个Module命名为day04_08account_tx,然后根据 day04_04tx 这个Module填写pom.xml文件。然后把day04_04tx的src下的文件拷贝到我们新建的Module的src下。
基于编程式的事务控制需要使用一个 org.springframework.transaction.support.TransactionTemplate 类,这个类中有一个 execute 方法。
在这里插入图片描述
为什么这个方法里没有事务开启的操作呢,因为这个类还需要一个注入一个 PlatformTransactionManager 类型的参数,在spring的ioc配置中我配置为DataSourceTransactionManager这个对象。在执行这个execute方法之前会先执行 PlatformTransactionManager 的 doBegin 方法来开启事务。
因为仍然是事务控制,所以需要在bean.xml文件中配置一个事务管理器。具体的bean.xml配置如下。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置业务层-->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"></property>
        <property name="transactionTemplate" ref="transactionTemplate"></property>
    </bean>
    <!--配置账户的持久层-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
        <!--只需要注入DataSource就行了,因位会触发创建JdbcTemplate的方法-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--配置数据源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/spring"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--事务模板对象-->
    <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
        <!--事务模板对象需要注入一个 配置事务管理器 对象-->
        <property name="transactionManager" ref="transactionManager"></property>
    </bean>
</beans>

最后我们在类配置一下AccountServiceImpl.java,把需要事务控制的方法进行改造。

public class AccountServiceImpl implements IAccountService {
    private IAccountDao accountDao;
    
    // 用于事务控制
    private TransactionTemplate transactionTemplate;
    
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }
    public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
        this.transactionTemplate = transactionTemplate;
    }
    public Account findAccountBuId(final Integer id) {
        return transactionTemplate.execute(new TransactionCallback<Account>() {
            public Account doInTransaction(TransactionStatus transactionStatus) {
                return accountDao.findAccountById(id);
            }
        });
    }

    public void transfer(final String sourceName, final String targetName, final float money) {
	    // 执行transactionTemplate.execute方法,创建一个匿名内部类TransactionCallback来作为参数。
        transactionTemplate.execute(new TransactionCallback<Object>() {
            public Object doInTransaction(TransactionStatus transactionStatus) {
                System.out.println("transfer....");
                //2.1根据账户名查询转出账户
                Account sourceAccount = accountDao.findAccountByName(sourceName);
                //2.2根据账户名查询转入账户
                Account targetAccount = accountDao.findAccountByName(targetName);
                //2.3转出账户减少money
                sourceAccount.setMoney(sourceAccount.getMoney() - money);
                //2.3转入账户增加money
                targetAccount.setMoney(targetAccount.getMoney() + money);

                //2.4更新账户
                accountDao.updateAccount(sourceAccount);

                // int i = 1 / 0;

                accountDao.updateAccount(targetAccount);
                return null;
            }
        });
    }
}

具体执行流程如下
在这里插入图片描述
这就是整个Spring 基于 编程式 的事务控制。但是如果Service的实现类中有很多方法都需要进行事务控制就需要写很多这种代码,又会造成service的实现类比较臃肿,有很多重复代码,所以在实际开发中很少使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值