Spring AOP配置管理
1 AOP切入点表达式
(1) 语法格式
//切入点:要进行增强的方法了。
//切入点表达式:要进行增强的方法的描述方式。
<1> 语法格式
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
//动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点。
//访问修饰符,还可以是public,private等,可以省略。
//异常名:方法定义中抛出指定异常,可以省略。
<3> 方式一:执行com.itheima.dao包下的BookDao接口中的无参数save方法execution(void com.itheima.dao.BookDao.save())
<3> 方式二:执行com.itheima.dao.impl包下的BookDaoImpl类中的无参数save方法execution(void com.itheima.dao.impl.BookDaoImpl.save())
//因为调用接口方法的时候最终运行的还是其实现类的方法,所以上面两种描述方式都是可以的。
(2) 通配符
<1> *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现。
//独立出现时代表任意某一个,但是不代表零个。
<2> ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写。
//表示任意多个,包括零个。
<3> +:专用于匹配子类类型。
//使用率较低。
<4> 使用
execution(* com.itheima.*.*Service.find*(..))
//将项目中所有业务层方法的以find开头的方法匹配
execution(* com.itheima.*.*Service.save*(..))
//将项目中所有业务层方法的以save开头的方法匹配
(3) 书写技巧
所有代码按照标准规范开发,否则以下技巧全部失效。
描述切入点*通常描述接口*,而不描述实现类,如果描述到实现类,就出现紧耦合了。
访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)。
返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述。
包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配。
接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名。
方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll。
参数规则较为复杂,根据业务方法灵活调整。
通常不使用异常作为匹配规则。
2 AOP通知类型
//只有环绕通知必须要返回值与连接点表达式中的返回值一致,其他通知可以与表达式中返回值不一致。
(1) 前置通知
<1> 基本使用
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.save())")
private void pt(){}
@Before("pt()")
//此处也可以写成 @Before("MyAdvice.pt()"),不建议。
public void before() {
System.out.println("before_advice");
}
}
<2> 运行结果
before_advice
BookDao_save
(2) 后置通知
<1> 基本使用
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.save())")
private void pt(){}
@Before("pt()")
public void before() {
System.out.println("before_advice");
}
@After("pt()")
public void after() {
System.out.println("after_advice");
}
}
<2> 运行结果
before_advice
BookDao_save
after_advice
(3) 环绕通知
//环绕通知类型不仅可以强制修改原始方法的参数,还可以强制修改方法返回值。
//当切入点表达式中返回值为*时,可以不接受返回值,但返回值应是一个可以接收null的数据类型。
<1> 基本使用
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.save())")
private void pt(){}
@Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("around_before_advice");
//表示对原始操作的调用
pjp.proceed();
System.out.println("around_after_advice");
}
}
<2> 运行结果
around_before_advice
//around和before同时出现时,around先执行。
BookDao_save
around_after_advice
//around和after同时出现时,around后执行。
<3> 注意事项
若原始方法save()有返回值,上述运行后会报错,错误内容为:空的返回不匹配原始方法的int返回。
//void就是返回Null,原始方法就是BookDao下的save()方法。
所以如果我们使用环绕通知的话,要根据原始方法的返回值来设置环绕通知的返回值,具体解决方案为:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.itheima.dao.BookDao.save())")
//若切入点表达式中返回值为*,环绕通知around()方法也可以不设置返回值。
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around_before_advice");
//表示对原始操作的调用
Object ret = pjp.proceed();
//返回的是Object的主要原因是Object类型更通用。
//在环绕通知中是可以对原始方法返回值就行修改的。
System.out.println("around_after_advice");
return ret;
//return的结果会返回给main()方法中的save()。
//本质还是执行你的方法,它本身是不知道有AOP操作的,所以还是使用原来的变量接收,只是AOP会组织个新的返回值给你。
}
}
<4> 模拟其他通知类型
//因为环绕通知是可以控制原始方法执行的,所以我们把增强的代码写在调用原始方法的不同位置就可以实现不同的通知类型的功能。
@component
@Aspect
public class MyAdvice{
public object around(ProceedingJoinPoint pjp) throws Throwable {
//增强代码只写在该位置就是前置通知
Object ret = null;
try{
//增强代码只写在该位置就是前置通知。
ret = pjp.proceed();
//增强代码只写在该位置就是返回后通知。
}catch(Throwable t){
//增强代码只写在该位置就是异常后通知。
}
//增强代码只写在该位置就是后置通知。
return ret;
}
}
(4) 返回后通知
//返回后通知是需要在原始方法正常执行后才会被执行,如果方法执行的过程中出现了异常,那么返回后通知是不会被执行。
//后置通知是不管原始方法有没有抛出异常都会被执行。
<1> 基本使用
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.save())")
private void pt(){}
@AfterReturning("pt()")
public void afterReturning() {
System.out.println("afterReturning_advice");
}
}
<2> 运行结果
BookDao_save
//afterReturning和after同时出现时,after后执行。
afterReturning_advice
(5) 异常后通知
//异常后通知是需要原始方法抛出异常后会被执行,如果没有抛异常,异常后通知将不会被执行。
<1> 基本使用
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.save())")
private void pt(){}
@AfterReturning("pt()")
public void afterThrowing() {
System.out.println("afterThrowing_advice");
}
}
<2> 运行结果
//afterThrowing和after同时出现时,after后执行。
BookDao_save
3 AOP通知获取数据
(1) 获取方法信息
//使用JoinPoint或ProceedingJoinPoint
//获取执行签名信息
Signature signature = pjp.getSignature();
//通过签名获取执行操作名称(接口名)
String className = signature.getDeclaringTypeName();
//通过签名获取执行操作名称(方法名)
String methodName = signature.getName();
(2) 获取参数
//所有方法都可以获取参数。
<1> 非环绕通知获取方式
//在方法上添加JoinPoint,通过JoinPoint来获取参数。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.save(..))")
private void pt(){}
@Before/After/AfterRunning/AfterThrowing("pt()")
public void before/after/afterRunning/afterThrowing(JoinPoint jp){
//使用JoinPoint的方式获取参数适用于前置、后置、返回后、抛出异常后通知。
Object[] args = jp.getArgs();
//参数获取后会被置于数组args中。
//因为参数的个数是不固定的,所以使用数组更通配。
System.out.println(Arrays.toString(args));
System.out.println("before/after/afterRunning/afterThrowing_advice" );
}
}
<2> 环绕通知获取方式
//环绕通知使用的是ProceedingJoinPoint。
//因为ProceedingJoinPoint是JoinPoint类的子类,所以对于ProceedingJoinPoint类中应该也会有对应的getArgs()方法。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.save(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp)throws Throwable {
Object[] args = pjp.getArgs();
//参数获取后会被置于数组args中。
System.out.println(Arrays.toString(args));
Object ret = pjp.proceed();
return ret;
}
}
<3> 环绕通知proceed()修改参数
ProceedingJoinPoint对象pjp是有两个构造方法,分别是:proceed()、proceed(Object[] objects)
调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数,所以调用这两个方法的任意一个都可以完成功能。
但是当需要修改原始方法的参数时,就只能采用带有参数的方法,如下:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.save(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = "itheima";
//修改从原始方法中获取到的参数数组。
Object ret = pjp.proceed(args);
//pjp.proceed()可以换掉/改掉切入点的实际执行参数。
//将修改后的参数数组传递给proceed()方法,相当于修改了方法的参数。
//有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤。
return ret;
}
}
(3) 获取返回值
//只有返回后AfterReturing和环绕Around这两个通知类型可以获取。
<1> 返回后通知获取返回值
//在注解中添加returning键值对,在通知中添加ret参数。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.save(..))")
private void pt(){}
@AfterReturning(value = "pt()",returning = "ret")
//returning的值必须与通知中参数名一致。
public void afterReturning(Object ret) {
//参数类型可以写成其他,但是为了能匹配更多的参数类型,建议写成Object类型。
//方法参数的顺序问题:如果有JoinPoint参数,JoinPoint参数必须放在第一位。
System.out.println("afterReturning_advice_"+ret);
//只能获取,不能修改。
}
}
<2> 环绕通知获取返回值
//直接通过proceed()返回值获取。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.save(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object ret = pjp.proceed(args);
//ret即为获取到的参数,我们是可以直接获取,不但可以获取,如果需要还可以进行修改。
return ret;
}
}
(4) 获取异常
//只有抛出异常后 AfterThrowing和环绕 Around这两个通知类型可以获取。
<1> 抛出异常后通知获取异常
//在注解中添加throwing键值对,在通知中添加Throwable参数。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.save(..))")
private void pt(){}
@AfterThrowing(value = "pt()",throwing = "t")
//returning的值必须与通知中参数名一致。
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing_advice_"+t);
}
}
<2> 环绕通知获取异常
//在catch方法中就可以获取到异常。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
Object ret = null;
try{
ret = pjp.proceed();
}catch(Throwable throwable){
t.printStackTrace();
//t即为获取到的异常。
}
return ret;
}
}

浙公网安备 33010602011771号