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 找到以上的範例專案。