dubbo中的Filter鏈原理及應用

filter在dubbo中的應用非常廣泛,它可以對服務(wù)端躏结、消費端的調(diào)用過程進行攔截却盘,從而對dubbo進行功能上的擴展,我們所熟知的RpcContext就用到了filter媳拴。本文主要嘗試從以下3個方面來簡單介紹一下dubbo中的filter:
1.filter鏈原理
2.自定義filter
3.使用filter透傳traceId

1.filter鏈原理

dubbo中filter鏈的入口在ProtocolFilterWrapper中黄橘,這是Protocol的一個包裝類,在其服務(wù)暴露和服務(wù)引用時都進行了構(gòu)建filter鏈的工作屈溉。

// 構(gòu)建filter鏈
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
    Invoker<T> last = invoker;

    // 獲取可用的filter列表
    List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
    if (!filters.isEmpty()) {
        for (int i = filters.size() - 1; i >= 0; i--) {
            final Filter filter = filters.get(i);
            final Invoker<T> next = last;

            // 典型的裝飾器模式塞关,將invoker用filter逐層進行包裝
            last = new Invoker<T>() {

                public Class<T> getInterface() {
                    return invoker.getInterface();
                }

                public URL getUrl() {
                    return invoker.getUrl();
                }

                public boolean isAvailable() {
                    return invoker.isAvailable();
                }

                // 重點,每個filter在執(zhí)行invoke方法時子巾,會觸發(fā)其下級節(jié)點的invoke方法帆赢,最后一級節(jié)點即為最原始的服務(wù)
                public Result invoke(Invocation invocation) throws RpcException {
                    return filter.invoke(next, invocation);
                }

                public void destroy() {
                    invoker.destroy();
                }

                @Override
                public String toString() {
                    return invoker.toString();
                }
            };
        }
    }
    return last;
}

// 服務(wù)端暴露服務(wù)
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    if (Constants.REGISTRY_PROTOCOL.equals(invoker.getUrl().getProtocol())) {
        return protocol.export(invoker);
    }
    return protocol.export(buildInvokerChain(invoker, Constants.SERVICE_FILTER_KEY, Constants.PROVIDER));
}

// 客戶端引用服務(wù)
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
        return protocol.refer(type, url);
    }
    return buildInvokerChain(protocol.refer(type, url), Constants.REFERENCE_FILTER_KEY, Constants.CONSUMER);
}

可以看到,每一個filter節(jié)點都為原始的invoker服務(wù)增加了功能线梗,是典型的裝飾器模式椰于。構(gòu)建filter鏈的核心在于filter列表的獲取,也就是這一行代碼:

List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);

通過Filter的ExtendLoader實例獲取其激活的filter列表仪搔,getActivateExtension邏輯分為兩部分:
1.加載標注了Activate注解的filter列表
2.加載用戶在spring配置文件中手動注入的filter列表

public List<T> getActivateExtension(URL url, String key, String group) {
    // 根據(jù)key來獲取服務(wù)方/消費方自定義的filter列表
    String value = url.getParameter(key);
    return getActivateExtension(url, value == null || value.length() == 0 ? null : Constants.COMMA_SPLIT_PATTERN.split(value), group);
}

public List<T> getActivateExtension(URL url, String[] values, String group) {
    List<T> exts = new ArrayList<T>();
    List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);

    // 如果用戶配置的filter列表名稱中不包含-default瘾婿,則加載標注了Activate注解的filter列表
    if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {

        // 加載配置文件,獲取所有標注有Activate注解的類烤咧,存入cachedActivates中
        getExtensionClasses();
        for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
            String name = entry.getKey();
            Activate activate = entry.getValue();

            // Activate注解可以指定group偏陪,這里是看注解指定的group與我們要求的group是否匹配
            if (isMatchGroup(group, activate.group())) {
                T ext = getExtension(name);

                // 對于每一個dubbo中原生的filter,需要滿足以下3個條件才會被加載:
                // 1.用戶配置的filter列表中不包含該名稱的filter
                // 2.用戶配置的filter列表中不包含該名稱前加了"-"的filter
                // 3.該Activate注解被激活煮嫌,具體激活條件隨后詳解
                if (!names.contains(name)
                        && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
                        && isActive(activate, url)) {
                    exts.add(ext);
                }
            }
        }

        // 對加載的dubbo原生的filter列表進行排序笛谦,ActivateComparator排序器會根據(jù)Activate注解的before、after昌阿、order屬性對filter列表排序
        Collections.sort(exts, ActivateComparator.COMPARATOR);
    }

    // 加載用戶在spring配置文件中配置的filter列表
    List<T> usrs = new ArrayList<T>();
    for (int i = 0; i < names.size(); i++) {
        String name = names.get(i);

        // 針對用戶配置的每一個filter饥脑,需要滿足以下兩個條件才會被加載:
        // 1.名稱不是以"-"開頭
        // 2.用戶配置的所有filter列表中不包含-name的filter
        if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
                && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {

            // 用戶自己配置filter列表時恳邀,可以使用default的key來代表dubbo原生的filter列表,這樣一來就可以控制dubbo原生filter列表和用戶自定義filter列表之間的相對順序
            if (Constants.DEFAULT_KEY.equals(name)) {
                if (!usrs.isEmpty()) {
                    exts.addAll(0, usrs);
                    usrs.clear();
                }
            } else {
                T ext = getExtension(name);
                usrs.add(ext);
            }
        }
    }
    if (!usrs.isEmpty()) {
        exts.addAll(usrs);
    }
    return exts;
}

判斷Activate注解是否被激活的邏輯是這樣的:

private boolean isActive(Activate activate, URL url) {
    // 如果注解沒有配置value屬性好啰,則一定是激活的
    String[] keys = activate.value();
    if (keys == null || keys.length == 0) {
        return true;
    }

    // 對配置了value屬性的注解轩娶,如果服務(wù)的url屬性中存在與value屬性值相匹配的屬性且改屬性值不為空儿奶,則該注解也是激活的
    for (String key : keys) {
        for (Map.Entry<String, String> entry : url.getParameters().entrySet()) {
            String k = entry.getKey();
            String v = entry.getValue();
            if ((k.equals(key) || k.endsWith("." + key))
                    && ConfigUtils.isNotEmpty(v)) {
                return true;
            }
        }
    }
    return false;
}

ActivateComparator比較器的規(guī)則如下框往,總結(jié)起來有這么幾條規(guī)則:
1.before指定的過濾器,該過濾器將在這些指定的過濾器之前執(zhí)行
2.after指定的過濾器闯捎,該過濾器將在這些指定的過濾器之后執(zhí)行
3.order數(shù)值越小椰弊,越先執(zhí)行
4.order數(shù)值相等的條件下,順序?qū)⒁蕾囉趦蓚€filter的加載順序
5.before/after的優(yōu)先級高于order

public class ActivateComparator implements Comparator<Object> {

    public static final Comparator<Object> COMPARATOR = new ActivateComparator();

    public int compare(Object o1, Object o2) {
        if (o1 == null && o2 == null) {
            return 0;
        }
        if (o1 == null) {
            return -1;
        }
        if (o2 == null) {
            return 1;
        }
        if (o1.equals(o2)) {
            return 0;
        }

        // 配置了before/after屬性時瓤鼻,按照規(guī)則1秉版、2進行排序,比較完直接返回茬祷,此時指定的order值將被忽略
        Activate a1 = o1.getClass().getAnnotation(Activate.class);
        Activate a2 = o2.getClass().getAnnotation(Activate.class);
        if ((a1.before().length > 0 || a1.after().length > 0
                || a2.before().length > 0 || a2.after().length > 0)
                && o1.getClass().getInterfaces().length > 0
                && o1.getClass().getInterfaces()[0].isAnnotationPresent(SPI.class)) {
            ExtensionLoader<?> extensionLoader = ExtensionLoader.getExtensionLoader(o1.getClass().getInterfaces()[0]);
            if (a1.before().length > 0 || a1.after().length > 0) {
                String n2 = extensionLoader.getExtensionName(o2.getClass());
                for (String before : a1.before()) {
                    if (before.equals(n2)) {
                        return -1;
                    }
                }
                for (String after : a1.after()) {
                    if (after.equals(n2)) {
                        return 1;
                    }
                }
            }
            if (a2.before().length > 0 || a2.after().length > 0) {
                String n1 = extensionLoader.getExtensionName(o1.getClass());
                for (String before : a2.before()) {
                    if (before.equals(n1)) {
                        return 1;
                    }
                }
                for (String after : a2.after()) {
                    if (after.equals(n1)) {
                        return -1;
                    }
                }
            }
        }

        // 沒有配置before/after的條件下清焕,按照規(guī)則3、4進行排序
        int n1 = a1 == null ? 0 : a1.order();
        int n2 = a2 == null ? 0 : a2.order();
        // never return 0 even if n1 equals n2, otherwise, o1 and o2 will override each other in collection like HashSet
        return n1 > n2 ? 1 : -1;
    }

}

分析上面的分析祭犯,可以發(fā)現(xiàn)dubbo在構(gòu)建filter鏈時非常靈活秸妥,有幾個關(guān)鍵點在這里做一下總結(jié):

  • filter被分為兩類,一類是標注了Activate注解的filter沃粗,包括dubbo原生的和用戶自定義的粥惧;一類是用戶在spring配置文件中手動注入的filter
  • 對標注了Activate注解的filter,可以通過before最盅、after和order屬性來控制它們之間的相對順序突雪,還可以通過group來區(qū)分服務(wù)端和消費端
  • 手動注入filter時,可以用default來代表所有標注了Activate注解的filter涡贱,以此來控制兩類filter之間的順序
  • 手動注入filter時咏删,可以在filter名稱前加一個"-"表示排除某一個filter,比如說如果配置了一個-default的filter问词,將不再加載所有標注了Activate注解的filter
2.自定義filter

自定義filter非常簡單督函,只需要實現(xiàn)Filter接口即可,對于Filter的某一個具體實現(xiàn)戏售,有兩種方式可以在構(gòu)建filter鏈時將其包含進去侨核,但無論哪種方式,都需要在Filter對應的SPI文件中進行相應的配置
1.通過標注Activate注解來實現(xiàn)

@Activate(group = Constants.PROVIDER)
public class ProviderFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        System.out.println("=== provider ===");
        return invoker.invoke(invocation);
    }
}

2.在spring配置文件中通過配置filter屬性來實現(xiàn)

<dubbo:service interface="com.alibaba.dubbo.demo.TraceIdService" ref="traceIdService" filter="providerFilter"/>

這兩種方式除了該filter在filter鏈中的順序不同外灌灾,其它地方都是等價的搓译。當然,按照上面的分析锋喜,順序也是可以按照我們的要求來靈活控制的些己。

3.利用filter實現(xiàn)traceId透傳

在微服務(wù)場景下豌鸡,一次調(diào)用過程常常會涉及多個應用,在定位問題時段标,往往需要在多個應用中查看某一次調(diào)用鏈路上的日志涯冠,為了達到這個目的,一種常見的做法是在調(diào)用入口處生成一個traceId逼庞,并基于RpcContext來實現(xiàn)traceId的透傳蛇更。

在開始進一步的嘗試之前,我們不妨先來看看兩個filter赛糟,大致了解下RpcContext是怎么實現(xiàn)traceId透傳的派任。
客戶端的ConsumerContextFilter:

@Activate(group = Constants.CONSUMER, order = -10000)
public class ConsumerContextFilter implements Filter {

    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext.getContext()
                .setInvoker(invoker)
                .setInvocation(invocation)
                .setLocalAddress(NetUtils.getLocalHost(), 0)
                .setRemoteAddress(invoker.getUrl().getHost(),
                        invoker.getUrl().getPort());
        if (invocation instanceof RpcInvocation) {
            ((RpcInvocation) invocation).setInvoker(invoker);
        }
        try {
            return invoker.invoke(invocation);
        } finally {
            RpcContext.getContext().clearAttachments();
        }
    }

}

服務(wù)端的ContextFilter:

@Activate(group = Constants.PROVIDER, order = -10000)
public class ContextFilter implements Filter {

    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        Map<String, String> attachments = invocation.getAttachments();
        if (attachments != null) {
            attachments = new HashMap<String, String>(attachments);
            attachments.remove(Constants.PATH_KEY);
            attachments.remove(Constants.GROUP_KEY);
            attachments.remove(Constants.VERSION_KEY);
            attachments.remove(Constants.DUBBO_VERSION_KEY);
            attachments.remove(Constants.TOKEN_KEY);
            attachments.remove(Constants.TIMEOUT_KEY);
            attachments.remove(Constants.ASYNC_KEY);// Remove async property to avoid being passed to the following invoke chain.
        }
        RpcContext.getContext()
                .setInvoker(invoker)
                .setInvocation(invocation)
//                .setAttachments(attachments)  // merged from dubbox
                .setLocalAddress(invoker.getUrl().getHost(),
                        invoker.getUrl().getPort());

        // mreged from dubbox
        // we may already added some attachments into RpcContext before this filter (e.g. in rest protocol)
        if (attachments != null) {
            if (RpcContext.getContext().getAttachments() != null) {
                RpcContext.getContext().getAttachments().putAll(attachments);
            } else {
                RpcContext.getContext().setAttachments(attachments);
            }
        }

        if (invocation instanceof RpcInvocation) {
            ((RpcInvocation) invocation).setInvoker(invoker);
        }
        try {
            return invoker.invoke(invocation);
        } finally {
            RpcContext.removeContext();
        }
    }
}

通過這兩個filter不難發(fā)現(xiàn),之所以利用RpcContext可以實現(xiàn)traceId的透傳璧南,是因為invocation的存在掌逛,客戶端在調(diào)用invoke方法的時候,會將當前調(diào)用的參數(shù)載體invocation透傳給服務(wù)端司倚,而服務(wù)端會從其中取出attachments屬性進行相關(guān)處理后在重新設(shè)置到invocation中向后傳遞豆混,因此只需要在客戶端將traceId設(shè)置到attachments中即可。

于是我們開始以下嘗試:
服務(wù)端接口:

public interface TraceIdService {

    void test(String key);
}

服務(wù)端實現(xiàn):

public class TraceIdServiceImpl implements TraceIdService {

    @Override
    public void test(String key) {
        String traceId = RpcContext.getContext().getAttachment("traceId");
        System.out.println("key = " + key + ", traceId = " + traceId);
    }
}

客戶端代碼:

public class TraceIdConsumer {

    public static void main(String[] args) {

        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"META-INF/spring/dubbo-demo-consumer.xml"});
        context.start();
        TraceIdService traceIdService = (TraceIdService) context.getBean("traceIdService"); // get remote service proxy

        RpcContext.getContext().setAttachment("traceId", String.valueOf(System.currentTimeMillis()));
        System.out.println(RpcContext.getContext().getAttachments());
        traceIdService.test("1");

        System.out.println(RpcContext.getContext().getAttachments());
        traceIdService.test("2");
    }
}

以上代碼的執(zhí)行結(jié)果如下:

客戶端輸出:
{traceId=1538746615202}
{}

服務(wù)端輸出:
key = 1, traceId = 1538746615202
key = 2, traceId = null

我們發(fā)現(xiàn)动知,在第一次調(diào)用中皿伺,traceId確實從客戶端透傳到了服務(wù)端,但是在第二次調(diào)用時神奇的消失了拍柒!而這正是filter搗的鬼心傀。在ConsumerContextFilter的finally子句中,我們發(fā)現(xiàn)attachments對象被清空了拆讯,而在服務(wù)端ContextFilter中脂男,整個context對象都被清空了!V帜拧宰翅!

為了解決這個問題,我們需要在每次調(diào)用前都重新設(shè)置下attachments對象爽室,也就是在客戶端給調(diào)用鏈新增一個設(shè)置attachments對象的功能汁讼。前面我們說過,dubbo中每一個filter節(jié)點都為原始的invoker服務(wù)增加了功能阔墩,是典型的裝飾器模式嘿架。看到這里你想到了什么啸箫?是的耸彪,沒錯。我們可以新增一個filter來完成這一功能忘苛。

@Activate(group = Constants.CONSUMER)
public class TraceIdFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String traceId = String.valueOf(System.currentTimeMillis());
        RpcContext.getContext().setAttachment("traceId", traceId);
        System.out.println("traceId = " + traceId);

        return invoker.invoke(invocation);
    }
}

此時在客戶端中注釋小設(shè)置attachments的代碼蝉娜,再次執(zhí)行代碼的輸出如下唱较,此時兩次調(diào)用,traceId都可以正確地從客戶端傳遞到服務(wù)端召川,完美????乛?乛????

客戶端輸出:
traceId = 1538749616953
traceId = 1538749617199

服務(wù)端輸出:
key = 1, traceId = 1538749616953
key = 2, traceId = 1538749617199

更多技術(shù)文章南缓,咱們公眾號見,我在公眾號里等你~

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荧呐,一起剝皮案震驚了整個濱河市汉形,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坛增,老刑警劉巖获雕,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件薄腻,死亡現(xiàn)場離奇詭異收捣,居然都是意外死亡,警方通過查閱死者的電腦和手機庵楷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門罢艾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人尽纽,你說我怎么就攤上這事咐蚯。” “怎么了弄贿?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵春锋,是天一觀的道長。 經(jīng)常有香客問我差凹,道長期奔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任危尿,我火速辦了婚禮呐萌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谊娇。我一直安慰自己肺孤,他們只是感情好,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布济欢。 她就那樣靜靜地躺著赠堵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪法褥。 梳的紋絲不亂的頭發(fā)上茫叭,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天,我揣著相機與錄音挖胃,去河邊找鬼杂靶。 笑死梆惯,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的吗垮。 我是一名探鬼主播垛吗,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼烁登!你這毒婦竟也來了怯屉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤饵沧,失蹤者是張志新(化名)和其女友劉穎锨络,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狼牺,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡羡儿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了是钥。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掠归。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖悄泥,靈堂內(nèi)的尸體忽然破棺而出虏冻,到底是詐尸還是另有隱情,我是刑警寧澤弹囚,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布厨相,位于F島的核電站,受9級特大地震影響鸥鹉,放射性物質(zhì)發(fā)生泄漏蛮穿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一宋舷、第九天 我趴在偏房一處隱蔽的房頂上張望绪撵。 院中可真熱鬧,春花似錦祝蝠、人聲如沸音诈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽细溅。三九已至,卻和暖如春儡嘶,著一層夾襖步出監(jiān)牢的瞬間喇聊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工蹦狂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留誓篱,地道東北人朋贬。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像窜骄,于是被迫代替她去往敵國和親锦募。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355

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