SPring的事物有两种方式,一种是编程式事物,一种是基于注解的申明式事物,编程式事物由纯Java代码编写,侵入性比较强,所以实际项目上应用比较少。本章主要介绍的就是Spring的申明式事物,在介绍Spring事物之前,首先要了解几个基本概念:
- 事物是什么?
官方一点的说法,事物就是数据库的一次原子操作。何为原子操作?就是逻辑上一个不可分割的操作单元,比如本章中举的例子,取商店买书,你要给老板钱(减余额),然后老板给你书(减库存),这是两个不可少的步骤,其中一个操作不成功,那么本次操作就算失败了,这就是事物 - 事物的基本特性
原子性,隔离性,一致性,持久性。(具体本章不再讨论,不清楚的请自行百度)。
本案例中使用的数据库是oracle 11g,客户端工具使用的是PLSQL,首先测试之前要创建好相关的表结构和数据:
CREATE TABLE BOOK(
BID INTEGER PRIMARY KEY,--书本主键
BNAME VARCHAR2(50) NOT NULL,--书本名称
PRICE FLOAT NOT NULL,--价格
STOCK INTEGER DEFAULT 0--库存
);
CREATE TABLE CUSTOMER(
CID INTEGER PRIMARY KEY,--人员编号
CNAME VARCHAR2(25) NOT NULL,--人员名称
BALANCE FLOAT DEFAULT 0--人员余额
);
INSERT INTO BOOK VALUES(1,'三国演义',19.5,10);
INSERT INTO BOOK VALUES(2,'红楼梦',21,5);
INSERT INTO CUSTOMER VALUES(1,'张三',250);
这里创建了一张Customer和Book表,并插入了几条数据做测试数据。接下来Java代码的编写,本项目使用mavne和idea来搭建的:
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.12.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.12.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.oracle/ojdbc6 -->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>12.1.0.1-atlassian-hosted</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.11</version>
</dependency>
这里引用了spring-aop,spring-core,spring-beans,spring-context,spring-jdbc,spring-tx,aspectjweaver,commons-logging,commons-dbcp,commons-pools等一些关键的jar。
BookDao.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.Map;
@Repository("bookDao")
public class BookDao {
@Autowired
JdbcTemplate jdbc;
public Map<String,Object> findBookInfoById(int id,String fields) throws Exception{
String sql = "SELECT "+fields+" FROM BOOK WHERE BID="+id+"";
if (jdbc == null)
System.out.println("未获取到jdbc");
Map<String,Object> obj = jdbc.queryForMap(sql);
if (obj == null)
throw new RuntimeException("未找到书籍!");
return obj;
}
public void subBookStock(int id,int amount) throws Exception{
String sql = "UPDATE BOOK SET STOCK = STOCK-"+amount+" WHERE ID = "+id;
jdbc.execute(sql);
}
}
CustomerDao.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.Map;
@Repository("customerDao")
public class CustomerDao {
@Autowired
JdbcTemplate jdbc;
public float findCustomerBalance(int id) throws Exception{
String sql = "SELECT BALANCE FROM CUSTOMER WHERE CID = "+id;
Map<String,Object> obj = jdbc.queryForMap(sql);
if (obj == null)
throw new RuntimeException("没有找到该用户!");
return Integer.parseInt(obj.get("BALANCE").toString());
}
public void subCustomerBalance(int id,float total) throws Exception{
String sql = "UPDATE CUSTOMER SET BALANCE = BALANCE-"+total+"WHERE CID = "+id;
}
}
ShopService.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
@Service("shopService")
public class ShopService {
@Autowired
private BookDao bookDao;
@Autowired
private CustomerDao customerDao;
/**
* 销售数据
* @param bid 书本编号
* @param cid 人员编号
* @param amount 书本数量
*/
@Transactional
public void sellBook(int bid,int cid,int amount) throws Exception {
Map<String,Object> bookInfo = bookDao.findBookInfoById(bid,"PRICE,STOCK");
float balance = customerDao.findCustomerBalance(cid);
float price = Float.parseFloat(bookInfo.get("PRICE").toString());
int stock = Integer.parseInt(bookInfo.get("STOCK").toString());
float total = price*amount;
if (balance < total)
throw new RuntimeException("余额不足!");
customerDao.subCustomerBalance(cid, total);//顾客交钱
if (stock < amount)
throw new RuntimeException("库存不足!");
bookDao.subBookStock(bid, amount);//减书本库存
}
}
App.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App
{
public static void main( String[] args )
{
ApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml");
ShopService shopService = (ShopService) context.getBean("shopService");
if (shopService == null)
System.out.println("获取shopService失败");
try {
shopService.sellBook(1,1,11);//可以修改相关的参数和购买数量
System.out.println("出售成功");
}catch (Exception e){
System.out.println("出错--->"+e.getLocalizedMessage());
}
}
}
spring-context.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:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
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/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-4.0.xsd ">
<beans>
<context:annotation-config/>
<tx:annotation-driven/>
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="oracle.jdbc.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@localhost:1521:ORCL"/>
<property name="username" value="amxing"/>
<property name="password" value="950411"/>
<property name="defaultAutoCommit" value="true"/>
<property name="defaultQueryTimeout" value="10000"/>
<property name="enableAutoCommitOnReturn" value="true"/>
</bean>
<bean id="jdbc" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="bookDao" class="com.amx.BookDao"/>
<bean id="customerDao" class="com.amx.CustomerDao"/>
<bean id="shopService" class="com.amx.ShopService"/>
<tx:advice id="tx_bookservice">
<tx:attributes>
<tx:method name="sellBook" rollback-for="Exception" propagation="MANDATORY"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="sellMethod" expression="execution(* com.amx.ShopService.sellBook(..))"/>
<aop:advisor advice-ref="tx_bookservice" pointcut-ref="sellMethod"/>
</aop:config>
</beans>
</beans>
这就是主要的代码,可以看到spring的申明式事物主要是基于aop来实现的,配置好事物规则,再在相关的方法上添加@Transactional注解即可关于这个注解,有几个参数
- value String 可选的限定描述符,指定使用的事务管理器
- propagation enum: Propagation 可选的事务传播行为设置
- isolation enum: Isolation 可选的事务隔离级别设置
- readOnly boolean 读写或只读事务,默认读写
- timeout int (in seconds granularity) 事务超时时间设置
- rollbackFor Class对象数组,必须继承自Throwable 导致事务回滚的异常类数组
- rollbackForClassName 类名数组,必须继承自Throwable 导致事务回滚的异常类名字数组
- noRollbackFor Class对象数组,必须继承自Throwable 不会导致事务回滚的异常类数组
- noRollbackForClassName 类名数组,必须继承自Throwable 不会导致事务回滚的异常类名字数组
显而易见sellBook方法的参数为1,1,11,就是张三这个人要买11本红楼梦,但是呢,红楼梦只有10,所以事物是失败的,但是在sellBook方法中,用户是先交钱,张三的余额有250元是足够买11本红楼梦的,所以这时张三的余额是会减少的,但是接下来库存不够了,书本减少库存这个操作就不能做了,所以如果我们此处不用事物的话,会出现以下的情况:
程序执行失败,然后取查看一下用户的余额:
余额减少了19.5*11元,再去看一下书本的库存:
这个时候就发现问题了,用户的余额减少了,但是商品的库存没有减少,这在逻辑上是完全不正确的,所以这个时候就体现了事物的作用。如果这里我们使用了事物就不会出现这样的情况了。
这是一个最基本的Spring申明式事物的小例子,当然在实际开发的过程中,很多复杂的逻辑会写在存储过程中,这里我也写了一个小例子,仅供参考:
--出售商品
CREATE OR REPLACE PROCEDURE PROC_SELL_BOOK(V_BID IN INTEGER,
V_CID IN INTEGER,
V_AMOUNT IN INTEGER,
V_RESULT OUT VARCHAR2) AS
SELL_EXCEPTION EXCEPTION;
V_SLID INTEGER; --日志ID
V_NAME VARCHAR2(50); --商品名称
V_STOCK INTEGER; --库存数量
V_PRICE FLOAT; --商品价格
V_CNAME VARCHAR2(50); --顾客姓名
V_BALANCE FLOAT; --账户余额
BEGIN
SELECT SELL_LOG_ID.NEXTVAL INTO V_SLID FROM DUAL;
SELECT BNAME, PRICE, STOCK
INTO V_NAME, V_PRICE, V_STOCK
FROM BOOK
WHERE BID = V_BID;
SELECT CNAME, BALANCE
INTO V_CNAME, V_BALANCE
FROM CUSTOMER
WHERE CID = V_CID;
IF V_BALANCE < V_PRICE * V_AMOUNT THEN
V_RESULT := V_CNAME || '的余额不足!';
RAISE SELL_EXCEPTION;
ELSE
UPDATE CUSTOMER
SET BALANCE = BALANCE - V_PRICE * V_AMOUNT
WHERE CID = V_CID;
END IF;
IF V_STOCK < V_AMOUNT THEN
V_RESULT := V_NAME || '的库存不足!';
RAISE SELL_EXCEPTION;
ELSE
UPDATE BOOK SET STOCK = STOCK - V_AMOUNT WHERE BID = V_BID;
END IF;
INSERT INTO SELL_LOG VALUES (V_SLID, V_BID, V_CID, V_RESULT, SYSDATE);
COMMIT;
EXCEPTION
WHEN SELL_EXCEPTION THEN
ROLLBACK;
INSERT INTO SELL_LOG VALUES (V_SLID, V_BID, V_CID, V_RESULT, SYSDATE);
COMMIT;
WHEN NO_DATA_FOUND THEN
V_RESULT := '未找到对应的商品或用户,请检查用户ID是是否正确!';
INSERT INTO SELL_LOG VALUES (V_SLID, V_BID, V_CID, V_RESULT, SYSDATE);
COMMIT;
END;
测试结果如下: