定義 Introduction


在〈增添行為〉中談過,如何使用 Java 動態代理,來達到為物件增加原本未定義的行為,在 Spring AOP 中,增加行為的這個動作稱為 Introduction,第一個動作同樣地是要定義介面:

package cc.openhome.aspect;

public interface Nullable {
    void enable();
    void disable();
    boolean isEnabled();
}

接著要實作介面,例如:

package cc.openhome.aspect;

public class NullableIntroduction implements Nullable {
    private boolean enabled; 

    @Override
    public void enable() {
        enabled = true; 
    }

    @Override
    public void disable() {
        enabled = false;        
    }

    public boolean isEnabled() {
        return enabled;
    }
}

嗯?就這樣?沒錯!就這樣,要導入目標物件的行為是分開設計的,如果要將這個行為導入至 AccountDAO,可以如下設定:

package cc.openhome.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class NullableAspect {
    @DeclareParents(
        value = "cc.openhome.model.AccountDAO+",
        defaultImpl = cc.openhome.aspect.NullableIntroduction.class
    )
    public Nullable nullableIntroduction; // configuration only
}

@DeclareParents 用來宣告導入行為,value 用以設定導入的目標,+ 表示可以是其子類或實作類別,defaultImpl 表示實作的 Introduction 類別。

這麼一來,如果 accountDAO 變數參考了 Spring 注入的實例,該實例就會是實作了 Nullable 的代理物件,也就可以在 Cast 之後進行操作:

((Nullable) accountDAO).enable();

如果單純只是為了增加行為,不與目標物件上的資訊或方法互動,只寫上頭的程式碼當然沒問題,不過,若要進一步有〈增添行為〉中檢查參數值是否為 null 的功能,NullableAspect 就得多些設計,例如,結合 @Around 來進行 null 檢查:

package cc.openhome.aspect;

import java.util.Arrays;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class NullableAspect {
    @DeclareParents(
        value = "cc.openhome.model.AccountDAO+",
        defaultImpl = cc.openhome.aspect.NullableIntroduction.class
    )
    public Nullable nullableIntroduction; // configuration only

    @Around("execution(* cc.openhome.model.AccountDAO.*(..)) && this(nullable)")
    public Object around(ProceedingJoinPoint proceedingJoinPoint, Nullable nullable) throws Throwable {
        Object target = proceedingJoinPoint.getTarget();
        Object[] args = proceedingJoinPoint.getArgs();
        String methodName = proceedingJoinPoint.getSignature().getName();

        if(!nullable.isEnabled() && Arrays.stream(args).anyMatch(arg -> arg == null)) {
            throw new IllegalArgumentException(
                String.format("argument(s) of %s.%s cannot be null", 
                    target.getClass().getName(), 
                    methodName
                )
            );
        }

        return proceedingJoinPoint.proceed();
    }
}

@Around 的 Pointcut 設定中看到了 this(nullable)this 表示代理物件必須實作指定的介面,nullable 定義了方法中使用的參數名稱,從參數型態可得知要實作的介面,如此一來,代理物件就可以注入方法上定義的 Nullable nullable 參數,於是就可以檢查 nullable.isEnable() 傳回值,來決定要拋出例外,或者是執行目標物件上的方法。

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