实现自定义 Spring AOP 注解

时间:2023-01-09 16:19:21

实现自定义 Spring AOP 注解

翻译原文链接 Implementing a Custom Spring AOP Annotation

1. 介绍

在本文中,我们将使用 Spring 中的 AOP 支持来实现自定义 AOP 注解。

In this article, we'll implement a custom AOP annotation using the AOP support in Spring.

首先,我们将给出 AOP 的高级概述,解释它是什么及其优点。在此之后,我们将逐步实现我们的注解,逐步建立对 AOP 概念的更深入理解。

First, we'll give a high-level overview of AOP, explaining what it is and its advantages. Following this, we'll implement our annotation step by step, gradually building up a more in-depth understanding of AOP concepts as we go.

(这样做的)结果将是(帮助我们)更好地理解 AOP 以及 将来(拥有)创建自定义 Spring 注解的能力。

The outcome will be a better understanding of AOP and the ability to create our custom Spring annotations in the future.

2. 什么是 AOP 注解?

(让我们)快速总结下,AOP 代表面向切面的编程。本质上,这是一种向现有代码添加行为而不修改该代码的方法。

To quickly summarize, AOP stands for aspect orientated programming. Essentially, it is a way for adding behavior to existing code without modifying that code.

对于 AOP 的详细介绍,(这里)有关于 AOP 切点通知 的文章(以供参考)。本文假设我们已经具备基本知识。

For a detailed introduction to AOP, there are articles on AOP pointcuts and advice. This article assumes we have a basic knowledge already.

我们将在本文中实现的 AOP 类型是注解驱动的。如果我们使用过 Spring@Transactional 注解,可能已经熟悉了这一点了:

The type of AOP that we will be implementing in this article is annotation driven. We may be familiar with this already if we've used the Spring @Transactional annotation:

@Transactional
public void orderGoods(Order order) {
	// 要在事务中执行的一系列数据库调用
    // A series of database calls to be performed in a transaction
}

这里的关键是非侵入性。通过使用注解元数据,我们的核心业务逻辑不会被我们的事务代码污染。这使得单独推理、重构和测试变得更容易。

The key here is non-invasiveness. By using annotation meta-data, our core business logic isn't polluted with our transaction code. This makes it easier to reason about, refactor, and to test in isolation.

有些时,开发 Spring 应用程序的人可以将其视为 “Spring Magic”,而无需详细考虑它的工作原理。实际上,发生的事情并不是特别复杂。然而,一旦我们完成了本文中的(实现)步骤,我们将能够创建我们自己的自定义注解,以便理解和利用 AOP

Sometimes, people developing Spring applications can see this as ‘Spring Magic', without thinking in much detail about how it's working. In reality, what's happening isn't particularly complicated. However, once we've completed the steps in this article, we will be able to create our own custom annotation in order to understand and leverage AOP.

3. Maven 依赖

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

First, let's add our Maven dependencies.

例如下面,我们将使用 Spring Boot,因为它的约定优于配置(的特点)让我们能够尽快启动和运行:

For this example, we'll be using Spring Boot, as its convention over configuration approach lets us get up and running as quickly as possible:

<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>

请注意,(上述 Maven 依赖中)我们已经包含了 AOP 启动器 ,它引入了我们开始实现切面所需的库。

Note that we've included the AOP starter, which pulls in the libraries we need to start implementing aspects.

4. 创建自定义注解

我们将要创建的注解将用于记录方法执行所需的时间。让我们创建注解:

The annotation we are going to create is one which will be used to log the amount of time it takes a method to execute. Let's create our annotation:

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

}

尽管(上述代码)是一个相对简单的实现,但两个元注解的用途却值得关注。

Although a relatively simple implementation, it's worth noting what the two meta-annotations are used for.

@Target 注解告诉我们注解将适用于何处。这里我们使用 ElementType.Method ,这就意味着它只适用于方法。如果我们试图在其他任何地方使用该注解,那么我们的代码将无法编译通过。这里的特性也说得通,因为我们的注解将用于记录方法执行时间。

The @Target annotation tells us where our annotation will be applicable. Here we are using ElementType.Method, which means it will only work on methods. If we tried to use the annotation anywhere else, then our code would fail to compile. This behavior makes sense, as our annotation will be used for logging method execution time.

@Retention 只是说明注解在运行时是否可用于 JVM。默认情况下不是,因此 Spring AOP 将无法看到注解。这也就是它被重新配置的原因。

And @Retention just states whether the annotation will be available to the JVM at runtime or not. By default it is not, so Spring AOP would not be able to see the annotation. This is why it's been reconfigured.

5. 创建切面

现在我们有了注解,(接下来)让我们创建切面。这只是一个模块,用来封装我们的横切关注点,我们的例子是方法执行时间记录。它只是一个类,用 @Aspect 注解:

Now we have our annotation, let's create our aspect. This is just the module that will encapsulate our cross-cutting concern, which is our case is method execution time logging. All it is is a class, annotated with @Aspect:

@Aspect
@Component
public class ExampleAspect {

}

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

We've also included the @Component annotation, as our class also needs to be a Spring bean to be detected. Essentially, this is the class where we will implement the logic that we want our custom annotation to inject.

6. 创建切点和通知

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

Now, let's create our pointcut and advice. This will be an annotated method that lives in our aspect:

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

从技术上讲,上述代码还没有改变任何事物的行为,但仍有很多事情需要分析。

Technically this doesn't change the behavior of anything yet, but there's still quite a lot going on that needs analysis.

首先,我们用 @Around 注解了我们的方法。这是我们的通知,而环绕通知意味着我们在方法执行之前和之后都添加了额外的代码。还有其他类型的通知,例如之前和之后,但它们不在本文的讨论范围之内。

First, we have annotated our method with @Around. This is our advice, and around advice means we are adding extra code both before and after method execution. There are other types of advice, such as before and after but they will be left out of scope for this article.

接下来,我们的 @Around 注解有一个切点参数。它只是表明,“将此通知应用于任何用 @LogExecutionTime 注解的方法”。还有很多其他类型的切点,但如果它们在范围内,同样将被排除在外。

Next, our @Around annotation has a point cut argument. Our pointcut just says, ‘Apply this advice any method which is annotated with @LogExecutionTime.' There are lots of other types of pointcuts, but they will again be left out if scope.

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

The method logExecutionTime() itself is our advice. There is a single argument, ProceedingJoinPoint. In our case, this will be an executing method which has been annotated with @LogExecutionTime.

最后,当我们的注解方法最终被调用时,将会首先调用我们的通知。然后由我们的通知来决定下一步做什么。在我们的例子中,我们的通知除了调用 proceed() 之外什么都不做,它只是调用原始的带注解的方法。

Finally, when our annotated method ends up being called, what will happen is our advice will be called first. Then it's up to our advice to decide what to do next. In our case, our advice is doing nothing other than calling proceed(), which is the just calling the original annotated method.

7. 记录方法的执行时间

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

Now we have our skeleton in place, all we need to do is add some extra logic to our advice. This will be what logs the execution time in addition to calling the original method. Let's add this extra behavior to our 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;
}

同样,我们在这里没有做任何特别复杂的事情。我们刚刚记录了当前时间,执行了该方法,然后将花费的时间打印到控制台。我们还记录了方法签名,提供它以使用连接点实例。如果我们愿意,我们还可以访问其他信息,例如方法参数。

Again, we've not done anything that's particularly complicated here. We've just recorded the current time, executed the method, then printed the amount of time it took to the console. We're also logging the method signature, which is provided to use the joinpoint instance. We would also be able to gain access to other bits of information if we wanted to, such as method arguments.

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

Now, let's try annotating a method with @LogExecutionTime, and then executing it to see what happens. Note that this must be a Spring Bean to work correctly:

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

执行后,我们应该看到如下记录到控制台:

After execution, we should see the following logged to the console:

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

8. 总结

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

In this article, we've leveraged Spring Boot AOP to create our custom annotation, which we can apply to Spring beans to inject extra behavior to them at runtime.

我们应用程序的源代码可以在 GitHub 上找到;这是一个 Maven 项目,应该能够按原样运行。

The source code for our application is available on over on GitHub; this is a Maven project which should be able to run as is.