目录

Spring Boot AOP 实践

依赖及配置

1
2
3
4
5
6
<!-- 包含 aspectjweaver spring-aop -->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1
2
3
4
5
6
7
8
<!-- 上面会引入下面的 -->
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
    <scope>runtime</scope>
</dependency>

使用 Spring,您可以使用 AspectJ 的注解声明 Advice,必须首先将@EnableAspectJAutoProxy 注解应用于您的配置类,这将支持处理标记有 AspectJ 的 @Aspect 注解的组件。

在 Spring Boot 项目中,我们不必显式使用@EnableAspectJAutoProxy。如果Aspect或 Advice在类路径上,则有一个专用的 AopAutoConfiguration 启用 Spring 的 AOP 支持 。

1
2
3
4
5
@Configuration
@EnableAspectJAutoProxy
public class AopConfiguration {
    ...
}

Pointcut Designators(PCD) 切入点指示符

pointcut 表达式以一个切入点指示符 PCD 开始

execution

主要的 Spring PCD 是执行,匹配方法执行 join points。

1
2
3
@Pointcut("execution(public String com.ynthm.pointcutadvice.dao.FooDao.findById(Long))")

@Pointcut("execution(* com.ynthm.pointcutadvice.dao.FooDao.*(..))")

within

within PCD 它将匹配限制为特定类型的连接点。

1
2
@Pointcut("within(com.ynthm.pointcutadvice.dao.FooDao)")
@Pointcut("within(com.ynthm..*)")

this and target

this 将匹配限制到 bean 引用是给定类型的实例的连接点,而 target 限制匹配到目标对象是给定类型的实例的连接点。前者在 Spring AOP 创建基于 CGLIB 的代理时工作,后者在创建基于 JDK 的代理时使用。

1
2
3
4
5
public class FooDao implements BarDao {
    ...
}
@Pointcut("this(com.baeldung.pointcutadvice.dao.FooDao)")
@Pointcut("target(com.baeldung.pointcutadvice.dao.BarDao)")

args

此 PCD 用于匹配特定的方法参数

1
2
3
4
// 这个切入点匹配任何以 find 开头并且只有一个Long类型参数的方法。
@Pointcut("execution(* *..find*(Long))")
// 匹配具有任意数量参数但具有Long类型的第一个参数的方法
@Pointcut("execution(* *..find*(Long,..))")

@target

将匹配限制为执行对象的类具有给定类型的注解的连接点

1
@Pointcut("@target(org.springframework.stereotype.Repository)")

@args

将匹配的连接点中传递的实际参数的运行时类型具有给定类型的注解。

1
2
3
4
5
6
@Pointcut("@args(com.baeldung.pointcutadvice.annotations.Entity)")
public void methodsAcceptingEntities() {}
@Before("methodsAcceptingEntities()")
public void logMethodAcceptionEntityAnnotatedBean(JoinPoint jp) {
    logger.info("Accepting beans with @Entity annotation: " + jp.getArgs()[0]);
}

@within

此 PCD 将匹配限制为具有给定注释的类型内的连接点

@Pointcut("@within(org.springframework.stereotype.Repository)")

@annotation

此 PCD 将匹配限制为连接点的主题具有给定注释的连接点。

1
2
3
4
5
6
7
@Pointcut("@annotation(com.baeldung.pointcutadvice.annotations.Loggable)")
public void loggableMethods() {}
@Before("loggableMethods()")
public void logMethod(JoinPoint jp) {
    String methodName = jp.getSignature().getName();
    logger.info("Executing method: " + methodName);
}

组合切入点表达式

1
2
3
4
5
6
7
8
@Pointcut("@target(org.springframework.stereotype.Repository)")
public void repositoryMethods() {}

@Pointcut("execution(* *..create*(Long,..))")
public void firstLongParamMethods() {}

@Pointcut("repositoryMethods() && firstLongParamMethods()")
public void entityCreationMethods() {}

Advice

Before Advice

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Component
@Aspect
public class LoggingAspect {

    private Logger logger = Logger.getLogger(LoggingAspect.class.getName());

    @Pointcut("@target(org.springframework.stereotype.Repository)")
    public void repositoryMethods() {};

    @Before("repositoryMethods()")
    public void logMethodCall(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        logger.info("Before " + methodName);
    }
}

After Advice

使用 @After 注解声明的 After Advice 在匹配的方法执行后执行,无论是否引发异常。 在某些方面,它类似于finally块。

  • @AfterReturning 注解 如果您需要仅在正常执行后触发通知
  • @AfterThrowing 如果您希望仅在目标方法抛出异常时触发您的通知
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Component
@Aspect
public class PublishingAspect {

    private ApplicationEventPublisher eventPublisher;

    @Autowired
    public void setEventPublisher(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    @Pointcut("@target(org.springframework.stereotype.Repository)")
    public void repositoryMethods() {}

    @Pointcut("execution(* *..create*(Long,..))")
    public void firstLongParamMethods() {}

    @Pointcut("repositoryMethods() && firstLongParamMethods()")
    public void entityCreationMethods() {}

    @AfterReturning(value = "entityCreationMethods()", returning = "entity")
    public void logMethodCall(JoinPoint jp, Object entity) throws Throwable {
        eventPublisher.publishEvent(new FooCreationEvent(entity));
    }
}

@Component
public class FooCreationEventListener implements ApplicationListener<FooCreationEvent> {

    private Logger logger = Logger.getLogger(getClass().getName());

    @Override
    public void onApplicationEvent(FooCreationEvent event) {
        logger.info("Created foo instance: " + event.getSource().toString());
    }
}

Around Advice

环绕通知围绕一个连接点,例如方法调用。

环绕通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续到连接点还是通过提供自己的返回值或抛出异常来缩短建议的方法执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Aspect
@Component
public class PerformanceAspect {

    private Logger logger = Logger.getLogger(getClass().getName());

    @Pointcut("within(@org.springframework.stereotype.Repository *)")
    public void repositoryClassMethods() {};

    @Around("repositoryClassMethods()")
    public Object measureMethodExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        Object retval = pjp.proceed();
        long end = System.nanoTime();
        String methodName = pjp.getSignature().getName();
        logger.info("Execution of " + methodName + " took " + 
          TimeUnit.NANOSECONDS.toMillis(end - start) + " ms");
        return retval;
    }
}

该建议采用 ProceedingJointPoint 类型的一个参数。该参数使我们有机会在目标方法调用之前采取行动。

注解

创建自定义 Pointcut 注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccountOperation {
    String operation();
}

@Aspect
@Component
public class BankAccountAspect {

    @Before(value = "@annotation(com.ynthm.method.info.AccountOperation)")
    public void getAccountOperationInfo(JoinPoint joinPoint) {

        // Method Information
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        System.out.println("full method description: " + signature.getMethod());
        System.out.println("method name: " + signature.getMethod().getName());
        System.out.println("declaring type: " + signature.getDeclaringType());

        // Method args
        System.out.println("Method args names:");
        Arrays.stream(signature.getParameterNames())
          .forEach(s -> System.out.println("arg name: " + s));

        System.out.println("Method args types:");
        Arrays.stream(signature.getParameterTypes())
          .forEach(s -> System.out.println("arg type: " + s));

        System.out.println("Method args values:");
        Arrays.stream(joinPoint.getArgs())
          .forEach(o -> System.out.println("arg value: " + o.toString()));

        // Additional Information
        System.out.println("returning type: " + signature.getReturnType());
        System.out.println("method modifier: " + Modifier.toString(signature.getModifiers()));
        Arrays.stream(signature.getExceptionTypes())
          .forEach(aClass -> System.out.println("exception type: " + aClass));

        // Method annotation
        Method method = signature.getMethod();
        AccountOperation accountOperation = method.getAnnotation(AccountOperation.class);
        System.out.println("Account operation annotation: " + accountOperation);
        System.out.println("Account operation value: " + accountOperation.operation());
    }
}

附录