[TOC]
声明:从零开始,并不代表你对java Spring一点都不懂的程度哈,本实例只是过一个概貌,详细内容会分多篇文章拆解
业务介绍
要想要设计一个和事务操作相关的业务,我们以最经典的转账的例子来描述
转账,即两个用户之间钱的关系变动,但要求总和不变
版本声明
本案例使用的Web 版本2.5,Spring 版本为4.3
开发工具为eclipse
操作步骤
导包
下载的包包含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哦~~~