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

[TOC]

声明:从零开始,并不代表你对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-5b42d8-1542986512016)]

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

编写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-f10d1-1542986512016)]

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

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

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

使用eclipse 导入约束的方法:

windows -> preferences 里搜索xml Catalog

[图片上传失败...(image-820e1b-1542986512016)]

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

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

[图片上传失败...(image-a9f9f3-1542986512016)]

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

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

[图片上传失败...(image-ffe055-1542986512016)]

对beans添加命名空间

[图片上传失败...(image-18fa9c-1542986512016)]

首先先加载xsi

[图片上传失败...(image-457081-1542986512017)]

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

[图片上传失败...(image-a03e7f-1542986512017)]

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

[图片上传失败...(image-62fd17-1542986512017)]

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

[图片上传失败...(image-10a9c2-1542986512017)]

对于每一个约束,都要进行以上操作哦,也就是说,重复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哦~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容