對於某些語言來說,特別是動態定型語言,要做橫切入主流程的關切點分離,並不是件難事,因此不常聽到 AOP 這類概念,在這些語言的生態圈中被討論,然而,Java 是門蠻嚴謹的語言,要做到 AOP 需要一些技巧與技術,而 Spring AOP 則是提供了這類技術的一個框架,希望能省去 Java 開發者在進行 AOP 時的一些麻煩。
在使用 Spring AOP 設計 Aspect 時,可以使用 @Aspect
等標註,為了能使用這類標註,可以在 build.gradle 中設定 spring-aspects 相依:
apply plugin: 'java-library'
repositories {
jcenter()
}
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'org.springframework:spring-test:5.1.2.RELEASE'
compile 'com.h2database:h2:1.4.196'
compile 'org.springframework:spring-context:5.1.2.RELEASE'
compile 'org.springframework:spring-jdbc:5.1.2.RELEASE'
compile 'org.springframework:spring-aspects:5.1.2.RELEASE'
}
接著,可以在元件上加註 @Aspect
,表示這是一個 Aspect:
package cc.openhome.aspect;
import java.util.Arrays;
import java.util.logging.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class LoggingAspect {
@Before("execution(* cc.openhome.model.AccountDAO.*(..))")
public void before(JoinPoint joinPoint) {
Object target = joinPoint.getTarget();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
Logger.getLogger(target.getClass().getName())
.info(String.format("%s.%s(%s)",
target.getClass().getName(), methodName, Arrays.toString(args)));
}
}
使用 @Aspect
標註的 Aspect 元件,不用實作介面或繼承類別;如〈關於 AspectJ〉中談到的,橫切進主要流程的服務稱為 Advice,由於現在是希望某方法被呼叫前執行日誌,Spring AOP 中可以使用 @Before
來標註方法,表示這是個 Before Advice,一個 Aspect 元件,可以有多個 Advice,這之後還會看到。
Spring AOP 與其他純 Java 實現的 AOP 框架,基本上都是在執行時期進行織入,Advice 會在程式執行時的某些點上接入,這些點稱為 Joint Point,用來定義是否符合 Join Point 的斷言稱為 Pointcut,你可以在 @Before
這類標註上使用 Pointcut 表示式(pointcut expression)來定義是否符合 Join Point 的斷言。
在執行 Advice 時,Join Point 的資訊會封裝為 JoinPoint
實例,作為 Advice 的引數傳入,可以透過它來取得方法簽署、引數等資訊,若不需要 JoinPoint
實例,Advice 上可以不宣告該參數。
以上面的例子來說,execution(* cc.openhome.model.AccountDAO.*(..))
表示,會在 AccountDAO
的任何方法執行時進行日誌,就目前來說你可以先知道的是,第一個 *
表示任何傳回型態,第二個 *
表示所有方法,而 ..
表示任何引數,之後會再詳細一些討論 Pointcut 表示式。
而在 AppConfig
上,要加上 @EnableAspectJAutoProxy
,這樣才會啟用自動代理:
package cc.openhome;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class AppConfig {
@Bean(destroyMethod="shutdown")
public DataSource dataSource(){
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:testData.sql")
.build();
}
}
其他就沒什麼特別的設定了,如同先前談 Bean 管理的方式設定就好了,當某個 Bean 會使用到 AccountDAO
的方法時,就可以看到日誌訊息,相較於〈動態代理〉的範例,或者是〈關於 AspectJ〉中得自行動手編譯等操作,實作上確實是簡單了許多。
你可以在 Spring AOP 找到以上的範例專案。