責任鏈模式在 Spring 中的應用

zzzzbw發表於2022-03-19

前言

最近工作中有個業務場景非常適合使用責任鏈模式,且有好幾個地方都能使用到。為了寫一個比較通用且完善的責任鏈,閱讀了 Spring 框架中一些責任鏈的實現作為參考。

Spring 中責任鏈模式的應用

責任鏈的應用非常廣泛,在 Spring 中都有很多應用,這裡分析兩個非常常用的功能是如何實現的。

Spring Web 中的 HandlerInterceptor

HandlerInterceptor介面在web開發中非常常用,裡面有preHandle()postHandle()afterCompletion()三個方法,實現這三個方法可以分別在呼叫"Controller"方法之前,呼叫"Controller"方法之後渲染"ModelAndView"之前,以及渲染"ModelAndView"之後執行。

public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable Exception ex) throws Exception {
    }

}

HandlerInterceptor在責任鏈中充當處理者的角色,通過HandlerExecutionChain進行責任鏈呼叫。

public class HandlerExecutionChain {

    ...

    @Nullable
    private HandlerInterceptor[] interceptors;

    private int interceptorIndex = -1;

    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HandlerInterceptor[] interceptors = getInterceptors();
        if (!ObjectUtils.isEmpty(interceptors)) {
            for (int i = 0; i < interceptors.length; i++) {
                HandlerInterceptor interceptor = interceptors[i];
                if (!interceptor.preHandle(request, response, this.handler)) {
                    triggerAfterCompletion(request, response, null);
                    return false;
                }
                this.interceptorIndex = i;
            }
        }
        return true;
    }

    void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
            throws Exception {

        HandlerInterceptor[] interceptors = getInterceptors();
        if (!ObjectUtils.isEmpty(interceptors)) {
            for (int i = interceptors.length - 1; i >= 0; i--) {
                HandlerInterceptor interceptor = interceptors[i];
                interceptor.postHandle(request, response, this.handler, mv);
            }
        }
    }

    void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)
            throws Exception {

        HandlerInterceptor[] interceptors = getInterceptors();
        if (!ObjectUtils.isEmpty(interceptors)) {
            for (int i = this.interceptorIndex; i >= 0; i--) {
                HandlerInterceptor interceptor = interceptors[i];
                try {
                    interceptor.afterCompletion(request, response, this.handler, ex);
                }
                catch (Throwable ex2) {
                    logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
                }
            }
        }
    }
}

呼叫的方式非常簡單,即通過陣列儲存註冊在Spring中的HandlerInterceptor,然後通過interceptorIndex作為指標去遍歷責任鏈陣列按順序呼叫處理者。

HandlerExecutionChain

Spring AOP

Spring AOP實現的更加靈活,可以實現前置(@Before)、後置(@After)、環繞(@Around)等多種切入時機。目前Spring AOP 的動態代理有兩種實現:JdkDynamicAopProxyCglibAopProxy。這裡就以CglibAopProxy的實現流程看一下責任鏈的使用。

首先Spring會根據配置等決定對一個物件進行代理,JdkDynamicAopProxy實現了InvocationHandler介面並實現invoke()方法。熟悉JDK動態代理的都知道通過代理物件呼叫方法時,會進入到InvocationHandler物件的invoke()方法,所以我們直接從JdkDynamicAopProxy的這個方法開始:

final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        ...
        try {
            // 1. 校驗及跳過一些無需代理的方法
            ... 
            
            // 2. 獲取目標方法所有的攔截器
            List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

            
            if (chain.isEmpty()) {
                // 沒有攔截器則直接反射呼叫目標方法
                Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
                retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
            }
            else {
                // 3 生成動態代理的責任鏈
                MethodInvocation invocation =
                        new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
                // 4 執行責任鏈
                retVal = invocation.proceed();
            }

            // 5. 組裝處理返回值
            ...
            return retVal;
        }
        ...
    }
}

這個方法整體流程比較長的,我只展示了和責任鏈有關的部分。步驟2通過獲取和目標代理方法相關的所有AdviceAdvice可以說是攔截器在Spring中的封裝,即我們編寫的AOP方法的封裝。接著如果有攔截器則通過步驟3生成一個ReflectiveMethodInvocation,並且執行該責任鏈。

再看看ReflectiveMethodInvocationproceed()方法是怎麼樣的:

public class ReflectiveMethodInvocation implements ProxyMethodInvocation, Cloneable {

    protected ReflectiveMethodInvocation(
            Object proxy, @Nullable Object target, Method method, @Nullable Object[] arguments,
            @Nullable Class<?> targetClass, List<Object> interceptorsAndDynamicMethodMatchers) {

        this.proxy = proxy;
        this.target = target;
        this.targetClass = targetClass;
        this.method = BridgeMethodResolver.findBridgedMethod(method);
        this.arguments = AopProxyUtils.adaptArgumentsIfNecessary(method, arguments);
        this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers;
    }

    public Object proceed() throws Throwable {
        
        if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
            // 1. 如果當前Index和interceptorsAndDynamicMethodMatchers的攔截器數量相同,說明責任鏈呼叫結束,直接反射呼叫目標代理的方法
            return invokeJoinpoint();
        }

        // 2. 獲取當前的攔截器
        Object interceptorOrInterceptionAdvice =
                this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
        if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
            // InterceptorAndDynamicMethodMatcher 型別的攔截器,校驗MethodMatcher是否匹配

            InterceptorAndDynamicMethodMatcher dm =
                    (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
            Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
            // 3. 目標代理方法和當前攔截器是否matches
            if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
                // 4. 執行當前攔截器
                return dm.interceptor.invoke(this);
            }
            else {
                // 5. 不匹配則遞迴呼叫下一個攔截器
                return proceed();
            }
        }
        else {
            // 執行當前攔截器
            return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
        }
    }
}
  1. 首先ReflectiveMethodInvocation在構造方法中儲存JdkDynamicAopProxy傳入的攔截器列表(chain)到interceptorsAndDynamicMethodMatchers
  1. 接著在proceed()方法中,先判定當前currentInterceptorIndex指標是否到攔截器列表size,如果到了說明攔截器都執行過了,就去呼叫目標代理方法。
  2. 否則獲取當前攔截器,通常型別為InterceptorAndDynamicMethodMatcherInterceptorAndDynamicMethodMatcher裡面只有兩個屬性,MethodMatcherMethodInterceptor,前者用於判定攔截器是否需要匹配目標代理方法,後者就是攔截器本身。
  3. 如果當前InterceptorAndDynamicMethodMatcher匹配目標代理方法,則呼叫當前攔截器,否則直接再次呼叫當前proceed()形成遞迴。

ReflectiveMethodInvocation就是責任鏈中的“鏈條”,處理者是InterceptorAndDynamicMethodMatcher,更準確的說是InterceptorAndDynamicMethodMatcher裡的MethodInterceptor實現類。這裡的MethodInterceptor在Spring中被封裝成各種Advice,例如環繞(@Around)的切面會被封裝成AspectJAroundAdvice

AspectJAroundAdvice的詳細程式碼先不看,先寫一個最基本的AOP切面

@Slf4j
@Aspect
@Component
public class LogAop {
    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    public void logPointCut() {
    }

    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        log.info("LogAop start");
        Object result = point.proceed();
        log.info("LogAop end");
        return result;
    }
}

@Pointcut裡的匹配規則會被封裝到InterceptorAndDynamicMethodMatcherMethodMatcher作為匹配規則,而@Around註解的方法就會被封裝到MethodInterceptor,這裡即是AspectJAroundAdvice。在AspectJAroundAdvice裡會封裝ProceedingJoinPoint等引數,然後作為入參反射呼叫對應的AOP方法。

public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedenceInformation, Serializable {
    protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
        // 1. args AOP方法的入參,通常為ProceedingJoinPoint
        Object[] actualArgs = args;
        if (this.aspectJAdviceMethod.getParameterCount() == 0) {
            actualArgs = null;
        }
        try {
            ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
            // 2. this.aspectInstanceFactory.getAspectInstance()獲取AOP切面的例項
            return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
        }
        catch (IllegalArgumentException ex) {
            throw new AopInvocationException("Mismatch on arguments to advice method [" +
                    this.aspectJAdviceMethod + "]; pointcut expression [" +
                    this.pointcut.getPointcutExpression() + "]", ex);
        }
        catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }
}

入參ProceedingJoinPoint,裡面包含了目標代理方法的一些資訊,更重要的是我們會呼叫該物件的proceed()方法來嘗試呼叫目標代理方法。即我們可以在AOP切面呼叫proceed()方法前後編寫我們期望在目標代理方法前後要執行的程式碼。
如在LogAop裡會在目標代理方法執行前列印LogAop start,執行後列印LogAop end

ProceedingJoinPoint.proceed()又是怎麼做的呢,這裡看下Spring的實現類MethodInvocationProceedingJoinPoint的程式碼。

public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint, JoinPoint.StaticPart {
    @Override
    public Object proceed() throws Throwable {
        return this.methodInvocation.invocableClone().proceed();
    }
}

很簡單的一段程式碼,克隆methodInvocation並呼叫其proceed()方法。這裡的methodInvocation就是一開始提到的ReflectiveMethodInvocation,所以繞了一大圈,實際上形成了ReflectiveMethodInvocation.proceed()的遞迴。

ReflectiveMethodInvocation
![]()

兩種實現總結

Spring Web 中的 HandlerInterceptor採用陣列的順序遍歷模式來控制責任鏈鏈條的推動,這種模式可以讓處理者無需手動控制鏈條,每個處理者之間也不會相互收到干擾。但是同時處理者就無法打斷責任鏈,必須處理完所有的處理者才會結束。且處理者的呼叫時機也是由鏈來決定,相對沒那麼靈活。

Spring AOP則採用遞迴的方式,處理者要在自己的功能裡顯示的呼叫來推動鏈條,這樣相對比較靈活,可以自己決定推進時機,甚至可以打斷責任鏈。這樣的話對其他處理者來說有點不可控,有時責任鏈被其他處理者打斷而導致自己未被呼叫到,增加了除錯的困難。

這兩種責任鏈的鏈條都使用Index變數來控制鏈條推動,這就意味著無法“共享”一個鏈條,每次使用責任鏈都要重新生成,用完又需要銷燬,比較消耗處理器資源。

設計通用責任鏈

在前面學習了其他責任鏈的實現,現在自己實現一個通用的的責任鏈。

先寫一個基礎的處理器介面:

/**
 * 通用責任鏈的 Handler
 * <p>
 * 每個業務宣告一個該介面的子介面或者抽象類,再基於該介面實現對應的業務 Handler。
 * 這樣 BaseHandlerChain 可以直接注入到對應的 Handler List
 */
public interface BaseHandler<Param, Result> {

    @NonNull
    HandleResult<Result> doHandle(Param param);

    default boolean isHandler(Param param) {
        return true;
    }
}

處理器介面有兩個方法,isHandler()用於判定該處理器是否需要執行,預設為truedoHandle()方法執行處理器邏輯,且返回HandleResult用於返回處理結果,並判定是否繼續執行下一個處理器。

@Getter
public class HandleResult<R> {
    private final R data;

    private final boolean next;

    private HandleResult(R r, boolean next) {
        this.data = r;
        this.next = next;
    }

    public static <R> HandleResult<R> doNextResult() {
        return new HandleResult<>(null, true);
    }

    public static <R> HandleResult<R> doCurrentResult(R r) {
        return new HandleResult<>(r, false);
    }
}

最後編寫責任鏈控制器的程式碼:

/**
 * 通用責任鏈模式
 * <p>
 * 使用方法:
 * <p>
 * 1. 建立一個對應業務的責任鏈控制器, 繼承 BaseHandlerChain,
 * 如: {@code MyHandlerChain extends BaseHandlerChain<MyHandler, MyParam, MyResult>}
 * <p>
 * 2. 建立一個對應業務的責任鏈處理器 Handler,繼承 BaseHandler,
 * 如: {@code MyHandler extends BaseHandler<MyParam, MyResult>}
 * <p>
 * 3. 編寫業務需要的處理器 Handler 實現 MyHandler 介面的 doHandle 方法。推薦把控制器和處理器都交給 Spring 控制,可以直接注入。
 */
public class BaseHandlerChain<Handler extends BaseHandler<Param, Result>, Param, Result> {

    @Getter
    private final List<Handler> handlerList;


    public BaseHandlerChain(List<Handler> handlerList) {
        this.handlerList = handlerList;
    }

    public Result handleChain(Param param) {
        for (Handler handler : handlerList) {
            if (!handler.isHandler(param)) {
                continue;
            }
            HandleResult<Result> result = handler.doHandle(param);
            if (result.isNext()) {
                continue;
            }
            return result.getData();
        }

        return null;
    }
}

這樣就責任鏈就完成了,現在來做個簡單的使用演示

/**
 * 基於業務的基礎處理器介面
 */
public interface MyHandler extends BaseHandler<String, String> {
}

/**
 * 基於業務的控制器介面
 */
@Service
public class MyHandlerChain extends BaseHandlerChain<MyHandler, String, String> {

    @Autowired
    public MyHandlerChain(List<MyHandler> myHandlers) {
        super(myHandlers);
    }
}

/**
 * 處理器1
 */
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class MyLogHandler implements MyHandler {

    @Override
    public @NonNull HandleResult<String> doHandle(String param) {
        log.info("MyLogHandler hello {} !", param);
        return HandleResult.doNextResult();
    }
}

/**
 * 處理器2
 */
@Slf4j
@Component
public class MyDefaultHandler implements MyHandler {

    @Override
    public @NonNull HandleResult<String> doHandle(String param) {
        log.info("param is {}", param);
        return HandleResult.doCurrentResult("MyDefaultHandler");
    }
}

/**
 * 單元測試
 */
@Slf4j
@SpringBootTest
public class BaseHandlerChainTests {

    @Autowired
    private MyHandlerChain handlerChain;

    @Test
    public void handleChain() {
        String result = handlerChain.handleChain("zzzzbw");
        log.info("handleChain result: {}", result);
    }
}

最後日誌會輸出如下:

INFO 6716 --- [           main] c.z.s.demo.chain.handler.MyLogHandler    : MyLogHandler hello zzzzbw !
INFO 6716 --- [           main] c.z.s.d.chain.handler.MyDefaultHandler   : param is zzzzbw
INFO 6716 --- [           main] c.z.s.demo.BaseHandlerChainTests         : handleChain result: MyDefaultHandler

參考

責任鏈模式實現的三種方式

責任鏈模式的兩種實現

責任鏈的三種實現方式比較

責任鏈的2種實現方式,你更pick哪一種

責任鏈模式(Chain of Responsibility Pattern)。
這一次搞懂Spring代理建立及AOP鏈式呼叫過程 - 雲+社群 - 騰訊雲 (tencent.com)


原文地址: 責任鏈模式在 Spring 中的應用

相關文章