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类型,例如Before
和After
,但是它们不在本文的讨论范围之内。
接着,我们的@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启动之后执行这个方法,达到这个目的有很多种方法,比如 @PostConstruct
、CommandLineRunner
、ApplicationRunner
等等,你可以从中选一个,我是实现了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