Contents

Intro to AOP & AspectJ

前言

一直都有聽說 AOP ( Aspect-Oriented Programming ) 的概念,但是一直沒有真正的動手實作。

利用這次寫這篇文章的機會,簡單整理一下 AOP 相關的概念以及實作。

( Fact: 在跟 2022年 say goodbye 之前,補一下部落格發文進度 😏 )


Photo by Yaniv Knobel in Unsplash

Photo by Yaniv Knobel in Unsplash


本文

相關名詞介紹

AOP ( Aspect-Oriented Programming )

AOP's definition
剖面導向程式設計 (AOP) 是電腦科學中的一種程式設計泛型,旨在將橫切關注點與業務主體進行進一步分離,以提高程式碼的模組化程度 — wiki

再用下面這張圖來說明:

Photo derived from Konstantin’s article

Photo derived from Konstantin’s article


AOP 設計利用橫切的方式( cross-cutting ) 來加入額外邏輯。

像是圖中的「日誌 (Logging)」以及「安全驗證 (Security)」,這樣一來就可以避免在各個既有模組中一一加入邏輯,處理起來複雜、耗時且難以維護。

Join point

切面設計的切入點,會需要搭配 Pointcut 。

Pointcut

一種特殊的通用表示式 ( Regular Expression ) ,被用來定義是在哪些情況下應該要切入,並且依附某個切入點 ( Join Point )。

Advice

進入切入點之後要執行的邏輯。

Aspect

為收集各個地方的 cross-cutting concerns 之後獨立且可以重用的物件,通常一個 Aspect 會專注在一種邏輯上,像是日誌等等。

以上名詞之間的關係如下圖:


Photo derived from openhom.cc ( 感謝良葛格對台灣技術的貢獻,本人從中受益良多,R.I.P. – 2022/11 )

Photo derived from openhom.cc
( 感謝良葛格對台灣技術的貢獻,本人從中受益良多,R.I.P. – 2022/11 )


使用 AspectJ

What is AspectJ ?
AspectJ 是一種實現 AOP 的 Java 擴充套件 — wiki

安裝

For Maven

1
2
3
4
5
6
7
8
9
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.9.1</version>

    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.9.1</version>
</dependency>

情境一:加入日誌 ( Logging )

💡 假如我們想要針對 org.sample.entity 目錄底下所有的物件,在呼叫 getters & setters 的前後都紀錄我們客製化的日誌。


目錄底下的物件Car

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package org.example.entity;

public class Car {
  private String name;

  public Car(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

新增一個EntityAspect來處理這個橫切邏輯。

 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
package org.example.aop;

import java.util.logging.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class EntityAspect {
  private final Logger logger = Logger.getLogger(EntityAspect.class.getName());

  @Pointcut("execution(* org.example.entity.*.get*()) "
      + "|| execution(* org.example.entity.*.set*(..))")
  public void getterAndSetterJointPoint() {}

  @Before("getterAndSetterJointPoint()")
  public void beforeGetterAndSetterAdvice() {
    logger.info("[EntityAspect] --> Before advice executed!");
  }

  @After("getterAndSetterJointPoint()")
  public void afterGetterAndSetterAdvice() {
    logger.info("[EntityAspect] --> After advice executed!");
  }

}

@Pointcut中的表示式包含 jointpoint signature ( 範例中為 execution) 以及 regular expression ( 範例中為 * org.example.entity.*.get*())。

其中還可以搭配邏輯運算元 ( 範例中為 || ),更多的規則可以參考連結

@Before & @After 指定 Advice 的邏輯是在 Join point 之前還是之後執行,裡面填入要綁定的 Pointcut method ( 範例中為getterAndSetterJointPoint() )。


在測試中看看結果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import org.example.entity.Car;
import org.junit.jupiter.api.Test;

class RawTest {
  @Test
  void testGetterAdnSetter() {
    Car car = new Car("SpeedWagon");
    System.out.println("The car's name: " + car.getName());
  }

  // Output:
  // Nov 28, 2022 3:06:22 PM org.example.aop.EntityAspect beforeGetterAndSetterAdvice
  // INFO: [EntityAspect] --> Before advice executed!
  // Nov 28, 2022 3:06:22 PM org.example.aop.EntityAspect afterGetterAndSetterAdvice
  // INFO: [EntityAspect] --> After advice executed!
  // Car's name: SpeedWagon
}

可以看到在呼叫 getter 的前 ( Before ) 後 ( After ) 成功橫切插入我們要執行的邏輯。


情境二:檢查物件 ( Validation )

💡 假如我們想要針對 org.sample.entity 目錄底下所有的物件在使用 new 來產生 instance 的時候,檢查裡面的欄位是否符合我們要的邏輯。


目錄底下的物件Triangle

 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
package org.example.entity;

import java.util.Optional;

public class Triangle {
  private float[] sides;

  public Triangle(float[] sides) {
    this.sides = sides;
  }

  // To validate if the triangle is a valid one.
  public Optional<String> validateTheTriangle() {
    if (sides.length != 3) return Optional.of("Sides number must be 3.");

    for (float side : sides) {
      if (side <= 0) return Optional.of("Sides must be larger than 0.");
      if (sides[0] + sides[1] <= sides[2]
          || sides[1] + sides[2] <= sides[0]
          || sides[0] + sides[2] <= sides[1])
        return Optional.of("The sides cannot form a triangle.");
    }

    return Optional.empty();
  }
}

新增一個TriangleAspect來處理這個橫切邏輯。

 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
package org.example.aop;

import java.util.Optional;
import java.util.logging.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.example.entity.Triangle;

@Aspect
public class TriangleAspect {
  private final Logger logger = Logger.getLogger(TriangleAspect.class.getName());

  @Pointcut("execution(org.example.entity.*.new(..))")
  public void triangleJointPoint() {  }

  @After("triangleJointPoint()")
  public void afterCreatingTriangleAdvice(JoinPoint joinPoint) {
    Triangle instance = (Triangle) joinPoint.getTarget();
    Optional<String> op = instance.validateTheTriangle();
    if (op.isPresent()) {
      logger.info("Invalid due to: " + op.get());
    } else {
      logger.info("Valid!");
    }
  }
}

在測試中看看結果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import org.example.entity.Triangle;
import org.junit.jupiter.api.Test;

class RawTest {
  @Test
  void testValidTriangle() {
    Triangle triangle = new Triangle(new float[]{12F, 13F, 14F});
  }
  // Output:
  // Nov 28, 2022 4:50:48 PM org.example.aop.TriangleAspect afterCreatingTriangleAdvice
  // INFO: Valid! 

  @Test
  void testInValidTriangle() {
    Triangle triangle = new Triangle(new float[]{50F, 13F, 14F});
  }
  // Output:
  // Nov 28, 2022 4:48:21 PM org.example.aop.TriangleAspect afterCreatingTriangleAdvice
  // INFO: Invalid due to: The sides cannot form a triangle.
}

可以看到在呼叫 new 的時候,我們成功的執行了在 Triangle 裡面的檢查 (validateTheTriangle),這樣一來就可以利用 AOP 的方式來判斷三角形是不是合理的。


總結

  • 剖面導向程式設計 AOP ( Aspect-Oriented Programming ) 是一種設計方式,旨在將橫切關注點與業務主體分離,提高分離程度。

  • AOP 相關的概念:Join point, Pointcut, Advice & Aspect。

  • AspectJ 是一種實現 AOP 的 Java 擴充套件。相關的套件還有 Spring AOP

  • AOP 可以利用的情境有 Tracing, Profiling & Logging, Pre- and Post- conditions 等等,可以參考官方列出的 情境


參考