AOP简介和作用
- AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构,而 OOP(Object Oriented Programming) 是面向对象编程,AOP是对OOP的一种补充
- 作用:简单的说就是在不改变方法源代码的基础上对方法进行功能增强
AOP的一些概念
- 连接点(JoinPoint):正在执行的方法,例如:update()、delete()、select()等都是连接点
- 切入点(Pointcut):进行功能增强了的方法,例如:update()、delete()方法,select()方法没有被增强所以不是切入点,但是是连接点
- 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
- 通知(Advice):在切入点前后执行的操作,也就是增强的共性功能
- 在SpringAOP中,功能最终以方法的形式呈现
- 通知类:通知方法所在的类叫做通知类
- 切面(Aspect):描述通知与切入点的对应关系,也就是哪些通知方法对应哪些切入点方法
SpringAOP快速入门
- 导入依赖
<dependencies>
<!--spring-context-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!--aspectjweaver(spring面向切面开发必须的第三方组件)-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<!--junit,spring对junit4的要求必须是4.12版本以上-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<!--spring-test-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
- 定义Dao接口与实现类
//Dao层接口类
public interface BookDao {
void save();
void update();
void delete();
}
//Dao层实现类
@Repository
public class BookDaoImpl implements BookDao {
@Override
public void save() {
System.out.println("book dao save ...");
}
@Override
public void update() {
System.out.println("book dao update ...");
}
@Override
public void delete() {
System.out.println("book dao delete ...");
}
}
- 定义通知类,编写通知方法
/**
* 通知类
* 注意:需要将切面类加入Spring-IOC容器
*/
@Component //需要将切面类加入Spring-IOC容器
@Aspect //指定为切面类,Spring框架就会对这个类进行AOP操作
public class MyAdvice {
/**
* 定义切入点表达式,指定对 BookDaoImpl 的 save()、update()方法进行增强
*/
//最完整的写法,必须3个参数必写:返回值、方法名、参数
@Pointcut("execution(void com.itheima.dao.BookDao.save()) || execution(void com.itheima.dao.BookDao.update())")
public void pt() {
}
/**
* 通知方法
* <p>
* 注解@Before,前置通知,会在业务方法前,调用该增强方法
* 属性 @Before("pt()"),绑定切入点,目的是让增强方法和业务方法绑定关系
*/
@Before(value = "pt()")
public void before() {
System.out.println("写入日志");
}
}
- 在Spring配置类中开启AOP注解扫描
@Configuration
@ComponentScan("com.itheima")
//开启AOP注解扫描(开启后,才会扫描AOP注解,才能使用AOP面向切面功能)
@EnableAspectJAutoProxy
public class SpringConfig {
}
- 编写测试类,测试AOP
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class BookDaoTest {
/**
* 注入BookDao,这个对象是代理对象,AOP底层就是动态代理,调用方法时,先调用增强的代码,再调用我们的业务代码
*/
@Autowired
private BookDao bookDao;
@Test
public void test() {
System.out.println("运行 =======> 需要增强的方法");
bookDao.save();
bookDao.update();
System.out.println("运行 =======> 执行没有增强的方法");
bookDao.delete();
}
}
AOP工作流程
- Spring容器启动
- 读取所有切面配置中的切入点
- 初始化bean,判断bean对应的类中的方法是否匹配到任意切入点
- 匹配失败,创建原始对象
- 匹配成功,创建原始对象(目标对象)的代理对象
- 获取bean执行方法
- 获取的bean是原始对象时,调用方法并执行,完成操作
- 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
AOP核心概念
- 目标对象(Target):被代理的对象,也叫原始对象,该对象中的方法没有任何功能增强
- 代理对象(Proxy):代理后生成的对象,由Spring帮我们创建代理对象
AOP切入点表达式
语法格式
- 切入点:要进行增强的方法
- 切入点表达式:要进行增强的方法的描述方式
- 描述方式一:执行com.itheima.dao包下的BookDao接口中的无参数update方法
execution(void com.itheima.dao.BookDao.update())
- 描述方式二:执行com.itheima.dao.impl包下的BookDaoImpl类中的无参数update方法
execution(void com.itheima.dao.impl.BookDaoImpl.update())
- 切入点表达式标准格式:切点函数(访问修饰符 返回类型 类全名.方法名(参数类型) 异常类型)
execution(public User com.itheima.service.UserService.findById(int))
- 切点函数:描述切入点的行为动作,例如execution表示执行到指定切入点
- 访问修饰符:public,private等,可以省略
- 返回值类型
- 包名:多级包使用点连接
- 类全名:包名+类名/接口名
- 参数:直接写参数的类型,多个类型用逗号隔开
- 异常名:方法定义中抛出指定异常,可以省略
通配符
- 目的:可以使用通配符描述切入点,快速描述
通配符:*
-
*
单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
execution(public * com.itheima.*.UserService.find*(*))
通配符 ..
-
..
多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
execution(public User com..UserService.findById(..))
通配符 +
-
+
专用于匹配子类类型
execution(* *..*Service+.*(..))
书写技巧
- 所有代码按照标准规范开发,否则以下技巧全部失效
- 描述切入点通常描述接口,而不描述实现类
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
- 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
- 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
-
方法名书写以动词进行精准匹配,名词采用
*
匹配,例如getById书写成getBy*
,selectAllBook书写成selectAll*
- 参数规则较为复杂,根据业务方法灵活调整
- 通常不使用异常作为匹配规则
AOP通知类型
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
AOP通知共分为5种类型
- 前置通知:在切入点方法执行之前执行
- 最终通知:在切入点方法执行之后执行,无论切入点方法内部是否出现异常,最终通知都会执行
- 环绕通知:手动调用切入点方法并对其进行增强的通知方式。
- 后置通知:在切入点方法执行之后执行,如果切入点方法内部出现异常将不会执行
- 异常通知:在切入点方法执行之后执行,只有当切入点方法内部出现异常之后才执行
AOP通知详解
前置通知
- 名称:@Before
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
最终通知
- 名称:@After
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
@After("pt()")
public void after() {
System.out.println("after advice ...");
}
后置通知
- 名称:@AfterReturning
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行
@AfterReturning("pt()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
异常通知
- 名称:@AfterThrowing
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行
@AfterThrowing("pt()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
环绕通知
- 名称:@Around(重点,常用)
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
环绕通知注意事项
- 环绕通知方法形参必须是ProceedingJoinPoint,表示正在执行的连接点,使用该对象的proceed()方法表示对原始对象方法进行调用,返回值为原始对象方法的返回值。
- 环绕通知方法的返回值建议写成Object类型,用于将原始对象方法的返回值进行返回,哪里使用代理对象就返回到哪里。
AOP切入点数据获取
获取参数
- 在
前置通知
、环绕通知
中都可以获取到连接点方法的参数 - JoinPoint对象描述了连接点方法的运行状态,可以获取到原始方法的调用参数
- ProccedJointPoint是JoinPoint的子类
前置通知中,获取方法参数
@Before("pt()")
public void before(JoinPoint jp) {
Object[] args = jp.getArgs(); //获取连接点方法的参数们
System.out.println(Arrays.toString(args));
}
环绕通知中,获取方法参数
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs(); //获取连接点方法的参数们
System.out.println(Arrays.toString(args));
Object ret = pjp.proceed();
return ret;
}
获取返回值
- 在
后置通知
和环绕通知
中都可以获取到连接点方法的返回值 -
后置通知
可以获取切入点方法的返回值,使用形参接收返回值
异常通知中,获取返回值
@AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(Object ret) { //变量名要和returning="ret"的属性值一致
System.out.println("afterReturning advice ..."+ret);
}
环绕通知中,获取返回值
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 手动调用连接点方法,返回值就是连接点方法的返回值
Object ret = pjp.proceed();
return ret;
}
获取异常信息
- 在
异常通知
和环绕通知
中都可以获取到连接点方法中出现的异常
异常通知可以获取切入点方法中出现的异常信息,使用形参可以接收对应的异常对象
@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {//变量名要和throwing = "t"的属性值一致
System.out.println("afterThrowing advice ..."+ t);
}
异常通知可以获取切入点方法运行的异常信息,使用形参可以接收运行时抛出的异常对象
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Object ret = null;
//此处需要try...catch处理,catch中捕获到的异常就是连接点方法中抛出的异常
try {
ret = pjp.proceed();
} catch (Throwable t) {
t.printStackTrace();
}
return ret;
}
Spring事务管理
- Spring事务管理器接口
Spring事务作用
- 事务作用:在
数据层
保障一系列的数据库操作同成功同失败 - Spring事务作用:在
数据层
或业务层
保障一系列的数据库操作同成功或同失败
转账案例
- 需求:实现任意两个账户间转账操作(A账户减钱,B账户加钱)
环境准备
- 添加依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!-- Spring-JDBC依赖包,事务功能也在这里 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
- 创建表、并插入数据
-- 创建数据表,账户表
CREATE TABLE tbl_account (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(10),
money DOUBLE -- 金额
);
-- 添加数据
INSERT INTO tbl_account (NAME, money) VALUES ('Jack', 1000), ('Rose', 1000);
SELECT * FROM tbl_account;
-- 还原金额
update tbl_account set money=1000;
-
resources
目录下创建jdbc.properties
配置文件,配置JDBC相关信息
jdbc.username=root
jdbc.password=root
jdbc.url=jdbc:mysql://localhost:3306/springdb2?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
jdbc.driverClassName=com.mysql.cj.jdbc.Driver
- 配置Spring,并且开启事务注解扫描(重点)
//Spring配置类
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})
//注解 @EnableTransactionManagement 开启事务注解扫描(当前包名下都扫描,所以不需要配置包名)
@EnableTransactionManagement
public class SpringConfig {
}
- 配置JDBC以及Druid连接池
//Druid连接池配置
public class JdbcConfig {
@Value("${jdbc.driverClassName}")
private String driverClassName;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driverClassName);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
/**
* 配置spring提供的事务切面类:里面增强了事务管理功能,里面有事务提交和事务回滚功能
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager ptm = new DataSourceTransactionManager();
ptm.setDataSource(dataSource);
return ptm;
}
}
- 配置MyBatis
//MyBatis配置类
public class MybatisConfig {
@Bean //不仅可以将返回值加入IOC容器,而且可以实现方法参数进行依赖注入,参数默认会根据类型从IOC容器中获取对象自动注入
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("com.itheima.domain"); //告诉mybatis,设置实体类包别名
ssfb.setDataSource(dataSource); //将IOC容器中连接池给到Mybatis
//注意:必须导入org.apache.ibatis.session.Configuration;
Configuration configuration = new Configuration();
//设置整合mybatis驼峰命名映射
configuration.setMapUnderscoreToCamelCase(true);
//设置打印日志
configuration.setLogImpl(StdOutImpl.class);
ssfb.setConfiguration(configuration);
return ssfb;
}
//设置事务管理器,并将事务管理器添加到IOC容器中
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
//告诉mybatis扫描指定的接口映射包
mapperScannerConfigurer.setBasePackage("com.itheima.dao");
return mapperScannerConfigurer;
}
}
编写代码
- 编写数据持久层(Dao层)
public interface AccountDao {
/**
* 转入
*
* @param name 账号名
* @param money 金额
*/
@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money);
/**
* 转出
*
* @param name 账号名
* @param money 金额
*/
@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);
}
- 创建业务层接口以及实现,在接口的
transfer
转账方法添加@Transactional注解,让类方法加入Spring事务
//业务层接口
//(推荐这种)写到类头,那么所有实现的所有方法,都有了事务
@Transactional
public interface AccountService {
/**
* 转账
*
* @param out 转出给谁
* @param in 转入到谁
* @param money 金额
*/
//在方法上,添加 @Transactional 注解,那么所有的实现类的这个方法都有了事务
//@Transactional
void transfer(String out, String in, Double money) throws IOException;
}
//业务层实现类
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void transfer(String out, String in, Double money) throws FileNotFoundException {
accountDao.outMoney(out, money);
//如果没有事务,这个异常会导致结束程序,导致下面的转入没有执行
//int a = 1 / 0;
accountDao.inMoney(in, money);
}
}
注意事项
- Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
- 注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务
Spring事务角色
- 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
- 事务协调员:加入事务方,在Spring中通常指数据层方法,也可以是业务层方法
Spring事务相关配置
- 对于RuntimeException类型异常或者Error错误,Spring事务能够进行回滚操作。但是对于非运行时异常,Spring事务是不进行回滚的,所以需要使用
rollbackFor
属性来设置要回滚的异常
Spring事务的传播行为
- Spring事务的传播行为说的是,当多个事务同时存在的时候,Spring如何处理这些事务的行为
传播行为的种类
- require必须的
- support支持
- mandatory 强制托管
- requires-new 需要新建
- not -supported不支持
- never从不
- nested嵌套的
传播行为种类解释
PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
Spring的事务传播行为案例
- 转账业务追加日志
- 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行记录(A账户减钱,B账户加钱,数据库记录日志)
默认事务传播行为的问题
- 问题:日志的记录与转账操作隶属同一个事务,同成功同失败,失败时,日志记录也进行了回滚
- 目标:无论转账操作是否成功,日志必须保留
- 解决方案:日志记录的事务传播行为设置为
REQUIRES_NEW
,即就是调用方的方法已有事务,也创建一个独立的事务,即使调用方失败进行回滚,也不会将日志记录的回滚
环境准备
- 创建日志表
CREATE TABLE tbl_log(
id INT PRIMARY KEY AUTO_INCREMENT,
info VARCHAR(255),
create_date DATETIME
);
编写代码
- 创建日志持久层接口(Dao层)
/**
* 转账日志记录-持久层
*/
public interface LogDao {
/**
* 增加转账日志
*
* @param info 日志信息
*/
@Insert("insert into tbl_log (info,create_date) values(#{info},now())")
void log(String info);
}
- 创建日志业务层接口,并设置日志事务的事务传播配置
/**
* 转账日志业务层接口
*/
public interface LogService {
/**
* 日志方法,无论调用当前方法是否是否有事务,当前方法都创建一个新的事务
* <p>
* Propagation.REQUIRES_NEW,就是无论调用方是否有事务,都开启一个新的事务
* 如果不设置传播行为,那么就会加入到调用方的事务,调用方出异常回滚了,就会将当前log记录也一起回滚!
* 注解@Transactional(propagation = Propagation.REQUIRES_NEW)
*
* @param out 谁转出转出
* @param in 转出给谁
* @param money 金额
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String out, String in, Double money);
}
- 创建日志业务层实现类
/**
* 转账日志记录-业务层实现类
*/
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
public void log(String out, String in, Double money) {
logDao.log("转账操作由" + out + "到" + in + ",金额:" + money);
}
}
- 在转账业务层,增加插入转账日志的调用
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
public void transfer(String out, String in, Double money) throws FileNotFoundException {
try {
accountDao.outMoney(out, money);
//如果没有事务,这个异常会导致结束程序,导致下面的转入没有执行
//int a = 1 / 0;
accountDao.inMoney(in, money);
} finally {
//调用日志业务,记录日志(调用了另外一个业务方法,要求是一个独立的事务,就是不加入当前事务)
logService.log(out, in, money);
}
}
}
- 编写测试类,进行测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void testTransfer() throws IOException {
accountService.transfer("Jack", "Rose", 500d);
}
}