MENU

Spring Boot 与AOP

November 30, 2018 • Read: 172 • Spring

AOP(Aspect-oriented programming)译为面向切面编程,是的一种程序设计思想,它将横切关注点业务主体进行分离,以提高程序代码的模块化程度(解藕)。在现有代码基础上增加额外的Advice(通知)机制,对被声明为Pointcut(切点)的代码块进行统一管理;从核心关注点中分离出横切关注点是面向切面的程序设计的核心概念;像记录操作日志、记录执行时间、权限控制等功能,如果在代码实现上逐个方法去添加这些处理代码会造成代码冗余和代码入侵,也很难维护;而AOP就能很好的处理这些问题。

Hello Aspect

在Spring Boot 环境下,如果版本为Spring Boot 2.0 +,则默认使用CGLIB动态代理(基于类的动态代理),只需增加 spring-boot-starter-aop依赖即可使用;

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

如果是之前版本则需要配置

spring:
  aop:
    proxy-target-class: true

来开启基于类的动态代理,否则是基于接口的动态代理。

现在需要实现一个功能:在web包下,任意方法执行前都要记录执行时间日志。

首先,编写LogAspect切面类(省略了package和import):

/**
 * 日志切面
 */
@Component
@Aspect
public class LogAspect {

    /**
     * 定义切入点
     */
    @Pointcut(value = "execution(* cn.wangxs.spb.web.*.* (..))")
    public void logPointCut() {
    }

    /**
     * 前置通知
     */
    @Before("logPointCut()")
    public void logBefore(JoinPoint jp) {
        //打印执行方法
        System.out.println(LocalDateTime.now() + "执行" + jp.getSignature().getName());
    }

}

在类上使用 @Aspect 注解 使之成为切面类,@Component 注解 把切面类加入到Spring IOC容器中;通过 @Pointcut 定义的切入点为cn.wangxs.spb.web包下的所有方法做切入;通过 @Before 实现切入点的前置通知。

重启项目,再次执行web包下的方法,会发现控制台已经输出了执行时间日志。

2018-11-30T23:16:27.748执行hello

相关概念

  • 连接点(Joinpoint)

    程序执行的某个特定时间,比如某个方法调用前,调用后,方法抛出异常之后等,可以简单理解为时间LogAspect中的定义:

    @Before("logPointCut()")
  • 切点(Pointcut)

    切入点指切面具体织入的位置;也就是三要素中的地点;LogAspect中定义的:web包所有方法。

    @Pointcut(value = "execution(* cn.wangxs.spb.web.*.* (..))")
  • 通知(Advice)

    具体工作被称为通知。通知定义了某时何某地要做的工作;可以简单理解为事件LogAspect中的打印方法执行日志

    System.out.println(LocalDateTime.now() + "执行" + jp.getSignature().getName());
  • 切面(Aspect)

    横切关注点可以被抽取的特殊类被称为切面;切面包含了切点和通知,切点和通知共同定义了切面的全部内容(时间、地点、事件)。

定义切入点(Pointcut)

切入点由两部分组成:包含名称和参数的签名,以及一个切入点表达式;使用@Pointcut注解,且作为切入点签名的方法的方法必须是void类型。

AOP支持的AspectJ切入点指示符

  • execution:用于匹配符合的方法;这是Spring AOP使用时主要的切入点指示符。
  • within:用于匹配指定的类型的所有方法。
  • this:匹配可以向上转型为this指定的类型的代理对象中的所有方法。
  • target:匹配可以向上转型为target指定的类型的目标对象中的所有方法。
  • args:用于匹配运行时传入的参数列表的类型为指定的参数列表类型的方法;args属于动态切入点,开销非常大,非特殊情况最好不要使用。
  • @annotation:用于匹配持有指定注解的方法。
  • @within:用于匹配持有指定注解的类的所有方法。
  • @target:用于匹配持有指定注解的目标对象的所有方法。
  • @args:用于匹配运行时传入的参数列表的类型持有注解列表对应的注解 的方法。

通配符和表达式:

  • *:匹配任意数量的字符;
  • ..:匹配任意数量的参数,如在类型模式中匹配任意数量的子包;而在方法参数模式中匹配任意数量的参数。
  • +:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。
execution(modifiers-pattern?  //方法修饰符,支持通配符,可以省略
    ret-type-pattern //返回值类型,支持通配符
    declaring-type-pattern? //方法所属的类,可以省略
    name-pattern(param-pattern) //方法(参数),支持通配符
    throws-pattern?) //方法声明抛出的异常,支持通配符,可以省略

execution(* cn.wangxs.spb.web.*.* (..))这个表达式中,方法修饰符(方法修饰符)是省略的;返回值类型(ret-type-pattern)使用*表示匹配任意类型,方法所属的类(declaring-type-pattern)为cn.wangxs.spb.web.*.,表示web包下所有的类;方法(参数)(name-pattern(param-pattern))为* (..))表示任意方法,且参数为任意数量。

支持使用如下三个逻辑运算符来组合切入点表达式:

  • &&:连接点同时匹配两个切点表达式
  • ||:连接点匹配至少一个切入点表达式
  • !:连接点不匹配指定的切入点表达式

示例

execution

  1. 匹配所有public方;省略了方法所属的类(declaring-type-pattern)部分)

    execution(public * *(..))
  2. 匹配以set开始的方法;省略了方法修饰符(modifiers-pattern)部分

    execution(* set*(..))    
  3. 匹配AccountService下的所有方法

    execution(* com.xyz.service.AccountService.*(..))
  4. 匹配service包下(不包含子包)的所有方法

    execution(* com.xyz.service.*.*(..))
  5. 匹配在service包和子包里的所有类的所有方法

    execution(* com.xyz.service..*.*(..))
  6. 匹配在service包和所有子包里的所有类的无参方法

    execution(*  com.xyz.service..*.*())
    

within

  1. 匹配service包(不包含子包)下的所有类

    within(cn.wangxs.spb.service.*)
  2. 匹配service包和子包里所有的类

    within(cn.wangxs.spb.service..*)

this

  1. 匹配实现了IHelloService接口的代理对象的所有连接点

    this(cn.wangxs.spb.service.IHelloService)

target

  1. 匹配实现了IHelloService接口的对象的所有连接点

    target(cn.wangxs.spb.service.IHelloService)

args

  1. 匹配运行时动态传入参数值是Serializable类型的方法

    args(java.io.Serializable)

@within

  1. 匹配带有@RestController的所有类

    @within(org.springframework.web.bind.annotation.RestController)

@annotation

  1. 匹配持有@Log注解的所有的方法(@within和@target针对的注解,@annotation是针对方法的注解)

    @annotation(cn.wangxs.spb.annotation.Log)

定义通知(Advice)

通知类型

@Before

前置通知,在方法执行前执行。

@Override
public List<String> getNameList() throws JsonProcessingException {
    List<String> list = Stream.of("a", "b").collect(Collectors.toList());
    ObjectMapper mapper = new ObjectMapper();
    System.out.println("list:" + mapper.writeValueAsString(list));
    return list;
}
/**
 * 前置通知
 */
@Before("logPointCut()")
public void logBefore(JoinPoint jp) {
    //打印执行方法
    System.out.println(LocalDateTime.now() + "执行" + jp.getSignature().getName());
}

执行结果

2018-12-01T22:38:30.431执行getNameList

list:["a","b"]

@After

后置通知,在方法执行后执行

/**
 * 后置通知
 */
@After("logPointCut()")
public void logAfter(JoinPoint jp) {
    //打印执行方法
    System.out.println(jp.getSignature().getName() + "执行完毕");
}

执行结果(包含之前的前置通知)

2018-12-01T22:43:58.214执行getNameList

list:["a","b"]

getNameList执行完毕

@AfterRunning

返回通知, 在方法返回结果之后执行

@AfterReturning(value = "logPointCut()",returning = "result")
public void doAfter(JoinPoint joinPoint, List<String> result){
    //打印返回值
    System.out.println("返回值:"+result);
}

执行结果(包含之前的前置通知和后置通知)

2018-12-01T22:50:32.636执行getNameList

list:["a","b"]

getNameList执行完毕

返回值:[a, b]

@AfterThrowing

异常通知, 在方法抛出异常之后执行

方法增加除以0操作,抛出异常

@Override
public List<String> getNameList() throws JsonProcessingException {
    List<String> list = Stream.of("a", "b").collect(Collectors.toList());
    ObjectMapper mapper = new ObjectMapper();
    System.out.println("list:" + mapper.writeValueAsString(list));
    System.out.println(1 / 0);
    return list;
}
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void logAfterThrowing(Exception e) {
    System.out.println("抛出异常:" + e.getMessage());
}

执行结果(包含之前的通知)

2018-12-01T23:09:05.339执行getNameList

list:["a","b"]

getNameList执行完毕

抛出异常:/ by zero

2018-12-01 23:09:07.893 ERROR 13358 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero
at cn.wangxs.spb.service.impl.HelloService.getNameList(HelloService.java:27) ~[classes/:na]

通过执行结果发现,虽然方法内抛出异常,但是后置通知还是执行了,而且异常通知后置通知之后执行。

@Around

环绕通知, 围绕着方法执行

注意: 参数类型要修改为ProceedingJoinPoint;如果切入方法有返回值,通知中同样需要返回。

@Around("logPointCut()")
public Object logAround(ProceedingJoinPoint jp) throws Throwable {
    System.out.println("Around记录" + jp.getSignature().getName() + "开始执行");
    Object result = jp.proceed();
    System.out.println("Around记录" + jp.getSignature().getName() + "执行完毕");
    return result;
}

正常执行结果(包含之前的通知)

Around记录getNameList开始执行

2018-12-01T23:25:29.714执行getNameList

list:["a","b"]

Around记录getNameList执行完毕

getNameList执行完毕

返回值:[a, b]

异常执行结果(包含之前的通知)

Around记录getNameList开始执行

2019-12-01T23:27:17.588执行getNameList

list:["a","b"]

getNameList执行完毕

抛出异常:/ by zero
2018-12-01 23:27:19.914 ERROR 13465 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

注意同一切面中@Before、@After、@AfterReturn、@AfterThrowing、@Around的执行顺序。

连接点信息

Last Modified: December 1, 2019