Java筆記24 - Spring開發 - 使用AOP

  • Aspect Oriented Programming: 面向切面編程

  • OOP: 面向對象編程: 數據封裝, 繼承和多態

  • 把權限作爲切面(Aspect), 把日誌, 事務也視爲切面, 某種自動化的方式, 把切面植入到核心邏輯中, 實現Proxy模式

  • 以AOP的視角來編寫上述業務:

    1. 核心邏輯: BookService;
    2. 切面邏輯:
      1. 權限檢查邏輯的Aspect;
      2. 日誌的Aspect;
      3. 事務的Aspect;
  • 讓框架把上述3個Aspect以Proxy的方式"織入"到"BookService".

AOP原理

  • 三種方式AOP的織入:

    • 編譯期: 在編譯時, 由編譯器把切面調用編譯進字節碼, 這種方式需要定義新的關鍵字並擴展編譯器.
    • 類加載器: 在目標被裝載到JVM時, 通過一個特殊的類加載器, 對目標類的字節碼重新"增強".
    • 運行期: 目標對象和切面都是普通的Java類, 通過JVM的動態代理功能或者第三方庫實現運行期動態織入.
  • Spring的AOP實現是基於JVM的動態代理. 由於JVM的動態代理要求必須實現接口. 如果一個普通類沒有業務接口, 就需要通過CGLB或者javassist第三方庫實現.

裝配AOP

  • 主要概念
    • Aspect: 切面, 橫跨多個核心邏輯的功能, 或稱爲系統關注點
    • Joinpoint: 連接點, 定義在應用程序的何處插入切面的執行
    • Pointcut: 切入點: 一組連接點的集合
    • Advice: 增強, 特定連接點上執行的動作
    • Introduction: 引介, 爲一個已有的Java對象動態的增加新的接口
    • Weaving: 織入, 將切面整合到程序的執行流中
    • Interceptror: 攔截器, 一種實現增強的方式
    • Target Object: 目標對象, 真正執行業務的核心邏輯對象
    • AOP Proxy: AOP代理, 客戶端持有的增強後的對象引用
@Aspect
@Component
public class LoggingAspect {
  // 在UserService的每個方法前執行
  @Before("execution(public * com.zhangrh.spring.service.UserService.*(..))")
  public void doAccessCheck() {
    System.err.println("[Before] do access check...");
  }

  // 在MailService的每個方法前後執行
  @Around("execution(public * com.zhangrh.spring.service.MailService.*(..))")
  public Object doLooging(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("[Around] start " + pjp.getSignature());
    Object retVal = pjp.proceed();
    System.out.println("[Around] done" + pjp.getSignature());
    return retVal;
  }
}
  • Spring容器啓動時, 自動爲我們創建的注入了Aspect的子類, 取代了原始的UserService.

    1. 使用AcpectJ解析註解
    2. 通過CGLIB實現代理類
  • 使用AOP:

    1. 定義執行方法, 並在方法上通過AspectJ的註解告訴Spring應該在何處調用此方法
    2. 標記@Component@Aspect
    3. @Configuration類上標記@EnableAspectJAutoProxy
  • 攔截器類型:

    • @Before: 先執行攔截代碼, 再執行目標代碼. 如果攔截器拋出異常, 那目標代碼就不執行了
    • @After: 先執行目標代碼, 再執行攔截器代碼, 無論目標是否異常, 攔截器代碼都會執行
    • @AfterRunning: 只有當目標代碼正常返回時, 纔會執行
    • @AfterThrowing: 只有當目標代碼拋出異常時, 纔會執行
    • @Around: 能完全控制目標代碼是否執行, 並可在執行前後, 拋出異常前後, 任意攔截代碼, 是上面的合集.

使用註解裝配AOP

@Component
@Transactional // 所有的public都被安排
public class UserService {
  // 有事務
  @Transactional
  public User createUser(String name) {
    // ...
  }

  // 無事務
  public boolean isValidName(String name) {
    // ...
  }
}
  • 使用@Around("execution...")殺傷力太大
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MetricTime {
    String value();
}
@Component
@Aspect
public class MetricAspect {
  @Around("@annotation(metricTime)")
  public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {
    String name = metricTime.value();
    long start = System.currentTimeMillis();
    try {
      return joinPoint.proceed();
    } finally {
      long t = System.currentTimeMillis() - start;
      System.out.println("[Metrics] " + name + ": " + t + "ms");
    }
  }
}

  @MetricTime("login")
  public User Login(String email, String password) {
    // ...
  }

AOP避坑指南

  • 無論使用AspectJ語法, 還是配合Annotation, AOP的本質都是一個代理模式
  • 使得調用方無感知的調用指定方法
  • Spring通過CGLB創建的代理類, 不會初始化類自身繼承的任何成員變量, 包括final類型的成員變量
  • AOP避坑指南:
    1. 訪問被注入的Bean時, 總是調用方法而非直接訪問字段;
    2. 編寫Bean時, 如果可能被代理, 就不要編寫public final方法
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章