实现一个自定义的Spring AOP注解(AOP annotation)

1. 介绍

在这篇文章中,我们将会使用Spring中的AOP支持来实现一个自定义的AOP注解(AOP annotation)。

首先,我们会给出AOP的一个高级(high-level)概述,解释它是什么和它的优点。接着,我们会一步一步地实现自己的注解,从而逐渐地对AOP有更深入的了解。

我们将会获得(outcome):更好地理解AOP,以及将来创建自定义Spring注解的能力。

2. AOP注解是什么?

快速总结一下,AOP表示面向切面编程(Aspect-Orientated Programming)。本质上,这是一种向现有代码添加行为(behavior)而不修改该代码的方式。

对于更详细的AOP介绍,有很多关于AOP pointcut和advice的文章(这是AOP中的两个概念)。这篇文章假设我们对此已基本了解。

我们将在本文实现的AOP是注解驱动的(annotation driven)。你可能对此已经熟悉,如果你使用过Spring的@Transactional注解:

@Transactional
public void orderGoods(Order order) {
    // A series of database calls to be performed in a transaction
}

在上面,我们的关注点应是它是非侵入性的(non-invasiveness)。通过使用注解元数据,我们的核心业务逻辑没有被事务代码污染。这使单独推理、重构和测试更容易。

有时,开发Spring应用程序的人将此看做“Spring魔法”,因为不必过多考虑它怎样工作的细节。实际上,发生的事情并不是特别复杂。不过,一旦我们完成本文中介绍的步骤,我们将能创建自己的自定义注解,从而理解AOP,并做到举一反三(leverage)。

3. Maven依赖

首先,让我们添加用到的Maven依赖

对于我们的例子,我们将会使用Spring Boot,因为它的约定由于配置(convention over configuration,Spring Boot特性)方法可以让我们尽快启动和运行:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.2.RELEASE</version>
</parent>

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

请注意,我们已经包含了AOP starter,它引入了我们开始实现切面(aspect)所需的库。

个人注:我使用spring initializr添加依赖时,并没有找到关于aop的依赖,其实,我们只需要把上面的starter aop依赖添加进pom.xml中即可。

4. 创建我们的自定义注解

我们将要创建的注解会用来记录一个方法的运行时间。让我们创建它:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {

}

虽然是一个相对简单的实现,但值得注意的是这两个元注释的用途。

@Target注解告诉我们自己的注解(也就是LogExecutionTime)适用于哪里。这里我们使用ElementType.Method,这意味着它仅仅对方法可用。如果我们尝试使用这个注解在任何别的地方,那么我们将不能编译通过。这种行为是有道理的,因为我们的注解将用于记录方法执行时间。

@Retention只是说明注解是否可用于JVM的运行时(runtime)。默认它是不能用的,因此Spring AOP将不会检测到这个注解。这就是我们配置它的原因。

5. 创建我们的Aspect

现在,我们有个自己的注解,让我们创建自己的切面。这只是将封装我们的横切关注点(cross-cutting concern)的模块,我们的例子是方法运行时间记录。它是一个带有@Aspect注解的类:

@Aspect
@Component
public class ExampleAspect {

}

我们已经包含了@Component注解,因为我们的类也需要是一个能被检测到的Spring bean。本质上,这是我们将实现注入自定义注解逻辑的类。

6. 创建我们的Pointcut和Advice

现在,让我们创建pointcut和advice。这将是一个存在于我们切面带注解的方法:

@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    return joinPoint.proceed();
}

个人注:所以,这个应该放在👆的ExampleAspect类里。

从技术上讲,这还没改变任何东西的行为,但是仍然有很多东西需要分析。

首先,我们对这个方法使用了@Around注解。它是我们的advice,这个around advice意味着我们在方法执行之前和之后都添加了额外的代码。有其它的advice类型,例如BeforeAfter,但是它们不在本文的讨论范围之内。

接着,我们的@Around有一个切入点参数(point cut argument)。我们的pointcut只是说,“把这个advice应用到所有的带@LogExecutionTime注解的方法。”有许多其它的pointcut类型,但是同样它们也不在我们的讨论范围内。

这个logExecutionTime()方法本身就是我们的advice。有一个单一参数ProceedingJoinPoint。在我们的例子中,这将是一个用@LogExecutionTime 注解的执行方法。

最后,当我们注解的方法结束调用时,我们的advice将首先被调用。然后要看我们的advice决定接下来要做什么。在我们的例子中,我们的advice除了调用proceed()没有做任何事,这只是调用原始的被注解方法。

7. 记录执行时间(Logging Our Execution Time)

现在我们要做的骨架已就位,我们需要做的就是在我们的advice中添加一些额外的逻辑。除了调用原始方法之外,还将会记录执行时间。让我们把这些额外的行为添加到advice:

@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();

    Object proceed = joinPoint.proceed();

    long executionTime = System.currentTimeMillis() - start;

    System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms");
    return proceed;
}

同样,我们在这里没有做任何特别复杂的事情。我们只是记录当前时间,运行这个方法,然后把它的执行时间打印到控制台。我们也记录了这个方法的签名,它有joinpoint实例提供。如果我们愿意,我们还可以访问其他信息,例如方法参数。

现在,让我们尝试用@LogExecutionTime注解一个方法,然后运行它来看看会发生什么。请注意,这必须是Spring bean才能正常工作:

@LogExecutionTime
public void serve() throws InterruptedException {
    Thread.sleep(2000);
}

运行之后,我们应该能在控制台看到下面的内容:

void org.baeldung.Service.serve() executed in 2030ms

7A. 动手实践

个人注:该节是我自己添加的内容,因为我Spring学的不是很好,在上一节遇到了一些问题。如果你已经在控制台打印出上面的内容,该节就可以跳过了。

首先,我要定义一个bean,像下面这样:

@Component
public class Sleep {
    @LogExecutionTime
    public void serve() throws InterruptedException {
        Thread.sleep(2000);
    }
}

接着,我们需要在Spring启动之后执行这个方法,达到这个目的有很多种方法,比如 @PostConstructCommandLineRunnerApplicationRunner等等,你可以从中选一个,我是实现了CommandLineRunner接口:

@Component
public class MyCommandLineRunner implements CommandLineRunner {

    @Resource
    private Sleep sleep;

    @Override
    public void run(String... var1) throws Exception {
        System.out.println("Look here!");
        sleep.serve();
        System.out.println("WATMAN");
    }
}

这是我的执行结果:

Look here!
void com.example.customaopannotation.aopInAction.Sleep.serve() executed in 2032ms
WATMAN

8. 总结

在这篇文章中,我们利用Spring Boot AOP去创建了一个自定义注解,我们可以将其应用到Spring bean以在运行时向它们注入额外的行为。

我把自己的代码放在了GitHub上(代码地址),同时这是一个 Maven 项目,它应该能够按原样运行。

原文(可能需要科学上网🐳):Baeldung - Implementing a Custom Spring AOP Annotation

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值