从零开始认识并操纵Spring Aop事务

目录

声明:从零开始,并不代表你对java Spring一点都不懂的程度哈,本实例只是过一个概貌,详细内容会分多篇文章拆解

业务介绍

要想要设计一个和事务操作相关的业务,我们以最经典的转账的例子来描述

转账,即两个用户之间钱的关系变动,但要求总和不变

版本声明

本案例使用的Web 版本2.5,Spring 版本为4.3

开发工具为eclipse

操作步骤

导包

Spring 各版本官方包下载

下载的包包含Spring包以及第三方工具整合Spring的所有包,但是我们不需要全部导入,只需根据项目需求导入即可

Spring 核心包 + apache logging包

  • Spring-core
  • Spring-bean
  • Spring-express
  • Spring-context

  • apache.commons.logging
  • apache.log4j

Spring 测试包

  • Spring-test

Spring Aop 事务包

  • Spring-aop
  • Spring-aspect
  • com.springsource.org.aopalliance
  • com.springsource.org.aspectj.weaver
  • Spring-jdbc
  • Spring-tx

其他包

  • c3p0连接池
  • JDBC取得包

以上一共15个包

注意的是:每一类Spring 包分为 RELEASE 和 javadoc和source包,导入只需用RELEASE包

准备数据库

这里准备的数据库就简单一个表,主要包含用户名和账户即可

image.png

而我实际运用的表会多几个字段,但这并不影响

编写javaBean

编写User 的Bean类,注意的是属性名和表字段名一样即可,顺便加个toString和构造方法,一定要有空参构造哦。

书写Dao实现接口

为了完整起见,我们把Dao类的增删改查也顺便实现以下

public interface AccountDao {
    void save(User u);
    void delete(Integer id);
    void update(User u);
    User getById(Integer id);
    int getTotalCount();
    List<User> getAll();
    void increaseAccount(Integer id, Double money);
    void decreaseAccount(Integer id, Double money);
}

实现Dao类,这里我们不集成其他ORM框架,采用的是原生的JDBC + Spring技术

在写实现类的时候,需要继承一个JdbcDaoSupport来为我们生成JdbcTemplate模板

这里就顺带体验一下使用JdbcDaoSupport增删改查的操作,如果已经知道的可以略过

public class UserDaoImpl extends JdbcDaoSupport implements UserDao {

    @Override
    public void save(User u) {
        String sql = "insert into sys_user values(null, ?,?,?,?,?)";
        super.getJdbcTemplate().update(sql, u.getUser_code(), u.getUser_name(),
                            u.getUser_password(), u.getUser_state(), u.getAccount());
    }

    @Override
    public void delete(Integer id) {
        String sql = "delete from sys_user where user_id=?";
        super.getJdbcTemplate().update(sql, id);
    }

    @Override
    public void update(User u) {
        String sql = "update sys_user set user_name=? where user_id=?";
        super.getJdbcTemplate().update(sql, u.getUser_name(), u.getUser_id());
    }

    @Override
    public User getById(Integer id) {
        String sql = "select * from sys_user where user_id = ?";
        return super.getJdbcTemplate().queryForObject(sql, new RowMapper<User>() {

            @Override
            public User mapRow(ResultSet rs, int index) throws SQLException {
                User u = new User(rs.getInt("user_id"), rs.getString("user_code"),
                        rs.getString("user_name"), rs.getString("user_password"),
                        rs.getString("user_state").toCharArray()[0], rs.getDouble("account"));
                return u;
            }
            
        }, id);
    }

    @Override
    public int getTotalCount() {
        String sql = "select count(*) from sys_user";
        Integer count = super.getJdbcTemplate().queryForObject(sql, Integer.class);
        return count;
    }

    @Override
    public List<User> getAll() {
        String sql = "select * from sys_user";
        return super.getJdbcTemplate().query(sql, new RowMapper<User>() {

            @Override
            public User mapRow(ResultSet rs, int index) throws SQLException {
                User u = new User(rs.getInt("user_id"), rs.getString("user_code"),
                        rs.getString("user_name"), rs.getString("user_password"),
                        rs.getString("user_state").toCharArray()[0], rs.getDouble("account"));
                return u;
            }
            
        });
    }

    @Override
    public void increaseAccount(Integer id, Double money) {
        String sql = "update sys_user set account=account+? where user_id=?";
        super.getJdbcTemplate().update(sql, money, id);
    }

    @Override
    public void decreaseAccount(Integer id, Double money) {
        String sql = "update sys_user set account=account-? where user_id=?";
        super.getJdbcTemplate().update(sql, money, id);
    }
}

Spring 配置

在Spring 中,只要把业务写好了,剩下的就全靠配置了

数据库连接配置

这里我们采用通用的一种,使用properties文件配置数据库参数

在 src 目录下的db.properties中

jdbc.jdbcUrl=jdbc:mysql:///test
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.user=root
jdbc.password=******

创建Spring配置文件

原则上Spring配置文件可以放置在任何位置,起任意名字,但是一般都会被统一化

统一是src 目录下的 applicationContext.xml文件

导入约束

在这里导入约束的操作可以直接使用eclipse完成,Spring 约束文件都在Spring包中的schema目录下,

image.png

里边有很多,这里需要导入的只需要

  • beans (对象注入)
  • context (读取properties配置)
  • aop (aop配置)
  • tx (Spring事务)

里边找到最高版本的xsd约束文件即可

使用eclipse 导入约束的方法:

windows -> preferences 里搜索xml Catalog

image.png

里边找到刚才介绍的其中一个约束文件,比如这里使用context

注意将key type 修改为 Schema location,并在后面追加文件名

image.png

接下来是需要在配置文件标签中引入这种新版的xsd配置,这里使用eclipse操作

打开applicationContext.xml文件,输入beans标签,进入设计模式

image.png

对beans添加命名空间

image.png

首先先加载xsi

image.png

然后再加载刚刚在xml Catalog导入的xsd文件
如果刚才加入的是beans就先找spring-beans

image.png

这里让beans 使用无前缀,但是所有的约束当中只能存在一个xsi约束无前缀

image.png

比如下一个context就需要带前缀了

image.png

对于每一个约束,都要进行以上操作哦,也就是说,重复4次

其实最终的效果就是生成这样的配置

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" 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-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd ">

</beans>

配置数据库连接池

使用连接池进行数据库连接,连接参数读取properties文件

配置如下:

<!-- 读取db.properties配置 -->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 将连接池放入spring容器 -->
<bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
    <property name="driverClass" value="${jdbc.driverClass}"></property>
    <property name="user" value="${jdbc.user}"></property>
    <property name="password" value="${jdbc.password}"></property>
</bean>

注入连接池到UserDao

接下来我们要分析依赖关系,在UserDao中继承了JDBCDaoSupport类

这个类中可以获得JDBC模板,这个模板的创建需要依赖连接池

因此首先要先创建连接池对象(上面已经完成),再将连接池注入到UserDao类中去

<!-- 将UserDao放入到Spring容器中 -->
<bean name="UserDao" class="edu.scnu.dao.UserDaoImpl">
    <property name="dataSource" ref="dataSource"></property>
</bean>

使用Junit和Spring整合测试

到了这个阶段,有必要进行一波连接测试了,

这里测试我们显然需要使用JUnit,但是我们发现写每个测试方法都要加入这么一句Spring的Context对象创建的声明

ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        
UserDao userDao = (UserDao) ac.getBean("userDao");

Spring 给了我们贴心的简化测试类书写的操作,就是使用注解

  • RunWith
  • ContextConfiguration
  • 这些都在Spring-test包下
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestJdbc {
    @Resource(name="userDao")
    private UserDao ud;
    
    @Test
    public void testSave() {
        User u = new User();
        u.setUser_name("测试用户");
        u.setUser_state('0');
        u.setAccount(1000d);
        ud.save(u);
    }
    
    @Test
    public void testUpdate() {
        User u = new User();
        u.setUser_name("测试用户2");
        ud.update(u);
    }
    
    @Test
    public void testDelete() {
        ud.delete(7);
    }
    
    @Test
    public void testFind() {
        System.out.println(ud.getById(7));
        System.out.println(ud.getAll());
    }
}

一波测试后确实发现很多问题... 这里都修改完成了。

AOP 事务操作的实现方式

一些相关概念回顾

数据库事务

Spring Aop

编码式

书写转账的业务接口和实现类

public class UserServiceImpl implements UserService {
    
    private UserDao ud;

    @Override
    public void transfer(Integer from, Integer to, Double money) {
        ud.decreaseAccount(from, money);
        
        ud.increaseAccount(to, money);
    }

    public void setUd(UserDao ud) {
        this.ud = ud;
    }

}

将核心事务管理器配置到spring容器

<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>

配置TransactionTemplate模板

<bean name="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager" ref="transactionManager"></property>
</bean>

将事务模板注入Service

<bean name="userService" class="edu.scnu.service.UserServiceImpl">
    <property name="ud" ref="userDao"></property>
</bean>

在Service中使用事务模板

public class UserServiceImpl implements UserService {
    
    private UserDao ud;
    
    private TransactionTemplate txtemplate;

    @Override
    // 注意在老版本的java中,内部函数访问不了外部参数的时候,需要把外部参数类型加上final修饰
    public void transfer(Integer from, Integer to, Double money) {
        
        txtemplate.execute(new TransactionCallbackWithoutResult() {
            
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus arg0) {
                
                ud.decreaseAccount(from, money);
                
                ud.increaseAccount(to, money);
            }
        });

    }

    public void setUd(UserDao ud) {
        this.ud = ud;
    }

}

配置式

使用编程式的方法,其实本质上还是要写事务的代码到每个业务身上,只不过它内部封装的一下,让我们不用写这么多,但说白了还是要写

那么通过配置的方式,我们可以彻底在业务层上移除事务处理的代码

那这个怎么来配置呢?在Spring Aop中,对于这类问题的抽象,分为了三个核心概念:通知和切点以及他两构成的切面

我们需要配的就是通知和织入配置这两个部分了:

配置事务通知

首先配置通知,这个通知其实是用于完成事务操作,如果要自己写的话,我们肯定会选用环绕通知,异常包裹,但是贴心的Spring给我们简化了这步操作,我们只需要配置事务管理器注入即可。

配置事务的一些属性,分别注明了被通知的方法名,隔离级别,传播级别以及只读限制,这些内容会在后续补充哦!

    <bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
        <property name="dataSource" ref="dataSource" ></property>
    </bean>
    
    <!-- 配置事务通知 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="save*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
            <tx:method name="update*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
            <tx:method name="delete*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
            <tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true"/>
            <tx:method name="find*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true"/>
            <tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
        </tx:attributes>
    </tx:advice>

配置织入————切面

首先需要的是切点,切点要求用spring提供的表达式来匹配实际需要被通知的类方法名

其次是切面,切面可以说就是切点和通知的交际了!

<!-- 配置织入 -->
<aop:config>
    <!-- 配置切点 -->
    <aop:pointcut expression="execution(* edu.scnu.service.UserServiceImpl.*(..))" id="txPc"/>
    <!-- 配置切面: 通知+切点 -->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPc"/>
</aop:config>

<!-- 将UserDao放入到Spring容器中 -->
<bean name="userService" class="edu.scnu.service.UserServiceImpl">
    <property name="ud" ref="userDao"></property>
</bean>

到这里,配置就算完成了!

简化的Service

通过通知和切点的配置后,Service方法不再需要显示显明事务操作了,直接专注于业务操作即可,也就是回到远古时代,那个时候你不知道事务,不知道什么异常,你还是可以完成安全无误的代码

@Override
public void transfer(Integer from, Integer to, Double money) {
    ud.decreaseAccount(from, money);

    ud.increaseAccount(to, money);
}

测试方法

对于测试方法,需要创建UserService对象调用transfer方法即可,这里UserService对象我们也是采用@Resource注解引入,这项配置在刚才 将UserDao放入到Spring容器中 已经完成过

@Resource(name="userService")
private UserService us;

@Test
public void testTransfer() {
    us.transfer(3, 1, 100d);
}

对于异常测试,我们只需要尝试在业务方法,转账过程中间插入异常测试即可

@Override
public void transfer(Integer from, Integer to, Double money) {
    ud.decreaseAccount(from, money);
    int i = 1 / 0; // 引入异常测试
    ud.increaseAccount(to, money);
}

注解式

注解配置可以说是上述xml配置的一种简化版,因为上面的方法好是好,只不过配置文件的内容毕竟太多,而且很多时候还需要对照着来看

因此JDK1.6注解的引入,大大简化了大多数框架的xml配置

开启使用注解管理aop事务

<!-- 开启使用注解管理aop事务 -->
    <tx:annotation-driven/>

在需要使用事务的Service方法中添加如下注解

@Override
@Transactional(isolation=Isolation.REPEATABLE_READ, propagation=Propagation.REQUIRED,readOnly=false)
public void transfer(Integer from, Integer to, Double money) {
    ud.decreaseAccount(from, money);

    ud.increaseAccount(to, money);
}

注意,在eclipse中,修改了配置文件,最好要clean一下project否则容易报配置文件失效是产生的错误

java.lang.Error: Unresolved compilation problems:

也可以将该注解配置到类声明之前,让所有方法都有该事务通知

@Transactional(isolation=Isolation.REPEATABLE_READ,propagation=Propagation.REQUIRED,readOnly=false)
public class UserServiceImpl implements UserService {
    
    private UserDao ud;

    @Override
    public void transfer(Integer from, Integer to, Double money) {
        ud.decreaseAccount(from, money);

        ud.increaseAccount(to, money);
    }

    @Override
    public void save(User u) {
        ud.save(u);
    }

    @Override
    public User find(Integer id) {
        return ud.getById(id);
    }
    
// ...

}

当然,如果有某些方法需要特殊配置,那么只需要专门针对这个方法添加注解即可

@Override
@Transactional(isolation=Isolation.REPEATABLE_READ,propagation=Propagation.REQUIRED,readOnly=true)
public User find(Integer id) {
    return ud.getById(id);
}

@Override
@Transactional(isolation=Isolation.REPEATABLE_READ,propagation=Propagation.REQUIRED,readOnly=true)
public List<User> getAll() {
    return ud.getAll();
}

好了,整个流程到这里就算完成了,整个流程的详细项目代码可以踩我的github结合观光:https://github.com/Autom-liu/SpringAopLearn,整个流程涉及到诸多知识细节需要慢慢琢磨,这里只给大家展示整个概貌,具体细节会在后续的文章中慢慢解释,期待大家的支持和star哦~~~

猜你喜欢

转载自www.cnblogs.com/autom/p/10010218.html