在 AOP 之前


Spring 支援 AOP(Aspect-Oriented Programming),AOP 是有不少的術語,不過說穿了,第一步還是在找出關切的任務,將這些任務分離開來,令這些任務可以獨立地進行設計,說穿了,出發點仍是關切點分離(Separation of concerns)這古老的概念。

只不過有些關注與程式主流程一致,易於識別與分離為獨立的程式庫或框架,像是 Dependency Injection、MVC 等,而有些關注則是與主要流程橫切,容易破碎地出現在各個主要流程之中,例如效能量測、日誌、資源存取控制等,面對這些與主流程橫切的關注,可以將之設計為獨立可重用的元件,便於切入主要流程,也便於抽離主要流程,而這些切入與抽離,都不用修改與主要流程相關的元件。

這是什麼意思呢?若你接觸過 Servlet 程式設計,必然學過過濾器(Filter),正如同〈關於過濾器〉中談到的,過濾器是個可重用的元件,可以轉換對資源的請求,也可以轉換回應的內容(過濾器並不負責建立回應內容),過濾器通常作為一個服務加入至應用程式之中,即時地為應用程式增加功能,但不用修改原有的應用程式,在不需要使用服務時,可以直接將過濾器從應用程式抽離,而不用修改原應用程式。

過濾器概念上就像個濾網,需要時在某些資源存取前、回應前加上濾網,不需要時可直接將濾網拿掉,好的過濾器設計無論加上或拿掉,基本上都不應影響既有的應用程式流程。

過濾器

若以橫切主流程的任務的角度來看,過濾器可算是 AOP 概念的簡單實現,將關注的事抽離出來以便重用,只不過這個該抽離出來的關注,不是與主要流程有著一致的方向,而是橫切入主要流程。

當然,Servlet 的過濾器是攔截過濾器(Intercepting Filter)模式之實現,粒度大許多,切入主流程的點,主要就是在 Servlet 處理請求前後,如果想要橫切的點是某個方法執行前後呢?例如,〈使用 Spring DI〉中的 AccountDAOJdbcImpl 呼叫某方法前,都要做些簡單的日誌該怎麼做?

修改 AccountDAOJdbcImpl 的原始碼,在每個方法開頭與結尾都加入日誌程式碼,當然是不切實際的,一個簡單的想法是實作一個 AccountDAOLoggingProxy

package cc.openhome.model;

import java.util.Optional;
import java.util.logging.Logger;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class AccountDAOLoggingProxy implements AccountDAO {
    private AccountDAO target;
    private Logger logger;

    @Autowired
    public AccountDAOLoggingProxy(@Qualifier("accountDAOJdbcImpl") AccountDAO target) {
        this.target = target;
        logger = Logger.getLogger(target.getClass().getName());
    }

    @Override
    public void createAccount(Account acct) {
        logger.info(String.format("%s.createAccount(%s)",
                target.getClass().getName(), acct));    
        target.createAccount(acct);
    }

    @Override
    public Optional<Account> accountByUsername(String name) {
        logger.info(String.format("%s.accountByUsername(%s)", 
                target.getClass().getName(), name));
        return target.accountByUsername(name);
    }

    @Override
    public Optional<Account> accountByEmail(String email) {
        logger.info(String.format("%s.accountByEmail(%s)",
                 target.getClass().getName(), email));
        return target.accountByEmail(email);
    }

    @Override
    public void activateAccount(Account acct) {
        logger.info(String.format("%s.activateAccount(%s)",
                target.getClass().getName(), acct));
        target.activateAccount(acct);
    }

    @Override
    public void updatePasswordSalt(String name, String password, String salt) {
        logger.info(String.format("%s.updatePasswordSalt(%s, %s, %s)",
                target.getClass().getName(), name, password, salt));
        target.updatePasswordSalt(name, password, salt);
    }   
}

AccountDAOLoggingProxy 實作了 AccountDAO,建構時必須注入 AccountDAOJdbcImpl 實例,在真正呼叫某方法之前,進行了日誌的動作,接著,UserService 改注入 AccountDAOLoggingProxy

...略
@Component
public class UserService {
    private final AccountDAO acctDAO;
    private final MessageDAO messageDAO;

    @Autowired
    public UserService(@Qualifier("accountDAOLoggingProxy") AccountDAO acctDAO, MessageDAO messageDAO) {
        this.acctDAO = acctDAO;
        this.messageDAO = messageDAO;
    }
    ...略
}

這麼一來,在不修改 AccountDAOJdbcImpl 的情況下,若有呼叫到 AccountDAOJdbcImpl 的方法前,都會進行日誌。

不過 AccountDAOLoggingProxy 只能用於 AccountDAO 物件,而且如果要代理的方法很多,每個方法都要實作也是個麻煩,雖說如此,也算是實現了將橫切入主要流程的關切分離的概念,在面對關切點分離時,若能辨識出這類的需求,彈性的問題是可以解決的,例如,透過 Java 的 Reflection 等機制,來實現更有彈性的動態代理。

你可以在 BeforeAOP 找到以上的範例專案。