1. AOP简介
2. AOP入门案例
- 定义接口BookDao和实现类BookDaoImpl
@Repository
public class BookDaoImpl implements BookDao {
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 ...");
}
}
- 定义切面类MyAdvice
package com.lan.aop
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect // 设置当前类为AOP切面类
public class MyAdvice {
}
- 在切面类中定义切入点,表示要切入的方法
package com.lan.aop
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
// 切入点表示要切入的方法,要切入的方法叫连接点
@Pointcut("execution(void com.lan.dao.BookDao.delete())")
private void pt(){} // 该方法名随意
}
- 在切面类中定义通知和切面,切面是用来描述通知和切入点之间的关系
package com.lan.aop
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
// 切入点表示要切入的方法,要切入的方法叫连接点
@Pointcut("execution(void com.lan.dao.BookDao.delete())")
private void pt(){} // 该方法名随意
@Before("pt()") // 定义切面,@Before表示通知在连接点执行前执行
public void before(){ // 定义通知
System.out.println(System.currentTimeMillis());
}
@After("pt()") // 定义切面,@After表示通知在连接点执行后执行
public void after() {
System.out.println("after execution");
}
@Around("pt()") // 定义切面,@Around表示连接点在通知的中间某一步执行
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("------------------------------");
Long startTime = System.currentTimeMillis();
for (int i = 0 ; i<10 ; i++) {
//调用原始操作
pjp.proceed();
}
Long endTime = System.currentTimeMillis();
Long totalTime = endTime-startTime;
System.out.println("执行10次消耗时间:" + totalTime + "ms");
return null;
}
}
- 开启AOP功能
package com.lan.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.lan")
@EnableAspectJAutoProxy // 开启AOP功能
public class SpringConfig {
}
- 运行程序
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
// 此处的参数必须是接口类,不能是实现类BookDaoImpl,因为JDK Proxy代理的只能是接口
// JDK Proxy可以参考 https://blog.youkuaiyun.com/werewolf2017/article/details/124950536
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.delete();
3. AOP注解讲解
注解 | 作用 |
@EnableAspectJAutoProxy | 开启注解格式AOP功能 |
@Aspect | 设置当前类为切面类 |
@Pointcut | 在切面类中设置切入点方法,value为切入点表达式 |
@Before | 设置前置通知方法 |
@Around | 设置环绕通知方法 |
@After | 设置后置通知方法 |
@AfterReturning | 设置返回后通知方法 |
@AfterThrowing | 设置抛出异常后通知方法 |
4. AOP实现原理
AOP实际上是通过代理模式实现的,匹配到切入点的bean,spring会为其生成代理对象,其class是Proxy;没匹配到切入点的bean,其class是对应接口的实现类。输出bean的class对象可以验证:
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
System.out.println(bookDao); // 无论是否匹配到切入点,对象都是BookDaoImpl
// 匹配到切入点的bean,其class是Proxy;没匹配到切入点的bean,则其class是BookDao对应的实现类
System.out.println(bookDao.getClass());
5. AOP切入点表达式
表达式格式:
动作关键字 ( 访问修饰符 返回值 包名 . 类 / 接口名 . 方法名 ( 参数 ) 异常名)
以这个为例子
execution(public User com.lan.service.UserService.findById(int))
execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
public:访问修饰符,还可以是public,private等,可以省略
User:返回值,写返回值类型
com.lan.service:包名,多级包使用点连接
UserService:类/接口名称
findById:方法名
int:参数,直接写参数的类型,多个类型用逗号隔开
异常名:方法定义中抛出指定异常,可以省略
5.1 通配符
可以使用通配符描述切入点,简化切入点描述
- * :单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution (public * com.lan.*.UserService.find*(*))
- ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
execution (public User com..UserService.findById(..))
-
+ :专用于匹配子类类型
execution(* *..*Service+.*(..))
6. 通知中获取参数
@Before("pt()")
public void before(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before advice ..." );
}
@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);
} 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);
}
7. Spring事务管理
7.1 开启注解式事务驱动
使用@EnableTransactionManagement注解开启注解式事务驱动
@Configuration
@ComponentScan("com.lan")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
7.2 创建平台事务管理器PlatformTransactionManager
//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
7.3 使用@Transactional注解开启事务
在AccountService接口类加@Transactional注解表示该方法内的jdbc操作处于同一个事务中,注解也可以在实现类上加
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
@Transactional
public void transfer(String out,String in ,Double money) ;
}
7.3.1 @Transactional的属性
- readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true。
- timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超 时时间。
- rollbackFor:当出现指定异常进行事务回滚。需要注意的是,默认不配置rollbackFor的情况下,只会对Error和RuntimeException类型(包括其子类)的异常进行回滚,其它类型的异常不会回滚
- noRollbackFor:当出现指定异常不进行事务回滚
- rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串
- noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串
-
isolation 设置事务的隔离级别
7.3.2 事务的传播
场景:在转账操作中,不管成功或失败,都记录操作日志到数据库中。代码如下
@Transactional(rollbackFor=Exception.class)
public void transfer(String out,String in ,Double money) {
try {
// 转账的数据库操作
} finally {
// 记录日志的数据库操作
log();
}
}
public void log() {
}
以上代码中,转账发生异常时失败时,记录日志的数据库操作也会被回滚,若将日志记录的操作放在一个新的事务中,则可以避免,这就涉及到事务的传播。修改后的代码如下
@Transactional(rollbackFor=Exception.class)
public void transfer(String out,String in ,Double money) {
try {
// 转账的数据库操作
} finally {
// 记录日志的数据库操作
log();
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW) // 表示需要一个新的事务
public void log() {
}
@Transaction的propagation属性可以指定事务的传播方式,事务的传播方式有六种,分别是REQUIRED(默认)、REQUIRED_NEW、SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER、NESTED
以上面的transfer调用log为例,讲解两个方法的事务传播的影响:
log的传播属性 | transfer事务 | log事务结果 |
REQUIRED | 开启事务T | 加入事务T |
无事务 | 新建事务 | |
REQUIRED_NEW | 开启事务T | 新建事务 |
无事务 | 新建事务 | |
SUPPORTS | 开启事务T | 加入事务T |
无事务 | 无事务 | |
NOT_SUPPORTED | 开启事务T | 无事务 |
无事务 | 无事务 | |
MANDATORY | 开启事务T | 加入事务T |
无事务 | ERROR | |
NEVER | 开启事务T | ERROR |
无事务 | 无事务 | |
NESTED | 设置savePoint,一旦事务回滚,事务将回滚到savePoint处,交由客户响应提交或回滚 |