手把手教你spring aop的使用,一下子明明白白的(其二)

671 阅读14分钟

上篇文章介绍了一些切入点表达式,本篇文章进行详细阐述一下。

一、AOP切入点表达式

支持切点标识符 Spring AOP支持使用以下AspectJ切点标识符(PCD),用于切点表达式:

  • execution: 用于匹配方法执行连接点。 这是使用Spring AOP时使用的主要切点标识符。 可以匹配到方法级别 ,细粒度

  • within: 只能匹配类这级,只能指定类, 类下面的某个具体的方法无法指定,粗粒度

  • this: 匹配实现了某个接口:this(com.xyz.service.AccountService)

  • target: 限制匹配到连接点(使用Spring AOP时方法的执行),其中目标对象(正在代理的应用程序对象)是给定类型的实例。

  • args: 限制与连接点的匹配(使用Spring AOP时方法的执行),其中变量是给定类型的实例。 AOP) where the arguments are instances of the given types.

  • @target: 限制与连接点的匹配(使用Spring AOP时方法的执行),其中执行对象的类具有给定类型的注解。

  • @args: 限制匹配连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注解。

  • @within: 限制与具有给定注解的类型中的连接点匹配(使用Spring AOP时在具有给定注解的类型中声明的方法的执行)。

  • @annotation:限制匹配连接点(在Spring AOP中执行的方法具有给定的注解)。

1-1、表达式语法

spring AOP切入点表达式.jpg

访问修饰符:可不写 可以匹配任何一个访问修饰符
包名和类名:可不写 代码任意包下面的类

返回值:如果是jdk自带类型可以不用写完整限定名,如果是自定义类型需要写上完整限定名,如果被切入的方法返回值不一样可以使用*代表所有的方法值都能匹配

包名:com.* == com.jony == com.任意名字 但是只能匹配一级 比如 com.jony.service就无法匹配

如果要com.jony.service ==>com.jony.service , com.jony.* ==>com.jony.service.impl就无法匹配

com.jony..* ==>com.jony.service.impl 可以匹配

类名: 可以写*,代表任何名字的类名。 也可以模糊匹配 *ServiceImpl==> UserServiceImpl ==>RoleServiceImpl

方法名:可以写*,代表任何方法。 也可以模糊匹配 *add==> useradd ==>roleadd

参数:如果是jdk自带类型可以不用写完整限定名,如果是自定义类型需要写上完整限定名。 如果需要匹配任意参数 可以写:..

1-2、切入点标识符

1-2-1、within表达式

通过类名进行匹配 粗粒度的切入点表达式

within(包名.类名),则这个类中的所有的连接点都会被表达式识别,成为切入点。

within(com.jony.service.UserServiceImpl)

在within表达式中可以使用*号匹配符,匹配指定包下所有的类,注意,只匹配当前包,不包括当前包的子孙包。

within(com.jony.service.*)

在within表达式中也可以用*号匹配符,匹配包

within(com.jony.*.*)

在within表达式中也可以用..*号匹配符,匹配指定包下及其子孙包下的所有的类

within(com.jony..*)

1-2-2、execution()表达式

细粒度的切入点表达式,可以以方法为单位定义切入点规则

语法:execution(返回值类型 包名.类名.方法名(参数类型,参数类型…))

例子1

execution(void com.jony.service.UserServiceImpl.addUser(java.lang.String))

该切入点规则表示,切出指定包下指定类下指定名称指定参数指定返回值的方法

例子2:

execution(* com.jony.service.*.query())

该切入点规则表示,切出指定包下所有的类中的query方法,要求无参,但返回值类型不限。

例子3:

execution(* com.jony.service..*.query())

该切入点规则表示,切出指定包及其子孙包下所有的类中的query方法,要求无参,但返回值类型不限。

例子4:

execution(* com.jony.service..*.query(int,java.lang.String))

该切入点规则表示,切出指定包及其子孙包下所有的类中的query方法,要求参数为int java.langString类型,但返回值类型不限。

例子5:

execution(* com.jony.service..*.query(..))

该切入点规则表示,切出指定包及其子孙包下所有的类中的query方法,参数数量及类型不限,返回值类型不限。

例子6:

execution(* com.jony.service..*.*(..))

该切入点规则表示,切出指定包及其子孙包下所有的类中的任意方法,参数数量及类型不限,返回值类型不限。这种写法等价于within表达式的功能。

例子7:

execution(* com.jony.service..*.del*(..))

该切入点表示,切入指定包下的所有子孙包下的所有类中以del开头的方法,参数数量及类型不限,返回类型不限

1-2-3、@annotation

@annotation可以通过注解来设置切入点表达式,比如我们给UserServiceImpl里面的所有@Logger注解设置切入点

@annotation(jdk.nashorn.internal.runtime.logging.Logger)

需要注意的是,里面需要填入注解的完整限定名
那如果我们使用@Override是否可以完成切入点呢,答案是否定的,因为我们进入@Override的类,如下

image.png

RetentionPolicy.SOURCE 代表只有在java文件下才有作用,等我们java文件编译成class文件的时候,这个注解就不起作用了,所有如果我们切点是@Override是无效的。

我们再看@Logger

image.png

RetentionPolicy.RUNTIME 代表运行时起作用,因此这个注解可以被作为切入点。

1-2-4、合并切点表达式

可以使用 &&、|| 和 !等符号进行合并操作。也可以通过名字来指向切点表达式。

&& 需要多个表达式都满足,|| 满足一个 ,!只不满足,示例如下:


execution(void com.jony.service.UserServiceImpl.addUser(java.lang.String)) && @annotation(jdk.nashorn.internal.runtime.logging.Logger)

二、通知方法的执行顺序

spring4.0版本
1、正常执行:@Before­­­>@After­­­>@AfterReturning

2、异常执行:@Before­­­>@After­­­>@AfterThrowing image.png

我这边本地使用spring5.2.x(这个需要特别注意一下)
1、正常执行:@Before­­­>@AfterReturning­­­>@After

2、异常执行:@Before­­­>@AfterThrowing­­­>@After

三、获取方法的详细信息

在上篇文章的案例中 手把手教你spring aop的使用,一下子明明白白的(其一)我们并没有获取Method的详细信息,例如方法名、参数列表等信息,想要获取的话其实非常简单,只需要添加JoinPoint参数即可。

这样方法名、参数等相关信息就可以取到了

@Before("execution(* com.jony.proxy.service..*.*(..))")
public static void before(JoinPoint joinPoint){
    //获取方法名
    String name = joinPoint.getSignature().getName();
    //获取所有参数
    Object[] args = joinPoint.getArgs();
    //重新输出结果
    System.out.println(name+"方法运行前,参数是:"+(args==null?"": Arrays.toString(args)));
}

3-1、获取执行结果信息

刚刚只是获取了方法的信息,但是如果需要获取结果,还需要添加另外一个方法参数,并且告诉spring使用哪个参数来进行结果接收

image.png 通过上图,可以看到我们之前的表达式,改为了value="xxx",这是因为如果就只有一个的时候value我们省略了,但是因为我们需要接收参数所以value必须写上,另外 表达式后面我们加上了 && args(result),这个代表接收的参数,并且参数只有一个,而且这个参数result,必须和方法内参数名一致,否则会报错,如下:

//后置返回通知
@AfterReturning(value = "execution(* com.jony.proxy.service..*.*(..))",returning = "result")
public static void afterEnd(JoinPoint joinPoint,Object result){
    //获取方法名
    String name = joinPoint.getSignature().getName();
    //获取所有参数
    Object[] args = joinPoint.getArgs();

    System.out.println(name+"方法运行返回,参数是:"+(args==null?"": Arrays.toString(args))+",结果是:"+result);
    
}

执行结果

image.png 需要注意的是,获取结果需要加在@Afterruning中

3-2、获取异常信息

也可以通过相同的方式来获取异常的信息,在注解后面添加throwing="ex",然后再参数中添加Exception ex,同样注解的throwing的值需要和参数Exception 的名称一致。

//后置异常通知
@AfterThrowing(value="execution(* com.jony.proxy.service..*.*(..))",throwing = "ex")
public static void afterException(JoinPoint joinPoint,Exception ex){
    //获取方法名
    String name = joinPoint.getSignature().getName();
    //获取所有参数
    Object[] args = joinPoint.getArgs();
    System.out.println("AfterThrowing---"+name+"方法运行后报错,参数是:"+(args==null?"": Arrays.toString(args))+ex);
}

执行结果

image.png 那如果我们想捕获下面所有的异常信息,就需要添加如下代码中捕获错误信息的代码即可(固定写法)

//后置异常通知
@AfterThrowing(value="execution(* com.jony.proxy.service..*.*(..))",throwing = "ex")
public static void afterException(JoinPoint joinPoint,Exception ex){
    //获取方法名
    String name = joinPoint.getSignature().getName();
    //获取所有参数
    Object[] args = joinPoint.getArgs();

    //捕获错误信息--begin
    StringWriter stringWriter=new StringWriter();
    ex.printStackTrace(new PrintWriter(stringWriter,true));
    //捕获错误信息--end
    
    System.out.println("AfterThrowing---"+name+"方法运行后报错,参数是:"+(args==null?"": Arrays.toString(args))+stringWriter.getBuffer().toString());
}

执行结果

image.png 这样就方便我们以后根据错误信息,跟踪到错误的代码了,是不是很方便。

四、spring对通过方法的要求

spring对于通知方法的要求并不是很高,可以任意改变方法的返回值和方法的访问修饰符,但是唯一不能修改的就是方法的参数,会出现参数绑定的错误,原因在于通知方法是spring利用反射调用的,每次方法调用得确定这个方法的参数的值。

//后置通知
@After(value="execution(* com.jony.proxy.service..*.*())")
public static void after(JoinPoint joinPoint){
    //获取方法名
    String name = joinPoint.getSignature().getName();
    //获取所有参数
    Object[] args = joinPoint.getArgs();
    System.out.println("after---"+name+"方法运行后,参数是:"+(args==null?"": Arrays.toString(args)));
}

如上面方法,我们可以把public 改为private或者protect, 返回值也可以改为任意类型,这样都不会收到影响。

五、表达式的抽取@Pointcut

上面我们有LogUtils切面类,然后各个通知的表达式都一样,如果修改表达式,就得同时修改多个地方,这无疑就增加了工作量,甚至不小心还可能出错,因此我们可以使用@Pointcut来进行处理

我们声明了一个point的方法,然后加上@Pointcu注解,然后把表达式写里面,通知就可以使用point()代替原表达式了。

package com.jony.proxy.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;


@Aspect//设置切面
@Component //将类注册到IOC容器中,让spring管理,只要这样切面才会生效
public class LogUtils{

    //可以使用切点的方式,让其他类通用,重用性更强
    @Pointcut("execution(* com.jony.proxy.service..*.*(..))")
    public void point(){}

    //前置通知
    //设置任意返回值,..service包下所有子分包下的所有的类的所有方法,任意参数
    @Before(value="point()")
    public static void before(JoinPoint joinPoint){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取所有参数
        Object[] args = joinPoint.getArgs();
        //重新输出结果
        System.out.println("before---"+name+"方法运行前,参数是:"+(args==null?"": Arrays.toString(args)));
    }

    //后置通知
    @After(value="point()")
    public static void after(JoinPoint joinPoint){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取所有参数
        Object[] args = joinPoint.getArgs();
        System.out.println("after---"+name+"方法运行后,参数是:"+(args==null?"": Arrays.toString(args)));
    }

    //后置异常通知
    @AfterThrowing(value="point()",throwing = "ex")
    public static void afterException(JoinPoint joinPoint,Exception ex){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取所有参数
        Object[] args = joinPoint.getArgs();

        //捕获错误信息--begin
        StringWriter stringWriter=new StringWriter();
        ex.printStackTrace(new PrintWriter(stringWriter,true));
        //捕获错误信息--end

        System.out.println("AfterThrowing---"+name+"方法运行后报错,参数是:"+(args==null?"": Arrays.toString(args))+stringWriter.getBuffer().toString());
    }

    //后置返回通知
    @AfterReturning(value = "point()",returning = "result")
    public static void afterEnd(JoinPoint joinPoint,Object result){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取所有参数
        Object[] args = joinPoint.getArgs();

        System.out.println("AfterReturning---"+name+"方法运行返回,参数是:"+(args==null?"": Arrays.toString(args))+",结果是:"+result);

    }
}

六、获取注解中的值

AOP在切面中的通知中,我们也可以获得注解中的值,

6-1、首先我们先创建一个注解

package com.jony.proxy;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

//设置在运行时起作用
@Retention(RetentionPolicy.RUNTIME)
public @interface myannotation {
    public String name() default "";
}

6-2、给我们需要切的点添加我们自定义注解,并设置值

我们给UserServiceImpl 中的 get方法添加注解,并设置值为jony

@MyAnnotation(name="jony")
public User get(Integer id) {
    System.out.println("查询User");
    return userDao.get(id);
}

6-3、修改我们的切面通知

//前置通知
//设置任意返回值,..service包下所有子分包下的所有的类的所有方法,任意参数
@Before("point() && @annotation(myAnnotaion)")
public static void before(JoinPoint joinPoint, myannotation myAnnotaion){
    //获取方法名
    String name = joinPoint.getSignature().getName();
    //获取所有参数
    Object[] args = joinPoint.getArgs();
    //重新输出结果
    System.out.println("before---"+name+"方法运行前,参数是:"+(args==null?"": Arrays.toString(args))+"注解值为:"+myAnnotaion);
}

需要注意的是,如下图

image.png

6-4、测试

如下图,已经成功获得注解对象和name的值,如果想获取具体的值,只需要

//myAnnotation为传入的参数对象
String val=myAnnotaion.name;

image.png

6-5、总结

这种使用通过注解获取值,在项目中也会需要到,比如我们切面通知,在处理的时候,需要根据不同注解的值进行不同的判断。

举例:假如我们有两个方法都加了自定义注解,如果注解传入值为a,我们就走A逻辑,如果传入值为b,我们就走B逻辑,就好像我们去机场乘机,普通乘客和VIP乘客都有机票(自定义注解),虽然最终都是一架飞机,但是VIP就能享受不同的乘机待遇(根据票的类型,走不同的逻辑)。

七、环绕通知@Around

通过给方法添加@Around,将方法设置为环绕通知,环绕通知中,可以监控前置、返回、异常、后置通知

//环绕通知
@Around("point()")
public static void around(ProceedingJoinPoint joinPoint){
    //获取参数
    Object[] args = joinPoint.getArgs();
    //获取方法名
    String name = joinPoint.getSignature().getName();
    //设置返回值
    Object obj=null;
    //执行方法
    try {
        System.out.println("环绕前置通知-"+name+"方法开始,参数是:"+Arrays.asList(args));
        obj=joinPoint.proceed(args);
        System.out.println("环绕返回通知-"+name+"方法开始,返回值是:"+obj);
    } catch (Throwable e) {
        e.printStackTrace();
        System.out.println("环绕异常通知-"+name+"方法出现异常,返回值是:"+e);
    }finally {
        System.out.println("环绕后置通知-"+name+"方法结束");
    }
}

测试结果

image.png 可以发现,之前设置的通知和环绕通知都同时执行了,我这边使用的spring版本为5.2.19.RELEASE。其实不同的版本之间执行的顺序是不一样的

7-1、环绕通知顺序

Spring4.0

正常情况:环绕前置>目标方法执行>环绕返回>环绕最终
异常情况:环绕前置>目标方法执行>环绕异常>环绕最终

Spring5.2.19

正常情况:环绕前置>目标方法执行>环绕返回>环绕最终
异常情况:环绕前置>目标方法执行>环绕异常>环绕最终

7-2、混合通知顺序

Spring4.0

正常情况:环绕前置>@Before>目标方法执行>环绕返回>环绕最终>After>AfterReturning
异常情况:环绕前置>@Before>目标方法执行>环绕异常>环绕最终>@After>@AfterThrowing

Spring5.2.19

正常情况:环绕前置>@Before>目标方法执>@AfterReturning>@After>环绕返回>环绕最终
异常情况:环绕前置>@Before>目标方法执行>@AfterThrowing>@After>环绕异常>环绕最终

八、基于Schema的方式实现AOP

上面主要基于注解的AOP配置方式,下面我们开始看一下基于xml的配置方式

8-1、为了顺利,我们先把之前配置的信息去掉

8-1-1、去掉spring.xml里面的 aop配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">


    <!--扫描包:扫描类中所有注解,不扫描注解不是生效-->
    <context:component-scan base-package="com.jony" >
    </context:component-scan>

    <!--因为我们使用的是注解方式的AOP,所以要开启注解AOP功能-->
<!--    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>-->
</beans>

8-1-2、复制一个切面,并去掉里面的注解

LogUtilsSecond.java

package com.jony.proxy.aspect;

import com.jony.proxy.myannotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;


//@Aspect//设置切面
//@Component //将类注册到IOC容器中,让spring管理,只要这样切面才会生效
public class LogUtilsSecond {

    //前置通知
    //设置任意返回值,..service包下所有子分包下的所有的类的所有方法,任意参数
//    @Before("point() && @annotation(myAnnotaion)")
    public static void before(JoinPoint joinPoint, myannotation myAnnotaion){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取所有参数
        Object[] args = joinPoint.getArgs();
        //重新输出结果
        System.out.println("before---"+name+"方法运行前,参数是:"+(args==null?"": Arrays.toString(args))+"注解值为:"+myAnnotaion+"最终值:"+myAnnotaion.name());
    }

    //后置通知
//    @After(value="point()")
    public static void after(JoinPoint joinPoint){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取所有参数
        Object[] args = joinPoint.getArgs();
        System.out.println("after---"+name+"方法运行后,参数是:"+(args==null?"": Arrays.toString(args)));
    }

    //后置异常通知
//    @AfterThrowing(value="point()",throwing = "ex")
    public static void afterException(JoinPoint joinPoint,Exception ex){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取所有参数
        Object[] args = joinPoint.getArgs();

        //捕获错误信息--begin
        StringWriter stringWriter=new StringWriter();
        ex.printStackTrace(new PrintWriter(stringWriter,true));
        //捕获错误信息--end

        System.out.println("AfterThrowing---"+name+"方法运行后报错,参数是:"+(args==null?"": Arrays.toString(args))+stringWriter.getBuffer().toString());
    }

    //后置返回通知
//    @AfterReturning(value = "point()",returning = "result")
    public static void afterEnd(JoinPoint joinPoint,Object result){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取所有参数
        Object[] args = joinPoint.getArgs();

        System.out.println("AfterReturning---"+name+"方法运行返回,参数是:"+(args==null?"": Arrays.toString(args))+",结果是:"+result);

    }

    //环绕通知
//    @Around("point()")
    public static void around(ProceedingJoinPoint joinPoint){
        //获取参数
        Object[] args = joinPoint.getArgs();
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //设置返回值
        Object obj=null;
        //执行方法
        try {
            System.out.println("环绕前置通知-"+name+"方法开始,参数是:"+Arrays.asList(args));
            obj=joinPoint.proceed(args);
            System.out.println("环绕返回通知-"+name+"方法开始,返回值是:"+obj);
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println("环绕异常通知-"+name+"方法出现异常,返回值是:"+e);
        }finally {
            System.out.println("环绕后置通知-"+name+"方法结束");
        }
    }

}

8-1-3、再次执行测试代码

AOP的相关数据已经没有了 image.png

8-2、使用xml配置aop

8-2-1、开启aop相关设置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">


    <!--扫描包:扫描类中所有注解,不扫描注解不是生效-->
    <context:component-scan base-package="com.jony" >
    </context:component-scan>

    <!--因为我们使用的是注解方式的AOP,所以要开启注解AOP功能-->
<!--    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>-->

    <aop:config>
        <!--设置切面类,可以在上面设置一个bean,也可以给类添加@Component注解,我这边选择后者-->
        <aop:aspect ref="logUtilsSecond">
            <!--设置切面表达式-->
            <aop:pointcut id="myPointCut" expression="execution(* com.jony.proxy.service.impl.*.*(..))" />
            <!--设置前置通知 如果多个表达式,需要将所有表达式写在pointcut中,并且&符号需要转义使用&amp; 同时注解中的参数也需要和实际方法中的一致-->
            <aop:before method="before" pointcut="execution(* com.jony.proxy.service.impl.*.*(..)) &amp;&amp; @annotation(myAnnotaion)"/>
            <aop:after method="after" pointcut-ref="myPointCut"/>
            <!--方法返回通知,可以使用returning 设置返回参数,这个参数仍然需要和方法中的参数名一致-->
            <aop:after-returning method="afterEnd" pointcut-ref="myPointCut" returning="result"/>
            <!--方法异常通知,可以使用throwing 设置返回异常信息,这个ex 也需要和方法中的参数名一致-->
            <aop:after-throwing method="afterException" pointcut-ref="myPointCut" throwing="ex"/>
            <aop:around method="around" pointcut-ref="myPointCut"/>
        </aop:aspect>

    </aop:config>
</beans>

8-2-2、测试结果

image.png

总结

1、AOP底层使用的是动态代理的方式实现的,如果有接口则是JDK代理,如果没有接口则是CGlib代理
2、在后置返回通知(@AfterReturning)中可以使用returning获取返回值,在异常通知中(@AfterThrowing)中可以使用throwing获得异常信息
3、使用@excution设置表达式,需要记住表达式的语法
4、使用@annotation设置注解传值
5、使用环绕通知,并知道环绕通知和另外四个通知的执行顺序
6、掌握两种实现AOP的方式:

  • 1)给切面类添加注解@Aspect 和@Component,同时spring.xml配置 <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    1. 使用xml方式配置AOP