文章目录
日志编号 | 说明 |
---|---|
C-2020-10-12 | 第一次创建 |
AOP基础概念
AOP:Aspect Oriented Programming 面向切面编程
OOP:Object Oriented Programming 面向对象编程
面向切面编程:基于OOP基础之上新的编程思想,OOP面向的主要对象是类,而AOP面向的主要对象是切面,在处理日志、安全管理、事务管理等方面有非常重要的作用。AOP是Spring中重要的核心点,虽然IOC容器没有依赖AOP,但是AOP提供了非常强大的功能,用来对IOC做补充。通俗点说的话就是在程序运行期间,将某段代码动态切入到指定方法的指定位置进行运行的这种编程方式。
AOP的核心概念和术语
- 切面(Aspect): 指关注点模块化,这个关注点可能会横切多个对象。事务管理是企业级Java应用中有关横切关注点的例子。 在Spring AOP中,切面可以使用通用类基于模式的方式(schema-based approach)或者在普通类中以
@Aspect
注解(AspectJ 注解方式)来实现。 - 连接点(Join point): 在程序执行过程中某个特定的点,例如某个方法调用的时间点或者处理异常的时间点。在Spring AOP中,一个连接点总是代表一个方法的执行。
- 通知(Advice): 在切面的某个特定的连接点上执行的动作。通知有多种类型,包括“around”, “before” and “after”等等。通知的类型将在后面的章节进行讨论。 许多AOP框架,包括Spring在内,都是以拦截器做通知模型的,并维护着一个以连接点为中心的拦截器链。
- 切点(Pointcut): 匹配连接点的断言。通知和切点表达式相关联,并在满足这个切点的连接点上运行(例如,当执行某个特定名称的方法时)。切点表达式如何和连接点匹配是AOP的核心:Spring默认使用AspectJ切点语义。
- 引入(Introduction): 声明额外的方法或者某个类型的字段。Spring允许引入新的接口(以及一个对应的实现)到任何被通知的对象上。例如,可以使用引入来使bean实现
IsModified
接口, 以便简化缓存机制(在AspectJ社区,引入也被称为内部类型声明(inter))。 - 目标对象(Target object): 被一个或者多个切面所通知的对象。也被称作被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,那么这个对象永远是一个被代理(proxied)的对象。
- AOP代理(AOP proxy):AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能)。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。
- 织入(Weaving): 把切面连接到其它的应用程序类型或者对象上,并创建一个被被通知的对象的过程。这个过程可以在编译时(例如使用AspectJ编译器)、类加载时或运行时中完成。 Spring和其他纯Java AOP框架一样,是在运行时完成织入的。
AOP的通知类型
- 前置通知(Before advice): 在连接点之前运行但无法阻止执行流程进入连接点的通知(除非它引发异常)。
- 后置返回通知(After returning advice):在连接点正常完成后执行的通知(例如,当方法没有抛出任何异常并正常返回时)。
- 后置异常通知(After throwing advice): 在方法抛出异常退出时执行的通知。
- 后置通知(总会执行)(After (finally) advice): 当连接点退出的时候执行的通知(无论是正常返回还是异常退出)。
- 环绕通知(Around Advice):环绕连接点的通知,例如方法调用。这是最强大的一种通知类型,。环绕通知可以在方法调用前后完成自定义的行为。它可以选择是否继续执行连接点或直接返回自定义的返回值又或抛出异常将执行结束。
AOP的应用场景
- 日志管理
- 权限认证
- 安全检查
- 事务控制
Spring AOP 简单配置
出于学习的目的,同样的例子,我们做两个版本,一个是用配置文件,另一个是通过注解。
首先,导入需要的POM信息。根据之前说到的Spring结构图可以看出来在使用AOP、aspects的时候,需要引入这两个相应的模块。
先给出POM信息
<?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.phl</groupId>
<artifactId>spring_csdn</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
</dependencies>
</project>
准备好POM之后,我这次选用了网上常见的一个例子进行编写,编写一个有两个入参,一个出参,都是整数的计算器,AOP在其中的作用是记录日志。
基于XML的Spring AOP
先给出计算器类:
package com.phl.spring.csdn.bean;
public class MyCalculator {
public Integer add(Integer i,Integer j) throws Exception{
return i + j;
}
public Integer sub(Integer i,Integer j) throws Exception{
return i - j;
}
public Integer mul(Integer i,Integer j) throws Exception{
return i * j;
}
public Integer div(Integer i,Integer j) throws Exception{
return i / j;
}
}
之后给出日志工具类:
package com.phl.spring.csdn.util;
import org.aspectj.lang.JoinPoint;
import java.util.Arrays;
public class LogUtil {
private void start(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println(methodName + "方法即将执行,入参是:" + Arrays.asList(args));
}
public static void stop(JoinPoint joinPoint,Integer data) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + "方法执行结束,执行结果是:" + data.toString());
}
public static void exception(JoinPoint joinPoint,Throwable th) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + "方法发生异常,异常信息是:" + th.getMessage());
th.printStackTrace();
}
public static void end(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + "方法执行完毕");
}
}
接着给出对应的spring配置文件
<?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 https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="myCalculator" class="com.phl.spring.csdn.bean.MyCalculator"></bean>
<bean id="logUtil" class="com.phl.spring.csdn.util.LogUtil"></bean>
<aop:config>
<aop:pointcut id="baseCut" expression="execution(* com.phl.spring.csdn.bean.MyCalculator.*(Integer,Integer))"/>
<aop:aspect ref="logUtil">
<aop:before method="start" pointcut-ref="baseCut"></aop:before>
<aop:after-returning method="stop" pointcut-ref="baseCut" returning="data"></aop:after-returning>
<aop:after-throwing method="exception" pointcut-ref="baseCut" throwing="th"></aop:after-throwing>
<aop:after method="end" pointcut-ref="baseCut"></aop:after>
</aop:aspect>
</aop:config>
</beans>
最后,是测试类。
package com.phl.spring;
import com.phl.spring.csdn.bean.MyCalculator;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestXmlMyCalculator {
@Test
public void test01(){
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("application.xml");
MyCalculator myCalculator = (MyCalculator) classPathXmlApplicationContext.getBean("myCalculator");
try {
myCalculator.div(4,1);
} catch (Exception e){
}
}
}
基于XML的Spring AOP 简要讲解
这个讲解内容是匹配上面的例子进行的。有这么如下几个重点内容需要留意。
- 切面类中的方法,不论是不是public,static,各种返回值,在实际使用中没差别,仔细看我的切面类,里面有static,也有不是static,也有private的,这些在使用中没有区别
- 当要给切面中的通知方法增加入参的时候,第一个参数必须是joinpoint,往后,才可以写自己需要的参数
- 切面类中的stop,实际就是业务方法执行了return,而切面类中的end,正儿八经是业务方法执行完毕,不论业务方法有没有出异常
- 留意application.xml中的写法和顺序,aop:pointcut需要出现在aop:aspect的上方
- 配置文件中,切面内的那些before等就是不同的通知类型
- <aop:after-returning method=“stop” pointcut-ref=“baseCut” returning=“data”>为例,这里出现了一个属性“returning”,这个属性是aop命名空间中定义好的,表示返回值,在切面类的stop方法中,有一个Integer类型的入参data,returning="data"表示的意思就是说这个入参data就是业务方法的返回值。同理,这个配置下面的throwing="th"也是同样的作用
注意,如果大家觉得通过AOP的命名空间去查看里面的各个属性具体有什么太麻烦(确实也麻烦),那我们可以通过查看注解的代码达到同样的目的。
基于注解的Spring AOP
下面就给出基于注解的写法。
先给出MyCalculator类
package com.phl.spring.csdn.bean;
import org.springframework.stereotype.Component;
@Component
public class MyCalculator {
public Integer add(Integer i,Integer j) throws Exception{
return i + j;
}
public Integer sub(Integer i,Integer j) throws Exception{
return i - j;
}
public Integer mul(Integer i,Integer j) throws Exception{
return i * j;
}
public Integer div(Integer i,Integer j) throws Exception{
return i / j;
}
}
然后是LogUtil
package com.phl.spring.csdn.util;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Aspect
public class LogUtil {
@Pointcut("execution(* com.phl.spring.csdn.bean.MyCalculator.*(Integer,Integer))")
public void point() {}
@Before("point()")
private void start(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println(methodName + "方法即将执行,入参是:" + Arrays.asList(args));
}
@AfterReturning(returning = "data",value = "point()")
public static void stop(JoinPoint joinPoint, Integer data) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + "方法执行结束,执行结果是:" + data.toString());
}
@AfterThrowing(throwing = "th",value = "point()")
public static void exception(JoinPoint joinPoint, Throwable th) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + "方法发生异常,异常信息是:" + th.getMessage());
th.printStackTrace();
}
@After("point()")
public static void end(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + "方法执行完毕");
}
}
最后是application.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--开启IOC容器注解扫描-->
<context:component-scan base-package="com.phl.spring.csdn"></context:component-scan>
<!--开启AOP注解-->
<aop:aspectj-autoproxy ></aop:aspectj-autoproxy>
</beans>
测试类还是之前的测试类,没有什么特别的。
基于注解的Spring AOP的简要讲解
在使用注解的时候,首先有一个前提,就是要熟悉注解的写法和用法。如果对这里有疑问的,可以点击这个链接去查看相应内容Java基础知识-07-C-2020-08-11.
- 切面中,可以定义多个空方法,然后加上@pointcut注解,在注解中写上切点表达式。注意一个,execution只是切点表达式中的一种,只是大家都比较习惯这个写法了。关于execution的具体写法,这里不过多介绍,网上资料很多
- 如果不统一声明切点表达式的话,就需要在before等通知注解中的value里写上对应的表达式
- 以@AfterReturning为例,这里出现的returning,与XML中的一样,指明这个入参是业务方法的返回值,更多具体信息,可以点到AfterReturning内部去看
- 在使用注解的时候,需要在XML文件中开启 IOC容器注解扫描和AOP注解扫描
- 上面的例子里没有说around这个通知方式,因为demo演示起来看的不清晰,推荐自己练一下
声明式事务管理
所有涉及到数据库的应用中,都离不开事务管理。Spring AOP 提供了我们一种更简单的解决方案,声明式事务管理。
先提一句,与声明式事务管理对应的是编码式事务管理。
- 编程式事务:在代码中直接加入处理事务的逻辑,可能需要在代码中显式调用beginTransaction()、commit()、rollback()等事务管理相关的方法。
- 声明式事务:在方法的外部添加注解或者直接在配置文件中定义,将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。spring的AOP恰好可以完成此功能:事务管理代码的固定模式作为一种横切关注点,通过AOP方法模块化,进而实现声明式事务。
demo
要使用声明式事务管理,首先需要引入对应的pom信息。需要除了上面使用AOP的那些依赖之外,还需要将数据库连接,事务,jdbc工具引入到项目中。
<?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.phl</groupId>
<artifactId>spring_csdn</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.9.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
</dependencies>
</project>
因为这篇是讲解AOP,因此尽量弱化了其他工具主流工具的技术需求。在这个例子里,没有用各类的数据库连接池,没有引入mybatis,都在用Spring本身的内容。
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:db.properties"></context:property-placeholder>
<!--开启注解扫描-->
<context:component-scan base-package="com.phl.spring.csdn"></context:component-scan>
<!--开启AOP注解-->
<aop:aspectj-autoproxy ></aop:aspectj-autoproxy>
<!--数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${url}"></property>
<property name="driverClassName" value="${driver}"></property>
<property name="username" value="${username}"></property>
<property name="password" value="${password}"></property>
</bean>
<!--JDBC工具-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
</bean>
</beans>
暂且科普一下,Spring JdbcTemplate,是Spring针对JDBC做的一个封装工具,现实项目很少会直接用Spring JdbcTemplate。大家对他就知道一下就行,这个就是个便于执行SQL的工具。
再浅谈一下事务,所谓事务,就是一系列操作数据库的动作,统一成一个单元。这个操作单元要么都成功,要么一旦有一个或者多个失败,则需要把数据回滚到这个操作单元执行之前的样子。
现在举一个例子,就是卖书。书有价格,库存信息,顾客有自己的金额信息。下面给出具体代码。
首先是给出事务管理的配置信息,在application.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: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/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:property-placeholder location="classpath:db.properties"></context:property-placeholder>
<!--开启注解扫描-->
<context:component-scan base-package="com.phl.spring.csdn"></context:component-scan>
<!--开启AOP注解-->
<aop:aspectj-autoproxy ></aop:aspectj-autoproxy>
<!--数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${url}"></property>
<property name="driverClassName" value="${driver}"></property>
<property name="username" value="${username}"></property>
<property name="password" value="${password}"></property>
</bean>
<!--JDBC工具-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
</bean>
<!--下面是事务管理的配置信息-->
<!--配置事务管理器的bean-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--开启基于注解的事务控制模式,依赖tx名称空间-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>
先对上面的配置文件进行简单说明,在这个配置文件中,已经引入了三个命名空间,分别是context,aop,tx,context是开启IOC容器注解扫描,aop的配置是开启基于注解的AOP,tx的配置是开启基于注解的事务管理控制模式。顺带提一句,这仅仅是因为两个例子写在了一起,所以在POM和application里感觉引入了很多。大家在自己写的时候,pom里的内容可以一个一个引进去,这样就更能看出来哪个dependency对应了哪些jar包。
下面给出相应的代码,首先是三个bean
package com.phl.trans.bean;
import org.springframework.stereotype.Component;
@Component
public class Account {
private String userName;
private Integer userId;
private Double balance;
@Override
public String toString() {
return "Account{" +
"userName='" + userName + '\'' +
", userId=" + userId +
", balance=" + balance +
'}';
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
public Account() {
}
public Account(String userName, Integer userId, Double balance) {
this.userName = userName;
this.userId = userId;
this.balance = balance;
}
}
package com.phl.trans.bean;
import org.springframework.stereotype.Component;
@Component
public class Book {
private String bookName;
private Double price;
private Integer bookId;
@Override
public String toString() {
return "Book{" +
"bookName='" + bookName + '\'' +
", price=" + price +
", bookId=" + bookId +
'}';
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getBookId() {
return bookId;
}
public void setBookId(Integer bookId) {
this.bookId = bookId;
}
public Book(String bookName, Double price, Integer bookId) {
this.bookName = bookName;
this.price = price;
this.bookId = bookId;
}
public Book() {
}
}
package com.phl.trans.bean;
import org.springframework.stereotype.Component;
@Component
public class Stock {
private Integer bookStock;
private Integer bookId;
@Override
public String toString() {
return "Stock{" +
"bookStock=" + bookStock +
", bookId=" + bookId +
'}';
}
public Integer getBookStock() {
return bookStock;
}
public void setBookStock(Integer bookStock) {
this.bookStock = bookStock;
}
public Integer getBookId() {
return bookId;
}
public void setBookId(Integer bookId) {
this.bookId = bookId;
}
public Stock() {
}
public Stock(Integer bookStock, Integer bookId) {
this.bookStock = bookStock;
this.bookId = bookId;
}
}
然后给出三个dao
package com.phl.trans.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class AccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public int decreaseBalanceByUserId(Double price,Integer userId){
String sql = "update user set balance = balance-? where user_id = ?";
int update = jdbcTemplate.update(sql, price, userId);
return update;
}
}
package com.phl.trans.dao;
import com.phl.trans.bean.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public Book searchBookInfoByBookId(Integer id){
String sql = "select * from book where book_id = ?";
Book book = this.jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);
return book;
}
}
package com.phl.trans.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class StockDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public int decreaseStockByBookId(Integer bookId){
String sql = "update stock set book_stock = book_stock-1 where book_id = ?";
int update = jdbcTemplate.update(sql, bookId);
return update;
}
}
然后是负责买书的service,这个service里只写了一个方法,并且在这个方法上加上了@Transactional注解
package com.phl.trans.service;
import com.phl.trans.bean.Book;
import com.phl.trans.dao.AccountDao;
import com.phl.trans.dao.BookDao;
import com.phl.trans.dao.StockDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BuyBookService {
@Autowired
private AccountDao accountDao;
@Autowired
private BookDao bookDao;
@Autowired
private StockDao stockDao;
private Integer bookId = 1;
private Integer userId = 1;
@Transactional
public void BuyBook(){
Book book = bookDao.searchBookInfoByBookId(bookId);
accountDao.decreaseBalanceByUserId(book.getPrice(), userId);
stockDao.decreaseStockByBookId(bookId);
}
}
最后是对应的测试类
package com.phl.noioc.test;
import com.phl.trans.service.BuyBookService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.sql.SQLException;
public class Test {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
@org.junit.Test
public void test01() throws SQLException {
BuyBookService service = (BuyBookService) context.getBean("buyBookService");
service.BuyBook();
}
}
在上面的例子中,我故意把库存表(book_stock)的表名在stockDao中写成了(book),这样会在运行的时候出错。
然后,如果service中的方法加上了Transactional注解,那么第一个成功的语句(更改账户余额)也会被回滚。如果没有加上注解,则第一个语句还是成功。
@Transactional详解
在@Transactional注解中有如下属性可以配置:
- isolation:设置事务的隔离级别
- noRollbackFor:那些异常事务可以不回滚
- noRollbackForClassName:填写的参数是全类名
- rollbackFor:哪些异常事务需要回滚
- rollbackForClassName:填写的参数是全类名
- readOnly:设置事务是否为只读事务
- timeout:事务超出指定执行时长后自动终止并回滚,单位是秒
- propagation:事务的传播特性
上面给出了各个属性的解释,下面逐个进行讲解。
- isolation:隔离级别。这里说的隔离级别就是数据库的隔离级别,这个属性的默认值是当前数据库的隔离级别,其他可以配置的信息包括读未提交,读已提交,可重复读,序列化,共计四种
- noRollbackFor:哪些异常不回滚,如,noRollbackFor = {ArithmeticException.class,NullPointerException.class}
- noRollbackForClassName:哪些异常不回滚,如,noRollbackForClassName = {“java.lang.ArithmeticException”}
- rollbackFor:哪些异常事务需要回滚,如,rollbackFor = {FileNotFoundException.class}
- rollbackForClassName:哪些异常事务需要回滚,如,rollbackForClassName = {“java.io.FileNotFoundException”}
- readOnly:如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。对于只读查询,可以指定事务类型为readonly,即只读事务。由于只读事务不存在数据的修改,因此数据库将会为只读事务提供一些优化手段
最后就是propagation,事务的传播特性,也叫事务的传播行为。
注意,对于Transactional 声明式事务管理,如果方法内部catch,是不会进行回滚的。我的例子中,同样没有进行try catch。但是这种情况在实际使用中却不行,因为会引起程序终止。
要想在catch之后还回滚,需要在catch里面throw 一个新的异常,并且rollBackFor里面添加好对应的异常信息,通过rollBackFor或者noRollBackFor来控制。
再注意,假设在类A中有多个方法,如方法A1,方法A2,方法A3.给这3个方法上,都加上事务管理的注解。在类B中有调用方法,方法B1,在方法B1中去调用A类中的单个或者多个方法,此时,才会应用到传播特性。即,只有这种不同类之间的调用,才会出现事务的传播特性。
如果是在A类中,现在出现了方法A4,在A4中去调用A1,A2,A3。此时,不论1,2,3上面的传播特性定义的是什么,都将认为是最普通的当前类内的调用,这种调用关系下,就是统一用同一个事务,不存在传播特性。
这个是因为在当前类相互调用的时候,这中调用关系,最终不会通过AOP去进行织入。因此不牵扯传播特性。
传播特性名称 | 意义 | 详细运行结果说明 |
---|---|---|
Propagation.MANDATORY | 当前方法必须在一个事务中进行运行,如果当前没有事务正在发生,将抛出一个异常。 | |
Propagation.NEVER | 当前方法不应该在一个事务中运行。如果当前有事务正在进行,则会抛出一个异常。 | |
Propagation.NOT_SUPPORTED | 当前方法不应该在一个事务中运行。如果当前有事务正在进行,则会在事务执行期间挂起等事务结束后,再执行 | |
Propagation.SUPPORTS | 当前方法不必要挂载在事务中运行。但如果已经有一个运行的事务,则这个方法也可以在当前事务里进行运行。 | |
Propagation.REQUIRED | 当前方法必须在一个事务中运行。如果当前有正在进行的事务,这个方法就会在当前的事务中进行运行。如果当前没有事务,就会开始一个新的事务,确保这个方法的执行。 | 如果类A中的被调用方法上传播特性都是REQUIRED,那整体过程会在同一个事务中。一旦发生异常,整个都会回滚 |
Propagation.REQUIRES_NEW | 当前方法必须在事务中进行运行。并且每次调用时,一个新的事务都会被启动,这个方法会在新的事务中进行运行。如果当前已经有正在运行的事务,那么正在运行的事务,会在这个新事务运行期间被挂起。等新的事务运行完成之后,再继续执行。 | 对于REQUIRES_NEW,不论整个事务处理过程中是否出现异常,只要自己的事务正常,就会提交。同样,只要自己的事务异常,就会回滚。 |
Propagation.NESTED | NESTED,是一个“嵌套”的事务。他是已经存在事务的一个真正的子事务。当嵌套事务开始执行时,他会得到一个savepoint。如果这个嵌套事务失败,我们将回滚到这个savepoint。 | 与REQUIRES_NEW最大的区别就在于。虽然自己的事务执行成功,但是外层主事务要是出现异常,那么他也会回滚。 |
下面用一个场景去详细的描述上面说到的Propagation.REQUIRED,Propagation.REQUIRES_NEW,Propagation.NESTED这三个详细分析的传播特性。
假设在方法A中调用方法B
异常状态\传播特性 | Propagation.REQUIRES_NEW(两个独立事务) | Propagation.NESTED(B嵌套在A中) | Propagation.REQUIRED(同一个事务) |
---|---|---|---|
方法A异常,方法B正常 | 方法A回滚,方法B提交 | A和B一同回滚 | A和B一同回滚 |
方法A正常,方法B异常 | 如果A中捕获了B的异常,并没有继续向上抛出,则B先回滚,A在正常提交。如果A捕获了B的异常,并且继续向上抛出,则B先回滚,A再回滚 | B先回滚,A再正常提交 | A,B一同回滚 |
方法A异常,方法B异常 | B先回滚,A再回滚 | A和B一同回滚 | A和B一同回滚 |
方法A正常,方法B正常 | B先提交,A再提交 | A和B一同提交 | A和B一同提交 |