設(shè)計模式:責(zé)任鏈模式的應(yīng)用場景及源碼應(yīng)用

一泳猬、概述

責(zé)任鏈模式(Chain of Responsibility Pattern)是將鏈中每一個節(jié)點看作是一個對象应结,每個節(jié)點處理的請求均不同奴璃,且內(nèi)部自動維護一個下一節(jié)點對象己英。當(dāng)一個請求從鏈式的首端發(fā)出時姨夹,會沿著鏈的路徑依次傳遞給每一個節(jié)點對象甜癞,直至有對象處理這個請求為止夕晓,屬于行為型模式。就像一場足球比賽悠咱,通過層層傳遞蒸辆,最終射門征炼。

責(zé)任鏈模式的應(yīng)用場景

  1. 多個對象可以處理一個請求,但具體由哪個對象處理該請求在運行時自動確定躬贡。
  2. 可動態(tài)指定一組對象處理請求谆奥,或添加新的處理者。
  3. 需要在不明確指定請求處理者的情況下拂玻,向多個處理者中的一個提交請求酸些。

設(shè)計模式只是幫助減少代碼的復(fù)雜性,讓其滿足開閉原則纺讲,提高代碼的擴展性擂仍。如果不使用同樣可以完成需求。

假設(shè)業(yè)務(wù)場景是這樣的熬甚,我們 系統(tǒng)處在一個下游服務(wù)逢渔,因為業(yè)務(wù)需求,系統(tǒng)中所使用的 基礎(chǔ)數(shù)據(jù)需要從上游中臺同步到系統(tǒng)數(shù)據(jù)庫

基礎(chǔ)數(shù)據(jù)包含了很多類型數(shù)據(jù)乡括,雖然數(shù)據(jù)在中臺會有一定驗證肃廓,但是 數(shù)據(jù)只要是人為錄入就極可能存在問題,遵從對上游系統(tǒng)不信任原則诲泌,需要對數(shù)據(jù)接收時進行一系列校驗

最初是要進行一系列驗證原則才能入庫的盲赊,后來因為工期問題只放了一套非空驗證,趁著春節(jié)期間時間還算寬裕敷扫,把這套驗證規(guī)則骨架放進去

從我們系統(tǒng)的接入數(shù)據(jù)規(guī)則而言哀蘑,個人覺得需要支持以下幾套規(guī)則

  1. 必填項校驗,如果數(shù)據(jù)無法滿足業(yè)務(wù)所必須字段要求葵第,數(shù)據(jù)一旦落入庫中就會產(chǎn)生一系列問題
  2. 非法字符校驗绘迁,因為數(shù)據(jù)如何錄入,上游系統(tǒng)的錄入規(guī)則是什么樣的我們都不清楚卒密,這一項規(guī)則也是必須的
  3. 長度校驗缀台,理由同上,如果系統(tǒng)某字段長度限制 50哮奇,但是接入來的數(shù)據(jù) 500長度膛腐,這也會造成問題

如果不使用責(zé)任鏈模式,上面說的真實同步場景面臨兩個問題

  1. 如果把上述說的代碼邏輯校驗規(guī)則寫到一起鼎俘,毫無疑問這個類或者說這個方法函數(shù)奇大無比哲身。減少代碼復(fù)雜性一貫方法是:將大塊代碼邏輯拆分成函數(shù),將大類拆分成小類贸伐,是應(yīng)對代碼復(fù)雜性的常用方法律罢。如果此時說:可以把不同的校驗規(guī)則拆分成不同的函數(shù),不同的類,這樣不也可以滿足減少代碼復(fù)雜性的要求么误辑。這樣拆分是能解決代碼復(fù)雜性,但是這樣就會面臨第二個問題
  2. 開閉原則:添加一個新的功能應(yīng)該是歌逢,在已有代碼基礎(chǔ)上擴展代碼巾钉,而非修改已有代碼。大家設(shè)想一下秘案,假設(shè)你寫了三套校驗規(guī)則砰苍,運行過一段時間,這時候領(lǐng)導(dǎo)讓加第四套阱高,是不是要在原有代碼上改動

綜上所述赚导,在合適的場景運用適合的設(shè)計模式,能夠讓代碼設(shè)計復(fù)雜性降低赤惊,變得更為健壯吼旧。朝更遠的說也能讓自己的編碼設(shè)計能力有所提高。

優(yōu)點

  1. 將請求與處理解耦未舟。
  2. 請求處理者(節(jié)點對象)只需要關(guān)注自己感興趣的請求進行處理即可圈暗,對于不感興趣的請求,轉(zhuǎn)發(fā)給下一個節(jié)點裕膀。
  3. 具備鏈式傳遞處理請求功能员串,請求發(fā)送者無需知曉鏈路結(jié)構(gòu),只需等待請求處理結(jié)果昼扛。
  4. 鏈路結(jié)構(gòu)靈活寸齐,可以通過改變鏈路的結(jié)構(gòu)動態(tài)的新增或刪減責(zé)任。
  5. 易于擴展新的請求處理類(節(jié)點)抄谐,符合開閉原則渺鹦。

缺點

  1. 責(zé)任鏈太長或者處理時間過長,會影響整體性能斯稳。
  2. 如果節(jié)點對象存在循環(huán)引用時海铆,會造成死循環(huán),導(dǎo)致系統(tǒng)崩潰挣惰。

二卧斟、入門案例

2.1 類圖

image

《Java面試+學(xué)習(xí)》全家桶合集錄

2.2 基礎(chǔ)類介紹

抽象接口RequestHandler

public interface RequestHandler {

    void doHandler(String req);
}

image.gif

抽象類BaseRequestHandler

public abstract class BaseRequestHandler implements RequestHandler {

    protected RequestHandler next;

    public void next(RequestHandler next) {
        this.next = next;
    }
}

image.gif

具體處理類AHandler

public class AHandler extends BaseRequestHandler {

    @Override
    public void doHandler(String req) {
        // 處理自己的業(yè)務(wù)邏輯
        System.out.println("A中處理自己的邏輯");
        // 傳遞給下個類(若鏈路中還有下個處理類)
        if (next != null) {
            next.doHandler(req);
        }
    }
}

image.gif

當(dāng)然還有具體的處理類B、C等等憎茂,這里不展開贅述珍语。 使用類Client

public class Client {
    public static void main(String[] args) {
        BaseRequestHandler a = new AHandler();
        BaseRequestHandler b = new BHandler();
        BaseRequestHandler c = new CHandler();
        a.next(b);
        b.next(c);
        a.doHandler("鏈路待處理的數(shù)據(jù)");
    }
}

image.gif

2.3 處理流程圖

三、應(yīng)用場景

3.1 場景舉例

場景一

金融業(yè)務(wù)其中就有一個業(yè)務(wù)場景:一筆訂單進來竖幔,會先在后臺通過初審人員進行審批板乙,初審不通過,訂單流程結(jié)束。初審?fù)ㄟ^以后募逞,會轉(zhuǎn)給終審人員進行審批蛋铆,不通過,流程結(jié)束放接;通過刺啦,流轉(zhuǎn)到下個業(yè)務(wù)場景。 對于這塊業(yè)務(wù)代碼纠脾,一套if-else干到底玛瘸。后來,技術(shù)老大CodeReview苟蹈,點名要求改掉這塊糊渊。(當(dāng)然,比較復(fù)雜的情況慧脱,還是可以用工作流來處理這個場景)渺绒。

場景二

有的公司業(yè)務(wù)會調(diào)用我們接口,將數(shù)據(jù)同步過來磷瘤。同樣芒篷,我們需要將處理好的數(shù)據(jù),傳給他們采缚。由于雙方傳輸數(shù)據(jù)都是加密傳輸针炉,所以在接受他們數(shù)據(jù)之前,需要對數(shù)據(jù)進行解密扳抽,驗簽篡帕,參數(shù)校驗等操作。同樣贸呢,我們給他們傳數(shù)據(jù)也需要進行加簽镰烧,加密操作。

具體案例

對于場景二楞陷,我們結(jié)合代碼一起探討一下怔鳖。 1、一切從注解開始固蛾,我這里自定義了一個注解@Duty,這個注解有spring的@Component注解结执,也就是標(biāo)記了這個自定義注解的類,都是交給spring的bean容器去管理艾凯。 注解中献幔,有兩個屬性:1.type,定義相同的type類型的bean趾诗,會被放到一個責(zé)任鏈集合中蜡感。2.order,同一個責(zé)任鏈集合中,bean的排序郑兴,數(shù)值越小犀斋,會放到鏈路最先的位置,優(yōu)先處理情连。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Service
public @interface Duty {
    /**
     * 標(biāo)記具體業(yè)務(wù)場景
     * @return
     */
    String type() default "";

    /**
     * 排序:數(shù)值越小闪水,排序越前
     * @return
     */
    int order() default 0;
}

image.gif

2、定義一個頂層的抽象接口IHandler蒙具,傳入2個泛型參數(shù),供后續(xù)自定義朽肥。

public interface IHandler<T, R> {
    /**
     * 抽象處理類
     * @param t
     * @return
     */
    R handle(T t);
}

image.gif

3禁筏、定義一個責(zé)任鏈bean的管理類HandleChainManager,用來存放不同業(yè)務(wù)下的責(zé)任鏈路集合衡招。在該類中篱昔,有一個Map和兩個方法。

  1. handleMap:這個map會存放責(zé)任鏈路中始腾,具體的執(zhí)行類州刽,key是注解@Duty中定義的type值,value是標(biāo)記了@Duty注解的bean集合浪箭,也就是具體的執(zhí)行類集合穗椅。
  2. setHandleMap:傳入具體執(zhí)行bean的集合,存放在map中奶栖。
  3. executeHandle:從map中找到具體的執(zhí)行bean集合匹表,并依次執(zhí)行。
public class HandleChainManager {
    /**
     * 存放責(zé)任鏈路上的具體處理類
     * k-具體業(yè)務(wù)場景名稱
     * v-具體業(yè)務(wù)場景下的責(zé)任鏈路集合
     */
    private Map<String, List<IHandler>> handleMap;

    /**
     * 存放系統(tǒng)中責(zé)任鏈具體處理類
     * @param handlerList
     */
    public void setHandleMap(List<IHandler> handlerList) {
        handleMap = handlerList
                .stream()
                .sorted(Comparator.comparingInt(h -> AnnotationUtils.findAnnotation(h.getClass(), Duty.class).order()))
                .collect(Collectors.groupingBy(handler -> AnnotationUtils.findAnnotation(handler.getClass(), Duty.class).type()));
    }

    /**
     * 執(zhí)行具體業(yè)務(wù)場景中的責(zé)任鏈集合
     * @param type 對應(yīng)@Duty注解中的type宣鄙,可以定義為具體業(yè)務(wù)場景
     * @param t 被執(zhí)行的參數(shù)
     */
    public <T, R> R executeHandle(String type, T t) {
        List<IHandler> handlers = handleMap.get(type);
        R r = null;
        if (CollectionUtil.isNotEmpty(handlers)) {
            for (IHandler<T, R> handler : handlers) {
               r = handler.handle(t);
            }
        }
        return r;
    }
}

image.gif

4袍镀、定義一個配置類PatternConfiguration,用于裝配上面的責(zé)任鏈管理器HandleChainManager冻晤。

@Configuration
public class PatternConfiguration {

    @Bean
    public HandleChainManager handlerChainExecute(List<IHandler> handlers) {
        HandleChainManager handleChainManager = new HandleChainManager();
        handleChainManager.setHandleMap(handlers);
        return handleChainManager;
    }

}

image.gif

5苇羡、具體的處理類:SignChainHandler、EncryptionChainHandler鼻弧、RequestChainHandler设江,這里我以SignChainHandler為例。 在具體處理類上標(biāo)記自定義注解@Duty温数,該類會被注入到bean容器中绣硝,實現(xiàn)IHandler接口,只需關(guān)心自己的handle方法撑刺,處理具體的業(yè)務(wù)邏輯鹉胖。

@Duty(type = BusinessConstants.REQUEST, order = 1)
public class SignChainHandler implements IHandler<String, String> {
    /**
     * 處理加簽邏輯
     * @param s
     * @return
     */
    @Override
    public String handle(String s) {
        // 加簽邏輯
        System.out.println("甲方爸爸要求加簽");
        return "加簽";
    }
}

image.gif

6、具體怎么調(diào)用?這里我寫了個測試controller直接調(diào)用甫菠,具體如下:

@RestController
@Slf4j
public class TestController {

    @Resource
    private HandleChainManager handleChainManager;

    @PostMapping("/send")
    public String duty(@RequestBody String requestBody) {
        String response = handleChainManager.executeHandle(BusinessConstants.REQUEST, requestBody);
        return response;
    }
}

image.gif

7挠铲、執(zhí)行結(jié)果,會按照注解中標(biāo)記的order依次執(zhí)行寂诱。

image
image.gif

編輯

至此拂苹,完工。又可以開心的擼代碼了痰洒,然后在具體的執(zhí)行類中瓢棒,又是一頓if-else。丘喻。脯宿。

四、源碼中運用

4.1Mybatis源碼中的運用

Mybatis中的緩存接口Cache泉粉,cache作為一個緩存接口连霉,最主要的功能就是添加和獲取緩存的功能,作為接口它有11個實現(xiàn)類嗡靡,分別實現(xiàn)不同的功能跺撼,下面是接口源碼和實現(xiàn)類。

package org.apache.ibatis.cache;

import java.util.concurrent.locks.ReadWriteLock;

public interface Cache {
    String getId();

    void putObject(Object var1, Object var2);

    Object getObject(Object var1);

    Object removeObject(Object var1);

    void clear();

    int getSize();

    default ReadWriteLock getReadWriteLock() {
        return null;
    }
}
image.gif
image
image.gif

編輯

下面讨彼,我們來看下其中一個子類LoggingCache的源碼歉井。主要看他的putObject方法和getObject方法,它在方法中直接傳給下一個實現(xiàn)去執(zhí)行点骑。這個實現(xiàn)類其實是為了在獲取緩存的時候打印緩存的命中率的酣难。

public class LoggingCache implements Cache {
    private final Log log;
    private final Cache delegate;
    protected int requests = 0;
    protected int hits = 0;

    public LoggingCache(Cache delegate) {
        this.delegate = delegate;
        this.log = LogFactory.getLog(this.getId());
    }

    // ...
    public void putObject(Object key, Object object) {
        this.delegate.putObject(key, object);
    }

    public Object getObject(Object key) {
        ++this.requests;
        Object value = this.delegate.getObject(key);
        if (value != null) {
            ++this.hits;
        }

        if (this.log.isDebugEnabled()) {
            this.log.debug("Cache Hit Ratio [" + this.getId() + "]: " + this.getHitRatio());
        }

        return value;
    }
    // ...
}

image.gif

最后,經(jīng)過Cache接口各種實現(xiàn)類的處理黑滴,最終會到達PerpetualCache這個實現(xiàn)類憨募。與之前的處理類不同的是,這個類中有一個map袁辈,在map中做存取菜谣,也就是說,最終緩存還是會保存在map中的晚缩。

public class PerpetualCache implements Cache {
    private final String id;
    private final Map<Object, Object> cache = new HashMap();

    public PerpetualCache(String id) {
        this.id = id;
    }

    // ...

    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }

    public Object getObject(Object key) {
        return this.cache.get(key);
    }
    // ...

}

image.gif

4.2spring源碼中的運用

4.2.1DispatcherServlet類

DispatcherServlet 核心方法 doDispatch尾膊。HandlerExecutionChain只是維護HandlerInterceptor的集合,可以向其中注冊相應(yīng)的攔截器荞彼,本身不直接處理請求冈敛,將請求分配給責(zé)任鏈上注冊處理器執(zhí)行,降低職責(zé)鏈本身與處理邏輯之間的耦合程度鸣皂。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        try {
            ModelAndView mv = null;
            Exception dispatchException = null;
            try {
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);
                // Determine handler for the current request.
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }
                // Determine handler adapter for the current request.
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
                // Process last-modified header, if supported by the handler.
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }
                // Actually invoke the handler.
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
                applyDefaultViewName(processedRequest, mv);
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            catch (Exception ex) {
                dispatchException = ex;
            }
            catch (Throwable err) {
                // As of 4.3, we're processing Errors thrown from handler methods as well,
                // making them available for @ExceptionHandler methods and other scenarios.
                dispatchException = new NestedServletException("Handler dispatch failed", err);
            }
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        }
        catch (Throwable err) {
            triggerAfterCompletion(processedRequest, response, mappedHandler,
                    new NestedServletException("Handler processing failed", err));
        }
        finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                // Instead of postHandle and afterCompletion
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            }
            else {
                // Clean up any resources used by a multipart request.
                if (multipartRequestParsed) {
                    cleanupMultipart(processedRequest);
                }
            }
        }
    }

image.gif

4.2.2HandlerExecutionChain類

這里分析的幾個方法抓谴,都是從DispatcherServlet類的doDispatch方法中請求的暮蹂。

  • 獲取攔截器,執(zhí)行preHandle方法
boolean applyPreHandle(HttpServletRequest request, 
                       HttpServletResponse response) throws Exception {
    HandlerInterceptor[] interceptors = this.getInterceptors();
    if (!ObjectUtils.isEmpty(interceptors)) {
        for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
            HandlerInterceptor interceptor = interceptors[i];
            if (!interceptor.preHandle(request, response, this.handler)) {
                this.triggerAfterCompletion(request, response, (Exception)null);
                return false;
            }
        }
    }
    return true;
}

image.gif
  • 在applyPreHandle方法中癌压,執(zhí)行triggerAfterCompletion方法
void triggerAfterCompletion(HttpServletRequest request, 
                            HttpServletResponse response, Exception ex) throws Exception {
    HandlerInterceptor[] interceptors = this.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 var8) {
                logger.error("HandlerInterceptor.afterCompletion threw exception", var8);
            }
        }
    }
}

image.gif
  • 獲取攔截器仰泻,執(zhí)行applyPostHandle方法
void applyPostHandle(HttpServletRequest request, 
                     HttpServletResponse response, ModelAndView mv) 
                     throws Exception {
    HandlerInterceptor[] interceptors = this.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);
        }
    }
}

image.gif

五、代碼示例

員工在OA系統(tǒng)中提交請假申請滩届,首先項目經(jīng)理處理集侯,他能審批3天以內(nèi)的假期,如果大于3天帜消,則由項目經(jīng)理則轉(zhuǎn)交給總經(jīng)理處理棠枉。接下來我們用責(zé)任鏈模式實現(xiàn)這個過程。

1泡挺、封裝請假信息實體類

public class LeaveRequest {
    private String name;    // 請假人姓名
    private int numOfDays;  // 請假天數(shù)
    private int workingAge;  //員工工齡(在公司大于2年則總經(jīng)理會審批)
   //省略get..set..
}

image.gif

2术健、抽象處理者類 Handler,維護一個nextHandler屬性粘衬,該屬性為當(dāng)前處理者的下一個處理者的引用;

聲明了抽象方法process咳促,其實在這里也用了方法模板模式:

public abstract class ApproveHandler {

    protected  ApproveHandler nextHandler;//下一個處理者(與類一致,這段代碼很重要)

    public void setNextHandler(ApproveHandler approveHandler){
        this.nextHandler=approveHandler;
    }

    public abstract void process(LeaveRequest leaveRequest); // 處理請假(這里用了模板方法模式)

}

image.gif

3稚新、項目經(jīng)理處理者,能處理小于3天的假期跪腹,而請假信息里沒有名字時褂删,審批不通過:

public class PMHandler extends ApproveHandler{

    @Override
    public void process(LeaveRequest leaveRequest) {
        //未填寫姓名的請假單不通過
        if(null != leaveRequest.getName()){
            if(leaveRequest.getNumOfDays() <= 3){
                System.out.println(leaveRequest.getName()+",你通過項目經(jīng)理審批!");
            }else {
                System.out.println("項目經(jīng)理轉(zhuǎn)交總經(jīng)理");
                if(null != nextHandler){
                    nextHandler.process(leaveRequest);
                }
            }
        }else {
            System.out.println("請假單未填寫完整,未通過項目經(jīng)理審批!");
            return;
        }
    }
}

image.gif

4、總經(jīng)理處理者冲茸,能處理大于3天的假期屯阀,且工齡超過2年才會審批通過:

public class GMHandler extends ApproveHandler{

    @Override
    public void process(LeaveRequest leaveRequest) {
        //員工在公司工齡超過2年,則審批通過
        if(leaveRequest.getWorkingAge() >=2 && leaveRequest.getNumOfDays() > 3){
            System.out.println(leaveRequest.getName()+",你通過總經(jīng)理審批!");
            if(null != nextHandler){
                nextHandler.process(leaveRequest);
            }
        }else {
            System.out.println("在公司年限不夠,長假未通過總經(jīng)理審批!");
            return;
        }
    }
}

image.gif

實例代碼完成偿凭,我們測試一下:

public class Test {
    public static void main(String[] args) {
        PMHandler pm = new PMHandler();
        GMHandler gm = new GMHandler();

        LeaveRequest leaveRequest = new LeaveRequest();
        leaveRequest.setName("張三");
        leaveRequest.setNumOfDays(4);//請假4天
        leaveRequest.setWorkingAge(3);//工齡3年

        pm.setNextHandler(gm);//設(shè)置傳遞順序
        pm.process(leaveRequest);
    }
}

image.gif

運行結(jié)果:


項目經(jīng)理轉(zhuǎn)交總經(jīng)理 張三,你通過總經(jīng)理審批!

六西篓、源碼中的典型應(yīng)用

源碼中的典型應(yīng)用:

  1. Netty 中的 Pipeline和ChannelHandler通過責(zé)任鏈設(shè)計模式來組織代碼邏輯。
  2. Spring Security 使用責(zé)任鏈模式丛忆,可以動態(tài)地添加或刪除責(zé)任(處理 request 請求)逗栽。
  3. Spring AOP 通過責(zé)任鏈模式來管理 Advisor盖袭。
  4. Dubbo Filter 過濾器鏈也是用了責(zé)任鏈模式(鏈表),可以對方法調(diào)用做一些過濾處理彼宠,譬如超時(TimeoutFilter)鳄虱,異常(ExceptionFilter),Token(TokenFilter)等凭峡。
  5. Mybatis 中的 Plugin 機制使用了責(zé)任鏈模式拙已,配置各種官方或者自定義的 Plugin,與 Filter 類似摧冀,可以在執(zhí)行 Sql 語句的時候做一些操作倍踪。
  6. Tomcat 調(diào)用 ApplicationFilterFactory過濾器鏈系宫。

spring安全框架security使用責(zé)任鏈模式

spring安全框架security使用責(zé)任鏈模式,框架使用者可以動態(tài)地添加刪除責(zé)任(處理request請求)惭适。

UML 類圖

image
image.gif

編輯

活動圖:

image
image.gif

編輯

源碼解析:currentPosition表示責(zé)任鏈的要處理請求鏈條節(jié)點的位置笙瑟,使用additionalFilters來依次處理request請求。additionalFilters中的每個Filter成員都承擔(dān)某一項具體職責(zé)癞志,并且每個Filter都會被執(zhí)行到往枷。 責(zé)任鏈條的成員執(zhí)行完自己的職責(zé)后,會回調(diào)鏈條的處理請求方法凄杯,責(zé)任鏈條會找到下一個鏈條成員來執(zhí)行職責(zé)错洁,直到鏈條尾端。

private static class VirtualFilterChain implements FilterChain {
  private final FilterChain originalChain;      //鏈條中的節(jié)點全部執(zhí)行完后戒突,處理request請求的對象
  private final List<Filter> additionalFilters; //請求實際執(zhí)行者屯碴,
  private final FirewalledRequest firewalledRequest;
  private final int size;
  private int currentPosition = 0; //鏈條移動的位置,當(dāng)currentPosition==size,到達鏈條的尾端膊存。
  private VirtualFilterChain(FirewalledRequest firewalledRequest,
    FilterChain chain, List<Filter> additionalFilters) {
   this.originalChain = chain;
   this.additionalFilters = additionalFilters;
   this.size = additionalFilters.size();
   this.firewalledRequest = firewalledRequest;
  }

  public void doFilter(ServletRequest request, ServletResponse response)
    throws IOException, ServletException {
   if (currentPosition == size) { //到達鏈條尾端
    if (logger.isDebugEnabled()) {
     logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
       + " reached end of additional filter chain; proceeding with original chain");
    }

    // Deactivate path stripping as we exit the security filter chain
    this.firewalledRequest.reset();

    originalChain.doFilter(request, response);
   }
   else {
    currentPosition++; //依次移動鏈條指針到具體節(jié)點

    Filter nextFilter = additionalFilters.get(currentPosition - 1);

    if (logger.isDebugEnabled()) {
     logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
       + " at position " + currentPosition + " of " + size
       + " in additional filter chain; firing Filter: '"
       + nextFilter.getClass().getSimpleName() + "'");
    }

    nextFilter.doFilter(request, response, this);//將鏈條本身的對象傳遞給鏈條成員
   }
  }
 }

image.gif

鏈條成員Filter會執(zhí)行chain.doFilter(request, response )方法导而,而chain是鏈條本身的引用,這樣成員就將請求又重新交給了鏈條隔崎〗褚眨看SecurityContextHolderAwareRequestFilter源碼:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
   chain.doFilter(this.requestFactory.create((HttpServletRequest) req,
         (HttpServletResponse) res), res);
}

image.gif

七:設(shè)計模式重語意

最后說一下需要達成的業(yè)務(wù)需求。將一個批量數(shù)據(jù)經(jīng)過處理器鏈的處理爵卒,返回出符合要求的數(shù)據(jù)分類

image
image.gif

編輯

定義頂級驗證接口和一系列處理器實現(xiàn)類沒什么難度虚缎,但是應(yīng)該如何進行鏈式調(diào)用呢?

這一塊代碼需要有一定 Spring 基礎(chǔ)才能理解钓株,一起來看下 VerifyHandlerChain 如何將所有處理器串成一條鏈

image
image.gif

編輯

VerifyHandlerChain 處理流程如下:

  1. 實現(xiàn)自 InitializingBean 接口实牡,在對應(yīng)實現(xiàn)方法中獲取 IOC 容器中類型為 VerifyHandler 的 Bean,也就是 EmptyVerifyHandler轴合、SexyVerifyHandler
  2. 將 VerifyHandler 類型的 Bean 添加到處理器鏈容器中
  3. 定義校驗方法 verify()创坞,對入?yún)?shù)據(jù)展開處理器鏈的全部調(diào)用,如果過程中發(fā)現(xiàn)已無需要驗證的數(shù)據(jù)受葛,直接返回

這里使用 SpringBoot 項目中默認測試類摆霉,來測試一下如何調(diào)用

@SpringBootTest
class ChainApplicationTests {

    @Autowired
    private VerifyHandlerChain verifyHandlerChain;

    @Test
    void contextLoads() {
        List<Object> verify = verifyHandlerChain.verify(Lists.newArrayList("源碼圈", "@一只阿木木"));
        System.out.println(verify);
    }
}

image.gif

這樣的話,如果客戶或者產(chǎn)品提校驗相關(guān)的需求時奔坟,我們只需要實現(xiàn) VerifyHandler 接口新建個校驗規(guī)則實現(xiàn)類就 OK 了携栋,這樣符合了設(shè)計模式的原則:滿足開閉原則,提高代碼的擴展性

熟悉之前作者寫過設(shè)計模式的文章應(yīng)該知道咳秉,強調(diào)設(shè)計模式重語意婉支,而不是具體的實現(xiàn)過程。所以澜建,你看這個咱們這個校驗代碼向挖,把責(zé)任鏈兩種模式結(jié)合了使用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蝌以,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子何之,更是在濱河造成了極大的恐慌跟畅,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件溶推,死亡現(xiàn)場離奇詭異徊件,居然都是意外死亡,警方通過查閱死者的電腦和手機蒜危,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門虱痕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人辐赞,你說我怎么就攤上這事部翘。” “怎么了响委?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵新思,是天一觀的道長。 經(jīng)常有香客問我赘风,道長表牢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任贝次,我火速辦了婚禮,結(jié)果婚禮上彰导,老公的妹妹穿的比我還像新娘蛔翅。我一直安慰自己,他們只是感情好位谋,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布山析。 她就那樣靜靜地躺著,像睡著了一般掏父。 火紅的嫁衣襯著肌膚如雪笋轨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天赊淑,我揣著相機與錄音爵政,去河邊找鬼。 笑死陶缺,一個胖子當(dāng)著我的面吹牛钾挟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播饱岸,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼掺出,長吁一口氣:“原來是場噩夢啊……” “哼徽千!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起汤锨,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤双抽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后闲礼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牍汹,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年位仁,在試婚紗的時候發(fā)現(xiàn)自己被綠了柑贞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡聂抢,死狀恐怖钧嘶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情琳疏,我是刑警寧澤有决,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站空盼,受9級特大地震影響书幕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜揽趾,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一台汇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧篱瞎,春花似錦苟呐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至澄者,卻和暖如春笆呆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背粱挡。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工赠幕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人询筏。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓劣坊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親屈留。 傳聞我的和親對象是個殘疾皇子局冰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容