猫头鹰的深夜翻译:使用SpringBoot和AspectJ实现AOP

前言

这篇文章会帮助你使用Spring Boot Starter AOP实现AOP。我们会使用AspectJ实现四个不同的通知(advice),并且新建一个自定义的注解来追踪方法的执行时间。

你将会了解

  • 什么是交叉分割关注点(cross-cutting concern)?
  • 在应用中你如何实现交叉分割关注点?
    - 如果你想要将对web应用所有的访问请求记入日志,你能想到什么方法?
    - 如果你想追踪每个请求的性能,你能想到什么方法?
  • AOP中的切面(Aspects)和切点(Pointcut)是什么?
  • 有哪些不同类型的AOP通知(advice)?
  • 如何使用Spring Boot实现AOP?
  • 如何使用Spring AOP和AspectJ实现切面?
  • 有哪些AOP最佳实践?

项目代码结构

下图是我们即将创建的项目结构的截图:

一些细节:

  • SpringBootTutorialBasicsApplication.java: 由Spring Initializer初始化生成的Spring Boot应用类。这个类是应用启动类。
  • pom.xml: 创建项目所需的全部依赖。我们将会使用Spring Boot Starter AOP依赖。
  • Business1.java, Business2.java, Dao1.java, Dao2.java: 业务类依赖于Dao类。我们会写切面来拦截对这些业务类和DAO类的调用。
  • AfterAopAspect.java: 实现一些After通知。
  • UserAccessAspect.java: 实现一个Before通知,用来做访问权限检查
  • BusinessAopSpringBootTest.java:对业务方法进行单元测试
  • Maven3.0+:编译工具
  • Eclipse: 开发工具
  • JDK1.8+

源码Github地址

介绍AOP

应用通常划分为多个层进行开发,一个经典的JAVA应用有:

  • 网络层:用REST或是应用的形式将服务暴露给外部使用
  • 业务层:业务逻辑
  • 数据层:数据持久化逻辑
    虽然各个层的职责不同,但是每个层之间也有一些共通的地方
  • 日志
  • 安全

这些共通的切面成为交叉分割关注点(cross-cutting-concerns)

实现交叉分割关注点的一个方法是在每一个层分贝进行实现。但是,这样会使得代码难以维护。

面向切面编程为实现交叉分割关注点提供了一个解决方案:

  • 将交叉分割切入点实现为一个切面
  • 定义切点,说明这些切面在何时调用
    这样确保了交叉分割关注点定义在一个内聚的代码组件中,并且能够在需要的时候使用。

初始化Spring Boot AOP项目

使用Spring Initializer新建一个Spring AOP项目非常的方法。

Spring Initializer是创建Spring Boot项目的超级棒的工具。

备注:

  • 启动Spring Initializer并且选择一下内容
    • 选择com.in28minutes.springboot.tutorial.basics.example为Group
    • 选择 spring-boot-tutorial-basics为Artifact
    • 选择AOP依赖
  • 点击Generate Project
  • 将项目导入Eclipse

Spring Boot AOP starter

Spring Boot AOP Starter的关键依赖有:

  • Spring AOP提供的基本的AOP功能
  • AspectJ提供的完整的AOP框架
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.0.1.RELEASE</version>
    <scope>compile</scope>
    </dependency>
    <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.12</version>
    <scope>compile</scope>
    </dependency>

配置AOP

让我们添加一些业务逻辑类 - Business1和Business2。这些业务逻辑类依赖于一组数据类 - Data1和Data2。

1
2
3
4
5
6
7
8
9
10
11
@Service
public class Business1 {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private Dao1 dao1;
public String calculateSomething() {
String value = dao1.retrieveSomething();
logger.info("In Business - {}", value);
return value;
}
}
1
2
3
4
5
6
7
8
9
@Service
public class Business2 {
@Autowired
private Dao2 dao2;
public String calculateSomething() {
//Business Logic
return dao2.retrieveSomething();
}
}
1
2
3
4
5
6
@Repository
public class Dao1 {
public String retrieveSomething() {
return "Dao1";
}
}
1
2
3
4
5
6
@Repository
public class Dao2 {
public String retrieveSomething() {
return "Dao2";
}
}

备注:

  • @Autowired private Dao1 dao1: DAO作为依赖注入业务类中
  • public String calculateSomething(): 每个业务类包含一个简单的calculate方法

一个简单的AOP单元测试

让我们写一个简单的单元测试来调用刚刚创建的业务类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(SpringRunner.class)
@SpringBootTest
public class BusinessAopSpringBootTest {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private Business1 business1;
@Autowired
private Business2 business2;
@Test
public void invokeAOPStuff() {
logger.info(business1.calculateSomething());
logger.info(business2.calculateSomething());
}
}

备注:

  • @RunWith(SpringRunner.class) @SpringBootTest public class BusinessAopSpringBootTest:: 我们将在单元测试用启动一个完整的Spring Boot应用
  • @Autowired private Business1 business1@Autowiredprivate Business2 business2: 将业务类注入启动测试的Spring上下文中
  • @Test public void invokeAOPStuff(){...}: 调用业务层的方法

这时,我们没有实现任何的AOP逻辑,因此,测试的输出应该就是从DAO类和业务类中返回的简单的信息:

1
2
c.i.s.t.b.e.a.BusinessAopSpringBootTest  : In Business - Dao1
c.i.s.t.b.e.a.BusinessAopSpringBootTest : Dao1

实现@Before通知

通常来讲,当我们使用AOP来实现安全时,我们会想要拦截对方法的调用并进行检查。这可以直接通过@Before通知实现。

下面给出了一种实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Aspect
@Configuration
public class UserAccessAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
//What kind of method calls I would intercept
//execution(* PACKAGE.*.*(..))
//Weaving & Weaver
@Before("execution(* com.in28minutes.springboot.tutorial.basics.example.aop.data.*.*(..))")
public void before(JoinPoint joinPoint) {
//Advice
logger.info(" Check for user access ");
logger.info(" Allowed execution for {}", joinPoint);
}
}

备注:

  • @Aspect: 说明这是一个切面
  • @Configuration: 说明这是一个对切面的Spring Bean配置
  • @Before: 我们想要在方法执行前执行切面
  • ("execution(* com.in28minutes.springboot.tutorial.basics.example.aop.data.*.*(..))": 定义了切点。我们想要拦截com.in28minutes.springboot.tutorial.basics.example.aop.data包中的所有方法。

当我们运行单元测试时,你会看见,在执行DAO方法之前,会执行用户权限检查:

1
2
3
4
5
6
7
Check for user access 
Allowed execution for execution(String com.in28minutes.springboot.tutorial.basics.example.aop.data.Dao1.retrieveSomething())
c.i.s.t.b.e.a.BusinessAopSpringBootTest : In Business - Dao1
c.i.s.t.b.e.a.BusinessAopSpringBootTest : Dao1
Check for user access
Allowed execution for execution(String com.in28minutes.springboot.tutorial.basics.example.aop.data.Dao2.retrieveSomething())
c.i.s.t.b.e.a.BusinessAopSpringBootTest : Dao2

理解AOP术语: Pointcut, Advice, Aspect,Join Point

让我们花点时间来了解一下AOP术语:

  • 切点(Pointcut):该表达式用来定义何时方法应当被拦截。在上例中,切点为execution(* com.in28minutes.springboot.tutorial.basics. example.aop.data.*.*(..))
  • 通知(Advice):你想要做什么?一个通知是你在拦截方法时想要调用的逻辑。在上例中,通知为before(JoinPoint joinPoint)方法中的代码。
  • 切面(Aspect):定义何时拦截一个方法(Pointcut)以及做什么(Advice)和在一起成为切面
  • 连接点(Join Point):当代码开始执行,并且切点的条件满足时,通知被调用。连接点是一个通知运行的特定实例。
  • 织如(Weaver):实现AOP的框架 - AspectJ或Spring AOP

使用@After, @AfterReturning和@AfterThrowing通知

让我们现在来看看AOP提供的别的拦截选项:

  • @After: 在两种场景下执行 - 当一个方法成功执行或是抛出异常
  • @AfterReturning: 只有在方法成功执行后运行
  • @AfterThrowing: 只有在方法抛出异常后运行

让我们创建一个包含这些元素的简单的切面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Aspect
@Configuration
public class AfterAopAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@AfterReturning(value = "execution(* com.in28minutes.springboot.tutorial.basics.example.aop.business.*.*(..))",
returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
logger.info("{} returned with value {}", joinPoint, result);
}
@After(value = "execution(* com.in28minutes.springboot.tutorial.basics.example.aop.business.*.*(..))")
public void after(JoinPoint joinPoint) {
logger.info("after execution of {}", joinPoint);
}
}

执行后运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
Check for user access 
Allowed execution for execution(String com.in28minutes.springboot.tutorial.basics.example.aop.data.Dao1.retrieveSomething())
In Business - Dao1
after execution of execution(String com.in28minutes.springboot.tutorial.basics.example.aop.business.Business1.calculateSomething())
execution(String com.in28minutes.springboot.tutorial.basics.example.aop.business.Business1.calculateSomething()) returned with value Dao1
c.i.s.t.b.e.a.BusinessAopSpringBootTest : Dao1
Check for user access
Allowed execution for execution(String com.in28minutes.springboot.tutorial.basics.example.aop.data.Dao2.retrieveSomething())
after execution of execution(String com.in28minutes.springboot.tutorial.basics.example.aop.business.Business2.calculateSomething())
execution(String com.in28minutes.springboot.tutorial.basics.example.aop.business.Business2.calculateSomething()) returned with value Dao2
c.i.s.t.b.e.a.BusinessAopSpringBootTest : Dao2

可以看到,就在将值返回给调用的业务逻辑之前,after通知被执行了。

其它AOP功能:@Around和注解

能够使用AOP实现的功能之一是通过自定义注释来解析方法调用。

下面的例子展示了一个简单的TrackTiem注释:

1
2
3
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackTime {

我们可以添加一个切面来定义当添加TrackTime注解以后执行的逻辑。MethodExecutionCalculationAspect实现了一个简单的时间追踪功能。

1
2
3
4
5
6
7
8
9
10
11
12
@Aspect
@Configuration
public class MethodExecutionCalculationAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Around("@annotation(com.in28minutes.springboot.tutorial.basics.example.aop.TrackTime)")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
joinPoint.proceed();
long timeTaken = System.currentTimeMillis() - startTime;
logger.info("Time Taken by {} is {}", joinPoint, timeTaken);
}
}

备注:

  • @Around: 是一个环绕型通知。它拦截方法调用后使用joinPoint.proceed()来执行方法
  • @annotation(com.in28minutes.springboot.tutorial.basics.example.aop.TrackTime): 基于注解进行拦截的切点 - @annotation紧跟着完整的注解的名称

定义了注解和通知之后,我们可以将注解运用到想要跟踪的方法上,如下所示:

1
2
3
4
@Service
public class Business1 {
@TrackTime
public String calculateSomething(){

AOP最贱实践

AOP最佳实践之一是将所有的切点定义在一个类中。这样有利于在一个地方维护所有的切点。

1
2
3
4
5
6
public class CommonJoinPointConfig {
@Pointcut("execution(* com.in28minutes.spring.aop.springaop.data.*.*(..))")
public void dataLayerExecution() {}
@Pointcut("execution(* com.in28minutes.spring.aop.springaop.business.*.*(..))")
public void businessLayerExecution() {}
}

在定义其它切面的切入点时,可以这样调用上面的定义:

1
@Around("com.in28minutes.spring.aop.springaop.aspect.CommonJoinPointConfig.businessLayerExecution()")

完整的代码请前往GITHUB浏览

原文链接: https://dzone.com/articles/implementing-aop-with-spring-boot-and-aspectj#