Spring——AOP细节
5.1 切入点表达式
5.1.1 作用 通过表达式的方式定位一个或多个具体的连接点。
5.1.2 语法细节
切入点表达式的语法格式
execution([权限修饰符] [返回值类型] [简单类名/全类名] 方法名)
例子
表达式 execution(***** com.atguigu.spring.ArithmeticCalculator.*(…))
含义 ArithmeticCalculator 接口中声明的所有方法。
第一个“”代表任意修饰符及任意返回值。
第二个“”代表任意方法。
“…”匹配任意数量、任意类型的参数。
若目标类、接口与该切面类在同一个包中可以省略包名。
3)在 AspectJ 中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。
5.1.3 切入点表达式应用到实际的切面类中
5.2 当前连接点细节
5.2.1 概述
切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能 够确定对应的连接点。那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具 体信息,例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在 JoinPoint 接口的实例对象中。
5.2.2 JoinPoint
5.3 通知
5.3.1 概述
在具体的连接点上要执行的操作。
一个切面可以包括一个或者多个通知。
通知所使用的注解的值往往是切入点表达式。
5.3.2 前置通知
前置通知:在方法执行之前执行的通知
使用@Before 注解
5.3.3 后置通知
后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时 候
使用@After 注解
5.3.4 返回通知
返回通知:无论连接点是正常返回还是抛出异常,后置通知都会执行。如果只想在连接 点返回的时候记录日志,应使用返回通知代替后置通知。
使用@AfterReturning 注解,在返回通知中访问连接点的返回值
①在返回通知中,只要将 returning 属性添加到@AfterReturning 注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称
②必须在通知方法的签名中添加一个同名参数。在运行时 Spring AOP 会通过这个参数 传递返回值
③原始的切点表达式需要出现在 pointcut 属性中
5.3.5 异常通知
异常通知:只在连接点抛出异常时才执行异常通知
将 throwing 属性添加到@AfterThrowing 注解中,也可以访问连接点抛出的异常。
Throwable 是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误 和异常。
如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通 知就只在抛出这个类型及其子类的异常时才被执行
5.3.6 环绕通知
环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是 否执行连接点。
对于环绕通知来说,连接点的参数类型必须是 ProceedingJoinPoint。它是 JoinPoint 的 子接口,允许控制何时执行,是否执行连接点。
在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed()方法来执行被代理的方法。 如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。
注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed(); 的返回值,否则会出现空指针异常
5.4 重用切入点定义
在编写 AspectJ 切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达 式可能会在多个通知中重复出现。
在 AspectJ 切面中,可以通过@Pointcut 注解将一个切入点声明成简单的方法。切入点 的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。
切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面 中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为 public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中, 还必须包含包名。
其他通知可以通过方法名称引入该切入点
5.4 指定切面的优先级
在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。
切面的优先级可以通过实现 Ordered 接口或利用@Order 注解指定。
实现 Ordered 接口,getOrder()方法的返回值越小,优先级越高。
若使用@Order 注解,序号出现在注解中
实例:
@Aspect
@Component
@Order(1)//使用Order改变切面顺序;数值越小优先级越高
public class LogUtils {
/**
* 告诉Spring每个方法都什么时候运行;
* try{
* @Before
* method.invoke(obj,args);
* @AfterReturning
* }catch(e){
* @AfterThrowing
* }finally{
* @After
* }
*
* 5个通知注解
* @Before:在目标方法之前运行; 前置通知
* @After:在目标方法结束之后 后置通知
* @AfterReturning:在目标方法正常返回之后 返回通知
* @AfterThrowing:在目标方法抛出异常之后运行 异常通知
* @Around:环绕 环绕通知
*
*
* 抽取可重用的切入点表达式;
* 1、随便声明一个没有实现的返回void的空方法
* 2、给方法上标注@Pointcut注解
*/
@Pointcut("execution(public int com.atguigu.impl.MyMathCalculator.*(..))")
public void hahaMyPoint(){};
//想在执行目标方法之前运行;写切入点表达式
//execution(访问权限符 返回值类型 方法签名)
@Before("hahaMyPoint()")
public static void logStart(JoinPoint joinPoint){
//获取到目标方法运行是使用的参数
Object[] args = joinPoint.getArgs();
//获取到方法签名
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("[LogUtils-前置]【"+name+"】方法开始执行,用的参数列表【"+Arrays.asList(args)+"】");
}
/**
* 切入点表达式的写法;
* 固定格式: execution(访问权限符 返回值类型 方法全类名(参数表))
*
* 通配符:
* *:
* 1)匹配一个或者多个字符:execution(public int com.atguigu.impl.MyMath*r.*(int, int))
* 2)匹配任意一个参数:第一个是int类型,第二个参数任意类型;(匹配两个参数)
* execution(public int com.atguigu.impl.MyMath*.*(int, *))
* 3)只能匹配一层路径
* 4)权限位置*不能;权限位置不写就行;public【可选的】
* ..:
* 1)匹配任意多个参数,任意类型参数
* 2)匹配任意多层路径:
* execution(public int com.atguigu..MyMath*.*(..));
*
* 记住两种;
* 最精确的:execution(public int com.atguigu.impl.MyMathCalculator.add(int,int))
* 最模糊的:execution(* *.*(..)):千万别写;
*
* &&”、“||”、“!
*
* &&:我们要切入的位置满足这两个表达式
* MyMathCalculator.add(int,double)
* execution(public int com.atguigu..MyMath*.*(..))&&execution(* *.*(int,int))
*
*
* ||:满足任意一个表达式即可
* execution(public int com.atguigu..MyMath*.*(..))&&execution(* *.*(int,int))
*
* !:只要不是这个位置都切入
* !execution(public int com.atguigu..MyMath*.*(..))
*
* 告诉Spring这个result用来接收返回值:
* returning="result";
*/
//想在目标方法正常执行完成之后执行
@AfterReturning(value="hahaMyPoint()",returning="result")
public static void logReturn(JoinPoint joinPoint,Object result){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("[LogUtils-返回]【"+name+"】方法正常执行完成,计算结果是:"+result);
}
/**
* 细节四:我们可以在通知方法运行的时候,拿到目标方法的详细信息;
* 1)只需要为通知方法的参数列表上写一个参数:
* JoinPoint joinPoint:封装了当前目标方法的详细信息
* 2)、告诉Spring哪个参数是用来接收异常
* throwing="exception":告诉Spring哪个参数是用来接收异常
* 3)、Exception exception:指定通知方法可以接收哪些异常
*
* ajax接受服务器数据
* $.post(url,function(abc){
* alert(abc)
* })
*/
//想在目标方法出现异常的时候执行
@AfterThrowing(value="hahaMyPoint()",throwing="exception")
public static void logException(JoinPoint joinPoint,Exception exception) {
System.out.println("[LogUtils-异常]【"+joinPoint.getSignature().getName()+"】方法执行出现异常了,异常信息是【"+exception+"】:;这个异常已经通知测试小组进行排查");
}
//想在目标方法结束的时候执行
/**
* Spring对通知方法的要求不严格;
* 唯一要求的就是方法的参数列表一定不能乱写?
* 通知方法是Spring利用反射调用的,每次方法调用得确定这个方法的参数表的值;
* 参数表上的每一个参数,Spring都得知道是什么?
* JoinPoint:认识
* 不知道的参数一定告诉Spring这是什么?
*
* @param joinPoint
*/
@After("hahaMyPoint()")
private int logEnd(JoinPoint joinPoint) {
System.out.println("[LogUtils-后置]【"+joinPoint.getSignature().getName()+"】方法最终结束了");
return 0;
}
/**
* @throws Throwable
* @Around:环绕 :是Spring中强大的通知;
* @Around:环绕:动态代理;
* try{
* //前置通知
* method.invoke(obj,args);
* //返回通知
* }catch(e){
* //异常通知
* }finally{
* //后置通知
* }
*
* 四合一通知就是环绕通知;
* 环绕通知中有一个参数: ProceedingJoinPoint pjp
*
*环绕通知:是优先于普通通知执行,执行顺序;
*
*[普通前置]
*{
* try{
* 环绕前置
* 环绕执行:目标方法执行
* 环绕返回
* }catch(){
* 环绕出现异常
* }finally{
* 环绕后置
* }
*}
*
*
*[普通后置]
*[普通方法返回/方法异常]
*新的顺序:
* (环绕前置---普通前置)----目标方法执行----环绕正常返回/出现异常-----环绕后置----普通后置---普通返回或者异常
*注意:
*/
@Around("hahaMyPoint()")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
String name = pjp.getSignature().getName();
//args[0] = 100;
Object proceed = null;
try {
//@Before
System.out.println("【环绕前置通知】【"+name+"方法开始】");
//就是利用反射调用目标方法即可,就是method.invoke(obj,args)
proceed = pjp.proceed(args);
//@AfterReturing
System.out.println("【环绕返回通知】【"+name+"方法返回,返回值"+proceed+"】");
} catch (Exception e) {
//@AfterThrowing
System.out.println("【环绕异常通知】【"+name+"】方法出现异常,异常信息:"+e);
//为了让外界能知道这个异常,这个异常一定抛出去
throw new RuntimeException(e);
} finally{
//@After
System.out.println("【环绕后置通知】【"+name+"】方法结束");
}
//反射调用后的返回值也一定返回出去
return proceed;
}
}