1.AOP简介
代码参考Spring_17_aop_demo
什么是AOP?
AOP(Aspect Oriented Programming)
即面向切面编程,一种编程范式,指导开发者如何组织程序结构
AOP作用
在
BookDaoImpl.java
中,执行save
方法显然可以计算程序执行时间,但实际上运行update
和delete
方法也可以完成相同功能,这是为什么?通过AOP
实现,即在不惊动原始设计的基础上为其进行功能增强
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
// 记录程序当前执行执行(开始时间)
Long startTime = System.currentTimeMillis();
// 业务执行万次
for (int i = 0;i<10000;i++) {
System.out.println("book dao save ...");
}
// 记录程序当前执行时间(结束时间)
Long endTime = System.currentTimeMillis();
// 计算时间差
Long totalTime = endTime-startTime;
// 输出信息
System.out.println("执行万次消耗时间:" + totalTime + "ms");
}
public void update(){
System.out.println("book dao update ...");
}
public void delete(){
System.out.println("book dao delete ...");
}
public void select(){
System.out.println("book dao select ...");
}
}
AOP核心概念
其实在上述方法中
select
方法运行后并不会实现相同功能,为什么会被区别对待?这就需要先了解AOP
的一些核心概念
- 连接点:类中可以被增强的方法(广义上是程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等),像
select
、update
和delete
都属于连接点(切入点一定是连接点,但连接点不一定要被增强,所以可能不是切入点) - 切入点:类中实际需要被增强的方法(一个或多个),可以通过匹配符去匹配:
- 一个具体的方法:如
com.psj.dao
包下的BookDao
接口中的无形参无返回值的save
方法 - 匹配多个方法:所有的get开头的方法等
- 一个具体的方法:如
- 通知:存放共性功能的方法,即要增强的内容,比如
MyAdvice
类中的method
方法就是通知 - 切面:通知和切入点都可以有多个,哪个切入点需要添加哪个通知,需要切面进行它们之间的关系描述
- 通知类:通知是方法,用于定义该方法的类就是通知类
- 目标对象:原始功能除去共性功能后产生的对象,它无法完成最终工作
- 代理:目标对象要完成工作,则需要通过原始对象的代理对象实现
2.AOP入门案例
如何在方法执行前输出当前系统时间?
代码参考Spring_18_aop_quickstart
导入坐标
- 因为
spring-context
中已经导入了spring-aop
,所以无需再单独导入spring-aop
AspectJ
是AOP
思想的一个具体实现,Spring
有自己的AOP
实现,但比AspectJ
麻烦
制作连接点
即代码中的
BookDaoImpl.java
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
public void update(){
System.out.println("book dao update ...");
}
}
制作共性功能
即写通知类与通知
public class MyAdvice {
public void method(){ // 定义共性功能
System.out.println(System.currentTimeMillis());
}
}
定义切入点
假设只想增强
BookDaoImpl
类下的update
方法,需要定义切入点,而切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑
public class MyAdvice {
@Pointcut("execution(void com.psj.dao.BookDao.update())") // 定义在哪执行
private void pt(){} // 不具有实际意义
public void method(){
System.out.println(System.currentTimeMillis());
}
}
制作切面
即绑定切入点与通知之间的关系
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
// @Before即表示通知会在切入点方法执行之前执行
@Before("pt()") // 绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置
public void method(){
System.out.println(System.currentTimeMillis());
}
}
将通知类配给容器并标识其为切面类
@Component // 给Spring容器管理
@Aspect // 表示为切面类
public class MyAdvice {
@Pointcut("execution(void com.psj.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
开启注解格式AOP功能
@Configuration
@ComponentScan("com.psj")
@EnableAspectJAutoProxy // 告诉Spring有用注解开发的AOP
public class SpringConfig {
}
3.AOP工作流程
Spring
容器启动:此时bean
对象未被创建- 读取所有切面配置中的切入点:并不是读取所有切入点,因为有些定义了但是未使用
public class MyAdvice {
@Pointcut("execution(void com.psj.dao.BookDao.update())")
private void pt(){}
@Pointcut("execution(void com.psj.dao.BookDao.select())")
private void pt1(){} // 该切入点未被使用,所以不会被读取
@Before("pt()") // 只使用了pt
public void method(){
System.out.println(System.currentTimeMillis());
}
}
-
初始化
bean
:即判定bean
对应的类中的方法是否匹配到任意切入点-
匹配失败:说明不需要增强,直接调用原始对象的方法即可,比如
UserDao
中的方法就无法匹配到pt
切入点,容器中的对象是目标对象本身 -
匹配成功:创建原始对象(目标对象)的代理对象,容器中的对象是目标对象的代理对象(要验证是否为代理对象,可以打印当前对象的
getClass
方法)
-
4.AOP配置管理
AOP切入点表达式
切入点是要进行增强的方法,而切入点表达式是要进行增强的方法的描述方式
代码参考Spring_19_aop_pointcut
- 语法格式:
动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
// 两种都是可以的,因为调用接口方法时最终运行的还是其实现类的方法
execution(void com.psj.dao.BookDao.update())
execution(void com.psj.dao.impl.BookDaoImpl.update())
-
通配符:
*
:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
// 匹配com.psj包下的任意包中的UserService类/接口中所有find开头的带有一个参数的方法 execution(public * com.psj.*.UserService.find*(*))
..
:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
// 匹配com包下的任意包中的UserService类/接口中所有名称为findById的方法 execution(public User com..UserService.findById(..))
+
:专用于匹配子类类型,很少使用
// *Service+表示所有以Service结尾的接口的子类。 execution(* *..*Service+.*(..))
-
书写技巧:
- 描述切入点通常描述接口(如果描述到实现类会出现紧耦合)
- 访问控制修饰符针对接口开发均采用
public
描述(可省略) - 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用
*
通配快速描述 - 包名书写尽量不使用
..
匹配(效率过低),常用*
- 接口名/类名书写名称与模块相关的采用
*
匹配,例如UserService
书写成*Service
- 方法名书写以动词进行精准匹配,名词采用匹配,例如
getById
书写成getBy*
- 通常不使用异常作为匹配规则
AOP通知类型
通知描述了抽取的共性功能,根据共性功能抽取的位置不同,运行代码时要将其加入到合理的位置
代码参考Spring_20_aop_advice_type
-
前置通知(
before
):追加功能到方法执行前 -
后置通知(
after
):追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行 -
返回后通知(
afterReturning
):追加功能到方法执行后,只有方法正常执行结束后才进行(如果方法执行抛出异常,返回后通知将不会被添加) -
抛出异常后通知(
afterThrowing
):追加功能到方法抛出异常后,只有方法执行出异常才进行 -
环绕通知(
Around
):可以追加功能到方法执行的前后(可以不使用ProceedingJoinPoint
调用原始方法),它可以实现其他四种通知类型的功能- 原始方法不具有返回值:通知方法的返回值类型可以设置成
void
,也可以设置成Object
@Around("pt()") public void around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("around before advice ..."); // 表示对原始操作的调用 pjp.proceed(); System.out.println("around after advice ..."); return ret; }
- 原始方法具有返回值:可以不接收返回值,通知方法设置成
void
即可,如果接收返回值,最好设定为Object
类型
@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; // 执行原始方法后需要将其返回值返回(当然可以自定义返回值) }
- 原始方法不具有返回值:通知方法的返回值类型可以设置成
小Demo-业务层接口执行效率
完成一个需求:任意业务层接口执行均可显示其执行效率(执行时长)
代码参考Spring_21_case_interface_run_speed
AOP通知获取数据
AOP是在原始方法前后追加操作,所以获取数据即获取原始方法中的数据
代码参考Spring_22_aop_advice_data
-
获取切入点参数:所有的通知类型都可以获取参数
JoinPoint
:适用于前置、后置、返回后、抛出异常后通知ProceedingJoinPoint
:适用于环绕通知
-
获取切入点方法返回值:前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
- 返回后通知
- 环绕通知
-
获取切入点方法运行异常信息:前置和返回后通知不会有,后置通知可有可无,所以不做研究
- 抛出异常后通知
- 环绕通知
@Pointcut("execution(* com.psj.dao.BookDao.findName(..))")
private void pt() {}
// JoinPoint:用于描述切入点的对象,必须配置成通知方法中的第一个参数,可用于获取原始方法调用的参数
@Before("pt()")
public void before(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before advice ...");
}
@After("pt()")
public void after(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("after advice ...");
}
// ProceedingJoinPoint:专用于环绕通知,是JoinPoint子类,可以实现对原始方法的调用
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666; // 可以先进行处理再传给切入点
Object ret = null;
try {
ret = pjp.proceed(args); // 调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数
} catch (Throwable t) {
t.printStackTrace();
}
return ret;
}
// 设置返回后通知获取原始方法的返回值,要求returning属性值必须与方法形参名相同
@AfterReturning(value = "pt()", returning = "ret")
public void afterReturning(JoinPoint jp, String ret) {
System.out.println("afterReturning advice ..." + ret);
}
// 设置抛出异常后通知获取原始方法运行时抛出的异常对象,要求throwing属性值必须与方法形参名相同
@AfterThrowing(value = "pt()", throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..." + t);
}
小Demo-百度网盘密码数据兼容处理
删除百度网盘分享链接输入密码时尾部多输入的空格,处理方式:
- 在业务方法执行之前对所有的输入参数进行格式处理(使用trim方法)
- 使用处理后的参数调用原始方法(环绕通知中存在对原始方法的调用)
代码参考Spring_23_case_handle_password
@Around("DataAdvice.servicePt()")
public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
// 对参数进行预处理
for (int i = 0; i < args.length; i++) {
// 判断参数是不是字符串
if(args[i].getClass().equals(String.class)){
args[i] = args[i].toString().trim();
}
}
// 传入预处理的参数
Object ret = pjp.proceed(args);
return ret;
}
5.AOP事务管理
Spring事务简介
- 事务作用:在数据层保障一系列的数据库操作同成功同失败
Spring
事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败(以转账为例,减钱和加钱分别调用一次数据层,意味着出现两个事务,这没法保证存钱和取钱同时成功或失败,所以需要把事务放在业务层处理)
转账案例
实现任意两个账户间转账操作:
- 数据层提供基础操作,指定账户减钱(
outMoney
),指定账户加钱(inMoney
)- 业务层提供转账操作(
transfer
),调用减钱与加钱的操作- 提供2个账号和操作金额执行转账操作
代码参考Spring_24_case_transfer
- 在业务接口上添加
Spring
事务管理:@Transactional
可以加在业务层接口上方、业务层实现类上方以及业务方法上方
public interface AccountService {
// 配置当前接口方法具有事务(该注解一般写在接口上)
@Transactional
public void transfer(String out, String in, Double money);
}
- 设置事务管理器:
public class JdbcConfig {
// 其余代码省略
// 配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
Spring事务角色
Spring
不是数据库,为什么可以进行事务管理?通过事务管理员和协调员
- 未开启事务:
AccountDao
接口中的outMoney
方法和inMoney
方法修改操作,各自会开启事务T1
和T2
,此时AccountService
类中的transfer
方法没有事务,所以如果其中一个事务出现异常就会导致数据错误- 开启事务:在
transfer
方法上添加了@Transactional
注解后会有事务T
,T1
和T2
会加入到T
中,这就保证它们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性目前的事务管理基于
DataSourceTransactionManager
和SqlSessionFactoryBean
使用的是同一个数据源
- 事务管理员:发起事务方,在
Spring
中通常指代业务层开启事务的方法(transfer
) - 事务协调员:加入事务方,在
Spring
中通常指代数据层方法(outMoney
和inMoney
),也可以是业务层方法
Spring事务属性
- 事务配置:下面属性都可在
@Transactional
注解的参数上进行设置(Spring
的事务只会对Error
异常和RuntimeException
异常及其子类进行事务回滚,其他的异常类型可不回滚,比如IOException
,所以可设置@Transactional(rollbackFor = {IOException.class})
)
- 转账业务追加日志案例(代码参考Spring_25_case_transfer_log):在前面的转案例的基础上添加新的需求,完成转账后记录日志
- 基于转账操作案例添加日志模块,实现数据库中记录日志
- 业务层转账操作去调用减钱、加钱与记录日志功能
- 无论转账操作是否成功,均进行转账操作的日志留痕
// 对应接口还是添加了@Transactional注解的
public void transfer(String out,String in ,Double money) {
try{
accountDao.outMoney(out,money);
int i = 1/0;
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money); // 保证代码运行
}
}
-
事务传播行为:即事务协调员对事务管理员所携带事务的处理态度(相当于
T3
选择加入到T
,还是log
方法自己成为新事务)对于
log
、inMoney
和outMoney
都属于增删改,分别有事务T1
、T2
和T3
。前面提到Spring
事务会把三个事务加入到事务T
(即transfer
开启的事务)中,当转账失败后,所有的事务都回滚,导致日志没有记录下来,能不能让log
方法单独是一个事务呢?需用到事务传播行为
public interface LogService {
// propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String out, String in, Double money);
}
参考
https://www.bilibili.com/video/BV1Fi4y1S7ix?p=31-42