Dubbo的SPI之Filter实现

前面我们了解过了Java的SPI扩展机制,对于Java扩展机制的原理以及优缺点也有了大概的了解,这里继续深入一下Dubbo的扩展点加载机制。玩过Dubbo框架的同学都知道,Dubbo框架最强大的地方就是他的SPI机制,可以满足使用者天马行空的扩展性需求。
本文主要讨论2点:Dubbo的spi机制实现原理;基于SPI思想的Filter实现。

Dubbo的spi机制实现原理

这里以Protocol 协议接口来讲解,先上一张图来帮助理解:


ExtensionLoader加载过程.jpg
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

ExtensionLoader类的实现思想参考了JDK中的ServiceLoader类,也是用来加载指定路径下的接口实现,具体实现细节比JDK的复杂了很多。
首先看ExtensionLoader的静态方法getExtensionLoader。

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null)
            throw new IllegalArgumentException("Extension type == null");
        if(!type.isInterface()) {
            throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
        }
        if(!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type(" + type + 
                    ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
        }
        //根据接口对象取ExtensionLoader类
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            //如果为空保存接口类对应的 新建的ExtensionLoader对象
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

有4个点需要注意:
1.EXTENSION_LOADERS这个Map中以接口为key,以ExtensionLoader对象为value。
2.判断Map中根据接口get对象,如果没有就new个ExtensionLoader对象保存进去。并返回该ExtensionLoader对象。
3.注意创建ExtensionLoader对象的构造函数代码,将传入的接口type属性赋值给了ExtensionLoader类的type属性
4.创建ExtensionFactory objectFactory对象

@SPI("dubbo")
public interface Protocol {

      int getDefaultPort();

      @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

     @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    void destroy();

}

ExtensionLoader使用loadExtensionClasses方法读取扩展点中的实现类

loadExtensionClasses先读取SPI注解的value值,如果value有值,就把这个值作为默认扩展实现的key。然后再以此读取META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/下对应的文件。

  loadFile逐行读取com.alibaba.dubbo.rpc.Protocol文件中的内容,每行内容以key/value形式存储。先判断实现类上是否打上了@Adaptive注解,如果打上了该注解,将此类作为Protocol协议的设配类缓存起来,读取下一行。如果实现类上没有打上@Adaptive注解,判断实现类是否存在参数为该接口的构造器,有的话作为包装类存储在该ExtensionLoader的Set<Class<?>> cachedWrapperClasses;集合中,这里用到了装饰器模式。如果该类既不是设配类,也不是wrapper对象,那就是扩展点的具体实现对象,查找实现类上是否打了@Activate注解,有缓存到变量cachedActivates的map中将实现类缓存到cachedClasses中,以便于使用时获取。如ProtocolFilterWrapper的实现如下:
public class ProtocolFilterWrapper implements Protocol {

    private final Protocol protocol;

    public ProtocolFilterWrapper(Protocol protocol) {
        if (protocol == null) {
            throw new IllegalArgumentException("protocol == null");
        }
        this.protocol = protocol;
    }
  ..........      
}

获取或则创建设配对象getAdaptiveExtension

如果cachedAdaptiveClass有值,说明有且仅有一个实现类打了@Adaptive, 实例化这个对象返回。如果cachedAdaptiveClass为空, 创建设配类字节码。

为什么要创建设配类,一个接口多种实现,SPI机制也是如此,这是策略模式,但是我们在代码执行过程中选择哪种具体的策略呢。Dubbo采用统一数据模式com.alibaba.dubbo.common.URL(它是dubbo定义的数据模型不是jdk的类),它会穿插于系统的整个执行过程,URL中定义的协议类型字段protocol,会根据具体业务设置不同的协议。url.getProtocol()值可以是dubbo也是可以webservice, 可以是zookeeper也可以是redis。

设配类的作用是根据url.getProtocol()的值extName,去ExtensionLoader. getExtension( extName)选取具体的扩展点实现。

有上述的分析可知,能够使用javasist生成设配类的条件:

1)接口方法中必须至少有一个方法打上了@Adaptive注解

2)打上了@Adaptive注解的方法参数必须有URL类型参数或者有参数中存在getURL()方法
仔细看看Protocol接口代理的具体实现,在使用接口代理中的方法时,都会根据URL来确定接口的具体实现,因为URL中携带了用户大部分的参数配置,根据里面的属性来获取。里面关键代码:

com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);

看到这里思路应该比较清晰了!所有的接口代理中,并没有给定具体的实现,全部根据用户的参数配置来动态创建接口的具体实现。这样做让程序非常的灵活,让接口的实现插拔更加方便。如果想增加一个接口的实现,只需要按照SPI的配置方式增加配置文件,xml标签配置指定新接口实现的标记即可。

基于SPI思想的Filter实现

在微服务场景下,一次调用过程常常会涉及多个应用,在定位问题时,往往需要在多个应用中查看某一次调用链路上的日志,为了达到这个目的,一种常见的做法是在调用入口处生成一个traceId,并基于RpcContext来实现traceId的透传。下面来看一下怎么通过filter实现traceId的跟踪记录。
1.创建Dubbo框架的api项目,创建类FilterTest

package com.enjoy.filter;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.*;

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


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

        Result result = invoker.invoke(invocation);
        return result;
    }
}

项目结构图为


api项目结构.JPG

2.创建server项目,提供服务,创建订单类

package com.enjoy.service.impl;

import com.enjoy.dao.OrderDao;
import com.enjoy.entity.OrderEntiry;
import com.enjoy.service.OrderService;
import com.enjoy.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;

public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderDao orderDao;
    @Autowired
    private ProductService productService;


    @Override
    public OrderEntiry getDetail(String id) {
        OrderEntiry orderEntiry =  orderDao.getDetail(id);
        orderEntiry.addProduct(productService.getDetail("P001"));
        orderEntiry.addProduct(productService.getDetail("P002"));
        System.out.println(super.getClass().getName()+"被调用一次:"+System.currentTimeMillis());
        return orderEntiry;
    }

    @Override
    public OrderEntiry submit(OrderEntiry order) {
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (1==order.getStatus()){
            System.out.println("警告:订单重复提交!");
            throw new RuntimeException("订单重复提交!");
        }
        System.out.println(super.getClass().getName()+"被调用一次:"+System.currentTimeMillis());
        return orderDao.submit(order);
    }

    @Override
    public String cancel(OrderEntiry order) {
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(super.getClass().getName()+"被调用一次:"+System.currentTimeMillis());
        return orderDao.cancel(order);
    }
}

配置dubbo.xm文件

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context-3.1.xsd
     http://code.alibabatech.com/schema/dubbo
     http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <context:component-scan base-package="com.enjoy"/>

    <!-- 提供方应用信息,用于计算依赖关系 -->
    <dubbo:application name="storeServer"/>

    <!-- 使用zookeeper注册中心暴露服务地址 -->
    <dubbo:registry address="zookeeper://10.xxx.xxx.xxx:2181"/>

    <!--用dubbo协议在20880端口暴露服务 -->
    <dubbo:protocol name="dubbo" port="20880"/>

    <dubbo:consumer check="false" />

    <!-- 声明需要暴露的服务接口 -->
    <dubbo:service interface="com.enjoy.service.OrderService" ref="orderService" protocol="dubbo" />
    <dubbo:service interface="com.enjoy.service.PayService" ref="payService" protocol="dubbo" />
    <dubbo:service interface="com.enjoy.service.OtherService" ref="otherService" protocol="dubbo" />
    <dubbo:service interface="com.enjoy.service.ProductService" ref="productService" protocol="dubbo"/>
    <dubbo:service interface="com.enjoy.service.UserService" ref="userService" />

    <!-- 声明需要引用的服务接口 -->

    <!--和本地bean一样实现服务 -->
    <bean id="orderService" class="com.enjoy.service.impl.OrderServiceImpl"/>
    <bean id="payService" class="com.enjoy.service.impl.PayServiceImpl"/>
    <bean id="otherService" class="com.enjoy.service.impl.OtherServiceImpl"/>
    <bean id="productService" class="com.enjoy.service.impl.ProductServiceImpl"/>
    <bean id="userService" class="com.enjoy.service.impl.UserServiceImpl"/>

</beans>
server项目结构.JPG

3.创建消费端

package com.enjoy.controller;

import com.alibaba.dubbo.rpc.RpcContext;
import com.enjoy.entity.OrderEntiry;
import com.enjoy.service.OrderService;
import com.enjoy.service.PayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;


@Controller
public class OrderController {

    @Autowired
    private PayService payService;

    @Autowired
    private OrderService orderService;

    @RequestMapping(value = "/order", method = RequestMethod.GET)
    public String getDetail(HttpServletRequest request, HttpServletResponse response){
        OrderEntiry orderView = orderService.getDetail("1");

        request.setAttribute("order", orderView);
        return "order";
    }

    /**
     * 异步并发调用
     * @param request
     * @param response
     * @return
     */
    @RequestMapping(value = "/cancel", method = RequestMethod.GET)
    public String cancel(HttpServletRequest request, HttpServletResponse response)  {
        OrderEntiry orderView = orderService.getDetail("1");

        String cancel_order = null,cancel_pay = null;
        long start = System.currentTimeMillis();

        //若设置了async=true,方法立即返回null
        cancel_order = orderService.cancel(orderView);
        //只有async=true,才能得到此对象,否则为null
        Future<String> cancelOrder = RpcContext.getContext().getFuture();
        cancel_pay = payService.cancelPay(orderView.getMoney());
        Future<String> cancelpay = RpcContext.getContext().getFuture();

        /**
         * Future模式
         *
         */

        try {
            cancel_order = cancelOrder.get();
            cancel_pay = cancelpay.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        request.setAttribute("cancelOrder", cancel_order);
        request.setAttribute("cancelpay", cancel_pay);

        long time = System.currentTimeMillis() - start;
        request.setAttribute("time", time);

        return "/cancel";
    }

    /**
     * 事件通知
     * @param request
     * @param response
     * @return
     */
    @RequestMapping(value = "/order/submit", method = RequestMethod.GET)
    public String submit(HttpServletRequest request, HttpServletResponse response){
        OrderEntiry orderView = orderService.getDetail("1");
        orderView.setStatus(1);
        orderService.submit(orderView);

        request.setAttribute("order", orderView);
        return "/order";
    }

}
消费端项目结构

4.启动服务提供方和消费端项目,其中消费端项目控制台信息:


控制台.JPG

这个信息就是我们在api中定义的fiter过滤器的具体实现,即在微服务跨域调用过程中,traceId的追踪,方便后续排查日志。

终于写完了,接口代理的生成是不是有点动态代理的感觉。然后用户在XML中配置的dubbo标签属性都保存在了URL中,URL携带的参数贯穿了整个dubbo架构,所有的组件调用都根据URL中配置的参数做处理。其实SPI技术在很多地方都有用到,比如数据库的驱动,日志的处理,原理不是很复杂,仔细研究下就明白了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容

  • Dubbo采用微内核+插件体系,使得设计优雅,扩展性强。那所谓的微内核+插件体系是如何实现的呢!大家是否熟悉spi...
    carl_zhao阅读 934评论 1 3
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,644评论 18 139
  • 前面我们了解过了Java的SPI扩展机制,对于Java扩展机制的原理以及优缺点也有了大概的了解,这里继续深入一下D...
    加大装益达阅读 5,057评论 2 20
  • 一.概览 整体描述 dubbo利用spi扩展机制实现大量的动态扩展,要想充分了解dubbo的扩展机制,首先必须弄明...
    致虑阅读 883评论 0 2
  • 0 前言 站在一个框架作者的角度来说,定义一个接口,自己默认给出几个接口的实现类,同时 允许框架的使用者也能够自定...
    七寸知架构阅读 16,228评论 3 67