Spring 22 AOP – Before Advice、切点表达式语法和切点声明

Spring 22 AOP – Before Advice、切点表达式语法和切点声明

环境配置 很多切点是用注解来标示的,为了使用AOP先来进行环境配置,先看一下要如何做。 虽然我们的库里已经有了Spring全套框架,但依然需要下载导入AspectJ的包,这是因为Spring AOP使用了一些AspectJ的注解和包。 然后我们要看@Before advice的使用,操作的步骤是:

环境配置

很多切点是用注解来标示的,为了使用AOP先来进行环境配置,先看一下要如何做。 虽然我们的库里已经有了Spring全套框架,但依然需要下载导入AspectJ的包,这是因为Spring AOP使用了一些AspectJ的注解和包。 然后我们要看@Before advice的使用,操作的步骤是:
  1. 我们创建一个目标类,也就是业务类,叫做AccountDAO
  2. 然后创建Spring 的Java配置类
  3. 创建app
  4. 使用@Before来创建Aspect
https://mvnrepository.com/artifact/org.aspectj/aspectjweaver选择1.8.14版,然后下载AspectJ的Jar包,加入到项目库中。

@Before Advice

创建AccountDAO类,很简单,就是在控制台输出一句话:
import org.springframework.stereotype.Component;

@Component
public class AccountDAO {

    public void addAccount() {
        System.out.println(getClass() + ": Doing DB work: adding an account.");
    }
}
然后需要创建Spring的配置,采用Java配置类和XML结合的方式。在AccountDAO的同一个包下创建配置类:
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("aop.learn")
public class DemoConfig {

}
这里的第二个注解表示启用Spring 的AOP代理模式。 之后来创建一个app用于测试:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MainDemoApp {

    public static void main(String[] args) {

        //载入Spring的配置
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DemoConfig.class);
        //获取Bean
        AccountDAO accountDAO = context.getBean("accountDAO", AccountDAO.class);
        //调用方法
        accountDAO.addAccount();
        //关闭上下文
        context.close();
    }
}
这段代码也很简单,最关键的是下一步。 现在我们要让AccountDAO类在调用.addAccount()之前通过AOP进行一些操作,该如何做呢。 在同一个包或者子包内(在Spring扫描范围内)创建一个aspect类:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyDemoLoggingAspect {

    @Before("execution(public void addAccount())")
    public void beforeAddAccountAdvice() {
        System.out.println("\n=======>This is aspect before AddAccount advice executed.");
    }
}
这里就是使用Aspect的关键,首先使用@Aspect来注解了这个类,让其成为一个Aspect。 之后使用了@Before("execution(public void addAccont())")注解了一个方法。括号里的就是切点表达式,表示在执行public void addAccount()方法之前,执行这个被注解的beforeAddAccountAdvice()方法。 之后运行MainDemoApp,发现控制台里发生了神奇的事情:
=======>This is aspect before AddAccount advice executed.
class aop.learn.AccountDAO: Doing DB work: adding an account.
可以看到,我们没有对AccountDAO进行任何新的注解和进行编码操作,却在其方法运行之前,插入了beforeAddAccountAdvice()的运行。而且不止一次,只要调用.addAccount()方法,切面的方法就一定会先于其调用。 这就是Before Advice的用法。

切点表达式

在刚才的@Before注解中,已经使用了切点表达式,现在需要来系统看一下切点表达式,才能够继续学习AOP。 所谓切点表达式简单的说,就是描述在哪个地方切一刀把切面塞进去执行。Spring AOP使用了AspectJ的切点表达式。

切点表达式语法

切点表达式其实语法比较复杂,先从比较直观的execution表达式来看起:
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
这里的pattern实际上就是字符串,带?的表示这一部分可以不给出,依次来看:
  1. modifiers-pattern,Spring AOP中只支持public 或者 *,可以省略。
  2. return-type-pattern,这个不能省略,是返回值的类型,比如void,boolean,。
  3. declaring-type-pattern?,方法所在的类的名称,可以不写。
  4. method-name-pattern(param-pattern)。方法名与参数类型和名称,不能省略
  5. throws-pattern?,抛出异常的字符串。
这些pattern还都可以使用通配符*,表示匹配所有内容。 回想一下@Before("execution(public void addAccount())"),这其中的execution切点表达式就依次使用了modifiers,return-type和method-name-pattern(param-pattern)。但由于刚才还没有学习表达式,实际上我们这个切点表达式匹配太过宽泛,如果另外一个类里也有一个同名同返回类型的方法,也会匹配到。 下边来看一些具体例子:
仅匹配AccountDAO类里的addAccount()方法,添加方法所在的类的全名
@Before("execution(public void cc.conyli.aop.learn.AccountDAO.addAccout())")
匹配任意类中的addAccount()方法
@Before("execution(public void addAccout())")
匹配任意类中的任意以add开头的无参方法
@Before("execution(public void add*())")
匹配以process开头的任意无参,返回类型是Virtibird的方法
@Before("execution(public Virtibird process*())")
匹配任意修饰符,任意返回类型的以process开头的无参方法
@Before("execution(* * process*())")
和上一个匹配相同,但是modifier可以省略
@Before("execution(* process*())")
上边的这几种方式,只要在代码中稍加测试,就可以掌握具体使用方法,不过目前AccountDAO.addAccount()还是无参方法,下边要说一下关于param-pattern的匹配。 param-pattern有如下几种形式:
  1. (具体内容),匹配具体内容的参数
  2. (),匹配无参
  3. (*),匹配任意类型的单个参数
  4. (..),匹配无参或者任意类型的任意多个参数
*..还可以与具体类型搭配使用。我们给AccountDAO类加上一些重载的方法,算上原来的方法,一共有4个方法,按如下顺序称作1-4号方法:
public void addAccount() {
    System.out.println(getClass() + ": Doing DB work: adding an account.");
}

public void addAccount(int i) {
    System.out.println(getClass() + ": This is one INT param.");
}

public void addAccount(int i, String s) {
    System.out.println(getClass() + ": This is one INT param and one STRING param");
}

public void addAccount(int i, int j, int k) {
    System.out.println(getClass() + ": This is THREE INT params");
}
测试一下切点表达式(为了节省篇幅,表格里仅写方法部分的表达式),匹配结果如下:
表达式 匹配编号
addAccount() 1
addAccount(int) 2
addAccount(*) 2
addAccount(int, String) 3
addAccount(..) 1 2 3 4
addAccount(int, ..) 2 3 4
addAccount(.., String) 3
addAccount(.., int) 2 4
读者还可以自行实验其他例子。经过实验可以发现,..通配符就类似正则表达式的0-正无穷,*就是单个任意匹配,搭配上其他具体类型,参数匹配就很灵活了 掌握了切点表达式,之后再看一下切点的其他语法。

切点声明

在初步了解了切点表达式之后,我们有一个新的问题,如果要将切点表达式重用在其他地方该怎么办呢?一个一个粘贴过去固然可以解决问题,但面临着硬编码的坏处。 解决办法就是创建一个切点声明,然后将这个声明作用给很多advice。 先来修改一下我们的aspect类,创建一个切点声明并应用一下:
@Aspect
@Component
public class MyDemoLoggingAspect {

    @Before("forDAOPackage()")
    public void beforeAddAccountAdvice() {
        System.out.println("\n=======>This is aspect before AddAccount advice executed.");
    }

    @Pointcut("execution(* aop.learn.AccountDAO.*(..))")
    private void forDAOPackage() {}

}
@Pointcut就是切点声明,可以认为一个切点里边包裹了一个切点表达式,然后被修饰的方法名称可以任意起,之后在@Before("forDAOPackage()")中将这个方法名称作为注解的参数传进去。 这样就可以实现一个切点的复用了,我们可以轻松的再创建一个方法依然使用该切点表达式:
@Before("forDAOPackage()")
public void AnotherBeforeAddAccountAdvice() {
    System.out.println("\n=======>This is another aspect before AddAccount advice executed.");
}
再执行一下Demo,会发现两个方法都执行了。 如果只是单个复用,未免太简单了点,如果想实现一个切面对于一个类里边所有除了getter和setter方法之外的方法都生效,要如何配置呢。对于这种需求,切点声明可以通过逻辑运算来实现。 切点声明使用了如下逻辑运算符来使用多个声明:
  1. &&,表示与
  2. ||,表示或
  3. !,表示NOT,否
这几个逻辑符和Java的逻辑符号一样,具体的使用方法是:
@Before("expressionMethodOne() && expressionMethodTwo()")
@Before("expressionMethodOne() || expressionMethodTwo()")
@Before("expressionMethodOne() && !expressionMethodTwo()")
其实看到这里也知道大概用法了,声明两个或者更多的@Pointcut切点,然后通过@Before或者其他注解来使用就可以了,来看一个简单的例子: 我们给AccountDAO类添加两个字符串域变量name和serviceCode并设置getter和setter方法,然后去除之前为了学习切点表达式而创建的那些新的重载方法,然后把我们之前写的两个应用于AccountDAO类的切面方法只保留一个。然后完成一个需求,就是让这个切面仅对AccountDAO内部不属于getter和setter方法的方法生效: 目前的AccountDAO类:
import org.springframework.stereotype.Component;

@Component
public class AccountDAO {

    private String name;
    private String serviceCode;


    public void addAccount() {
        System.out.println(getClass() + ": Doing DB work: adding an account.");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getServiceCode() {
        return serviceCode;
    }

    public void setServiceCode(String serviceCode) {
        this.serviceCode = serviceCode;
    }

    public AccountDAO(String name, String serviceCode) {
        this.name = name;
        this.serviceCode = serviceCode;
    }

    public AccountDAO() {
    }

    @Override
    public String toString() {
        return "AccountDAO{" +
                "name='" + name + '\'' +
                ", serviceCode='" + serviceCode + '\'' +
                '}';
    }
}
很普通的一个类,然后我们来修改我们的Aspect类,创建切点然后进行配置:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyDemoLoggingAspect {

    @Pointcut("execution(* aop.learn.AccountDAO.*(..))")
    private void forDAOPackage() {}

    @Pointcut("execution(* aop.learn.AccountDAO.get*(..))")
    private void forGetterMethod(){}

    @Pointcut("execution(* aop.learn.AccountDAO.set*(..))")
    private void forSetterMethod() {}

    @Before("forDAOPackage() && !forGetterMethod() && !forSetterMethod()")
    public void beforeAddAccountAdvice() {
        System.out.println("\n=======>This is aspect for all method in AccountDAO executed.");
    }

}
这里的三个切点声明,第一个是我们之前创建过的,针对AccountDAO所有的方法。第二个和第三个分别是针对getter和setter方法的。 但是在配置切面方法的时候,使用了逻辑运算符,造成的结果就是getter和setter方法不会匹配这个切面方法。 然后修改MainDemoApp实验各种方法:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MainDemoApp {

    public static void main(String[] args) {

        //载入Spring的配置
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DemoConfig.class);
        //获取Bean
        AccountDAO accountDAO = context.getBean("accountDAO", AccountDAO.class);
        //调用各种方法

        accountDAO.setName("jenny");
        accountDAO.setServiceCode("SRWT");
        System.out.println(accountDAO.getName());
        System.out.println(accountDAO.getServiceCode());
        System.out.println(accountDAO);
        accountDAO.addAccount();

        //关闭上下文
        context.close();
    }
}
可以发现,调用setter和getter方法均没有切面进来,而打印accountDAO=调用.toString()方法和调用.addAccount()方法均有切面执行。这就是切点声明的组合使用。

切面的顺序

在刚才最开始学习切点声明的时候,我们为两个切面方法都应用了同样的一个切点声明,如果还有更多的切面需要在同一个方法上执行,那么顺序究竟是什么呢。 如果将切面都写在同一个@Aspect类内,实际上并没有定义顺序,如果需要确实的定义执行的顺序,则需要将切面分到不同的类中,然后使用@Order来标示顺序。 我们现在有一个@Aspect类MyDemoLoggingAspect,假设这个类执行的是日志功能,我们还需要两个类,一个类是安全Security切面,一个类是分析Audit切面。如果我们执行的顺序是安全--分析--日志,那排序的方法如下:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

//MyDemoSecurityAspect.java
@Aspect
@Component
@Order(1)
public class MyDemoSecurityAspect {

    @Pointcut("execution(* aop.learn.AccountDAO.*(..))")
    private void forDAOPackage() {
    }

    @Pointcut("execution(* aop.learn.AccountDAO.get*(..))")
    private void forGetterMethod() {
    }

    @Pointcut("execution(* aop.learn.AccountDAO.set*(..))")
    private void forSetterMethod() {
    }

    @Before("forDAOPackage() && !forGetterMethod() && !forSetterMethod()")
    public void beforeAddAccountAdvice() {
        System.out.println("\n=======>This is security aspect for all method in AccountDAO executed.");
    }

}

//MyDemoAuditAspect.java
@Aspect
@Component
@Order(2)
public class MyDemoAuditAspect {

    @Pointcut("execution(* aop.learn.AccountDAO.*(..))")
    private void forDAOPackage() {
    }

    @Pointcut("execution(* aop.learn.AccountDAO.get*(..))")
    private void forGetterMethod() {
    }

    @Pointcut("execution(* aop.learn.AccountDAO.set*(..))")
    private void forSetterMethod() {
    }

    @Before("forDAOPackage() && !forGetterMethod() && !forSetterMethod()")
    public void beforeAddAccountAdvice() {
        System.out.println("\n=======>This is audit aspect for all method in AccountDAO executed.");
    }

}

//MyDemoLoggingAspect.java
@Aspect
@Component
@Order(3)
public class MyDemoLoggingAspect {

    @Pointcut("execution(* aop.learn.AccountDAO.*(..))")
    private void forDAOPackage() {
    }

    @Pointcut("execution(* aop.learn.AccountDAO.get*(..))")
    private void forGetterMethod() {
    }

    @Pointcut("execution(* aop.learn.AccountDAO.set*(..))")
    private void forSetterMethod() {
    }

    @Before("forDAOPackage() && !forGetterMethod() && !forSetterMethod()")
    public void beforeAddAccountAdvice() {
        System.out.println("\n=======>This is Logging aspect for all method in AccountDAO executed.");
    }

}
这里给切面排序的方法就是@Order(?)注解。?可以是负数也可以是正数,按照从小到大的顺序执行,并且可以不连续。比如将Security切面顺序设置为-3,Audit切面设置为7,日志切面设置为15,则依然可以按照顺序运行。 如果两个切面的顺序是相同的,则这两个切面的先后顺序是未定义的,会任意先执行两个其中的一个,但这两个切面一定都会在比他们顺序靠前的切面后执行,在顺序靠后的切面前执行。例如四个切面的顺序是 1,6,6,12,则肯定先执行完1,然后两个6的顺序不固定,两个6都执行完后,再执行12。

使用切点获取方法参数

比如日志功能,需要记录每次操作的数据,很显然,如果无法获取方法的参数,那么依然需要在advice的前后各写一些代码,将参数传递给日志模块。Spring AOP提供了直接获取被修饰的方法的参数的功能,这样就非常方便了。 这个功能的原理是这样:我们定义的切点实际上包含了所有被修饰方法的元数据,通过将切点传入到切面方法里,我们可以获取相关的参数。 来进行一些实验,之前我们定义了切面规则是不用于setter和getter方法,现在全部打开;还定义了Security和Audit切面,现在把这两个切面关闭,让Aspect类简单一些。然后修改切面方法:
@Before("forDAOPackage()")
public void beforeAddAccountAdvice(JoinPoint joinPoint) {
    System.out.println("\n=======>This is logging aspect for all method in AccountDAO executed.");
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    System.out.println("Method: " + methodSignature);

    Object[] args = joinPoint.getArgs();

    for (Object arg : args) {
        System.out.println(arg);
    }
}
这个方法里值得注意的有如下几处:
  1. JoinPoint作为参数被传入切面方法,JoinPoint里包含被修饰方法的元数据
  2. joinPoint.getSignature()返回方法对象,打印出来可以看到其中的内容
  3. joinPoint.getArgs()返回方法的参数列表,打印出来也看一下内容
然后我们执行一个setter方法看看控制栏的输出:
=======>This is logging aspect for all method in AccountDAO executed.
Method: void aop.learn.AccountDAO.setName(String)
jenny
可见打印出了这个方法的方法签名对象,其中的参数是String类型的一个参数 还打印出了这个String类型的值。 OK,看来测试成功,那么我们只需要知道了参数的类型,就可以来获取具体的值了,哪怕参数是一个类,也可以通过类型转换,instanceof等运算符,判断是类型后获取类型的变量值。 这样在传递model对象的时候,就可以通过日志获取其中的model对象的值,然后进而获取其中的具体对象的值,将值传递到日志中进行日志保存了。
LICENSED UNDER CC BY-NC-SA 4.0
Comment