基于dubbo的filter过滤器实现微服务日志追踪,以及实现原理

实现:dubbo跨服务的日志追踪

用dubbo的filter 在provider和consumer两边添加日志id的存取,实现统一日志id的服务间透传。
首先 写filter代码,我是写一起了,也可以分开

@Activate(group = {"consumer", "provider"}, order = 0)
@Slf4j
public class DubboLogFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        //判断是消费者  还是 服务提供者
        if (RpcContext.getContext().isConsumerSide()) {
            //消费者 将logUUId set至上下文中
            String logUUid = LogUUidThreadLocal.getLogUUid();
            RpcContext.getContext().setAttachment(LogUUidThreadLocal.UUIDKey,
                    logUUid);
        } else {
            //生产者
            //取出业务流水号
            String logUUid =
                    RpcContext.getContext().getAttachment(LogUUidThreadLocal.UUIDKey);
            if (logUUid == null) {
                logUUid = UUID.randomUUID().toString();
            }
            //流水号set至线程局部变量
            LogUUidThreadLocal.setLogUUid(logUUid);
            //slf4j 中设置了日志打印格式用作日志链路追踪,需要日志模板配置才能生效
            MDC.put(LogUUidThreadLocal.UUIDKey, logUUid);
        }
        try {
            return invoker.invoke(invocation);
        }finally {
            if (RpcContext.getContext().isProviderSide()) {
                MDC.remove(LogUUidThreadLocal.UUIDKey);
                LogUUidThreadLocal.removeLogUUid();
            }
        }
    }
}

然后在resources下面添加META-INF/dubbo/com.alibaba.dubbo.rpc.Filter文件

logUUid=com.pikadog.autoconfig.log.DubboLogFilter

也可以不写成key=valuer,直接

com.pikadog.autoconfig.log.DubboLogFilter

yml配置文件加上provider.filter或者consumer.filter,值就是上面配置的key,用第二种方法直接写的没有key就不用配置了。

dubbo:
  application:
    name: micro  #提供者名字
  registry:
    address: zookeeper://127.0.0.1:2181  #zookeeper注册中心地址
  scan:
    base-packages: com.pikadog.micro #需要注册
  protocol:
    name: dubbo
    port: 20881 #如果要实现负载均衡效果,修改这里的端口,如果不配置,会显示端口占用
  config-center:
    timeout: 6000
  provider:
    filter: logUUid

到这里dubbo的filter就配置好,可以运行了。

这个过程中产生的疑问

  1. @Activate这个注解的作用?get
  2. 第二步中两种写法有什么区别?get
  3. 自己实现的扩展点是怎么注入到dubbo的执行中的? get
  4. RpcContext?
  5. dubbo的SPI机制?
@Activate这个注解的作用?

@Activate注解
表示一个拓展点是否被激活,可以放在类上,方法上, dubbo用它在spi扩展类定义上,表示这个扩展实现激活条件和时机(group、order)。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
    /**
    * {@link ExtensionLoader#getActivateExtension(URL, String, String)}这个方法里面去过滤匹配的组
    * 如果不写默认不过滤
    * @see ExtensionLoader#getActivateExtension(URL, String, String)
    */
    String[] group() default {};
    
   /**
    * Key过滤条件。包含{@link ExtensionLoader#getActivateExtension}的URL的参数Key中有,则返回扩展。
    * 注解的值 <code>@Activate("cache,validatioin")</code>,
    * 则{@link ExtensionLoader#getActivateExtension}的URL的参数有<code>cache</code>Key,或是<code>validatioin</code>则返回扩展。
    * 如没有设置,则不过滤。
    */
    String[] value() default {};
    
    /**
    下面都是排序信息
    */
    @Deprecated
    String[] before() default {};
    
    @Deprecated
    String[] after() default {};

    int order() default 0;
}

具体使用

@Activate(group = Constants.PROVIDER, value = Constants.TOKEN_KEY)
public class TokenFilter implements Filter {

}
//表示如果过滤器使用方(通过group指定)属于Constants.PROVIDER(服务提供方)并且 URL中有参数 Constants.TOKEN_KEY(token)时就激活使用这个过滤器

dubbo基于URL驱动,配置信息都放在URL里传递,在运行时,通过传入URL中的某些参数来动态控制具体实现,这便是Dubbo的扩展点自适应特性。
从RpcContext中打印的Url:
dubbo://172.16.3.191:20881/com.pikadog.micro.service.MicroDubboExceptionService?anyhost=true&application=micro&bind.ip=172.16.3.191&bind.port=20881&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.pikadog.micro.service.MicroDubboExceptionService&metadata-type=remote&methods=getUUId,getDoTest&pid=4040&qos.enable=false&release=2.7.8&service.filter=logUUid&side=provider&timestamp=1650518067829

这里的注解里的value参数其实对应的是第二部中的key=value的key,在yaml中配置filter:xx,相当于指定了要使用的filter的key(参数service.filter=logUUid)那么构造filter链的时候除了默认的filter以外还会加入指定的key下的filter。默认的filter也可以通过把key设置为包含’-default’时不加载默认的filter。

自己实现的扩展点是怎么注入到dubbo的执行中的?

构造执行链时调用到获取已经激活的拓展点的方法

 /**
     *获取已经激活的拓展点
     *
     * @param url    url
     * @param 激活的拓展点的名字,这里就是配置文件中配置的service.filter的值
     * @param group  provider/consumer 
     * @返回已经激活的filter的List
     * @see org.apache.dubbo.common.extension.Activate
     */
    public List<T> getActivateExtension(URL url, String[] values, String group) {
        List<T> activateExtensions = new ArrayList<>();
        //是否配置了自定义filter,没有配置就是空list,这里是service.filter:logUUid,names就是logUUid
        List<String> names = values == null ? new ArrayList<>(0) : asList(values);
        //判断自定义的拓展点列表里是否有“-default”
        if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {//如果没有就要先加载默认自带的的
            //刷新一下缓存
            getExtensionClasses();
            //循环所有的@Activete注解(cachedActivates存的是注解见图片)
            for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
                String name = entry.getKey();//logUUid
                Object activate = entry.getValue();/注解

                String[] activateGroup, activateValue;

                if (activate instanceof Activate) {
                    activateGroup = ((Activate) activate).group();
                    activateValue = ((Activate) activate).value();
                } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
                    activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
                    activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
                } else {
                    continue;
                }
                if (isMatchGroup(group, activateGroup)
                        && !names.contains(name)
                        && !names.contains(REMOVE_VALUE_PREFIX + name)//假如配置了“-cache”,那么chachFilter就不会被加进去
                        && isActive(activateValue, url)) {
                    activateExtensions.add(getExtension(name));//添加拓展点
                }
            }
            //排序
            activateExtensions.sort(ActivateComparator.COMPARATOR);
        }
        //如果配置了跳过默认,那就不看缓存中的拓展点了,只看配置
        List<T> loadedExtensions = new ArrayList<>();
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);
            //如果配置的service.filter里有“-”开始的name或者有“-XXX”的字样,这里的判断就进不去
            if (!name.startsWith(REMOVE_VALUE_PREFIX)
                    && !names.contains(REMOVE_VALUE_PREFIX + name)) {
                //如果name里面写了“default”,那么我们自己定义的filter链会被按顺序提到原生filter的前面
                //就是按我们配置的顺序排序
                if (DEFAULT_KEY.equals(name)) {
                    if (!loadedExtensions.isEmpty()) {
                        activateExtensions.addAll(0, loadedExtensions);
                        loadedExtensions.clear();
                    }
                } else {
                    loadedExtensions.add(getExtension(name));
                }
            }
        }
        //默认的加上我们自己定义的
        if (!loadedExtensions.isEmpty()) {
            activateExtensions.addAll(loadedExtensions);
        }
        return activateExtensions;
    }
第二步中两种写法有什么区别?

通过代码可以发现,自定义的filter不需要添加@Activate注解,也可以实现注入,添加注解可以指定group和valuer,加载进执行链也走的是上面默认filter的代码。反正很灵活,写了@Activate和yml配置都可以选择性的写。
在这里插入图片描述

几个重点的Filter
dubbo自己的Filter文件

cache=org.apache.dubbo.cache.filter.CacheFilter
validation=org.apache.dubbo.validation.filter.ValidationFilter
echo=org.apache.dubbo.rpc.filter.EchoFilter
generic=org.apache.dubbo.rpc.filter.GenericFilter
genericimpl=org.apache.dubbo.rpc.filter.GenericImplFilter
token=org.apache.dubbo.rpc.filter.TokenFilter
accesslog=org.apache.dubbo.rpc.filter.AccessLogFilter
activelimit=org.apache.dubbo.rpc.filter.ActiveLimitFilter
classloader=org.apache.dubbo.rpc.filter.ClassLoaderFilter
context=org.apache.dubbo.rpc.filter.ContextFilter
consumercontext=org.apache.dubbo.rpc.filter.ConsumerContextFilter
exception=org.apache.dubbo.rpc.filter.ExceptionFilter
executelimit=org.apache.dubbo.rpc.filter.ExecuteLimitFilter
deprecated=org.apache.dubbo.rpc.filter.DeprecatedFilter
compatible=org.apache.dubbo.rpc.filter.CompatibleFilter
timeout=org.apache.dubbo.rpc.filter.TimeoutFilter
tps=org.apache.dubbo.rpc.filter.TpsLimitFilter
trace=org.apache.dubbo.rpc.protocol.dubbo.filter.TraceFilter
future=org.apache.dubbo.rpc.protocol.dubbo.filter.FutureFilter
monitor=org.apache.dubbo.monitor.support.MonitorFilter

metrics=org.apache.dubbo.monitor.dubbo.MetricsFilter
RpcContext?

ConsumerContextFilter(consumercontext),关于RpcContext的回答也在这个Filter,相应的,provider侧也有一个ContextFilter。当服务接收到Rpc请求或者发起Rpc请求的时候,RpcContext都会发生相应变化。

/**
 * 设置当前的rpc调用信息
 * 用于消费者调用者。它这样做是为了使需要的信息对执行线程的RpcContext可用。
 * @see org.apache.dubbo.rpc.Filter
 * @see RpcContext
 */
@Activate(group = CONSUMER, order = -10000)
public class ConsumerContextFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext context = RpcContext.getContext();
        context.setInvoker(invoker)
                .setInvocation(invocation)
                .setLocalAddress(NetUtils.getLocalHost(), 0)
                .setRemoteAddress(invoker.getUrl().getHost(), invoker.getUrl().getPort())
                .setRemoteApplicationName(invoker.getUrl().getParameter(REMOTE_APPLICATION_KEY))
                .setAttachment(REMOTE_APPLICATION_KEY, invoker.getUrl().getParameter(APPLICATION_KEY));
        if (invocation instanceof RpcInvocation) {
            ((RpcInvocation) invocation).setInvoker(invoker);
        }

        // 通过最终用户设置的默认超时
        Object countDown = context.get(TIME_COUNTDOWN_KEY);
        if (countDown != null) {
            TimeoutCountDown timeoutCountDown = (TimeoutCountDown) countDown;
            if (timeoutCountDown.isExpired()) {
                return AsyncRpcResult.newDefaultAsyncResult(new RpcException(RpcException.TIMEOUT_TERMINATE,
                        "No time left for making the following call: " + invocation.getServiceName() + "."
                                + invocation.getMethodName() + ", terminate directly."), invocation);
            }
        }
        return invoker.invoke(invocation);
    }

}

RpcContext和dubbo的异步调用//todo

dubbo的SPI机制?

Dubbo的SPI机制通过其实上面已经了解了一部分了。
上一次接触SPI还是上次……JDK的SPI思想是java规定好接口,然后引用其他jar包的代码来实现这些接口,比如JDBC。
Dubbo的SPI相比于JDK的循环查找,一次性加载更加灵活
首先是@SPI注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {

    /**
     * default extension name
     */
    String value() default "";

}

参数很简单,只有一个默认拓展点。
在接口上加上@SPI注解,表明这个接口是可拓展接口,如果用户使用是没有做任何额外配置,没有指定具体使用哪个实现,就用注解的valuer默认值。比如Protocal,默认的值是dubbo,dubbo会在META-INF/dubbo/internal下的com.alibaba.dubbo.rpc.Protocol文件中找到key对应的value。

/**
 * Protocol. (API/SPI, Singleton, ThreadSafe)
 */
@SPI("dubbo")
public interface Protocol {

    /**
     * 获取默认的Port
     */
    int getDefaultPort();

    /**
     * 暴露远程服务:
     * 1. 协议在接收请求时,应记录请求来源方地址信息:RpcContext.getContext().setRemoteAddress();<br>
     * 2. export() 必须是幂等的,也就是暴露同一个 URL 的 Invoker 两次,和暴露一次没有区别。<br>
     * 3. export() 传入的 Invoker 由框架实现并传入,协议不需要关心。<br>
     *
     * @param <T>     服务的类型
     * @param invoker 服务的执行体
     * @return exporter 暴露服务的引用,用于取消暴露
     * @throws RpcException 当暴露服务出错时抛出,比如端口已占用
     */
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    /**
     * 引用远程服务:<br>
     * 1. 当用户调用 refer() 所返回的 Invoker 对象的 invoke() 方法时,协议需相应执行同 URL 远端 export() 传入的 Invoker 对象的 invoke() 方法。<br>
     * 2. refer() 返回的 Invoker 由协议实现,协议通常需要在此 Invoker 中发送远程请求。<br>
     * 3. 当 url 中有设置 check=false 时,连接失败不能抛出异常,并内部自动恢复。<br>
     *
     * @param <T>  服务的类型
     * @param type 服务的类型
     * @param url  远程服务的URL地址
     * @return invoker 服务的本地代理
     * @throws RpcException 当连接服务提供方失败时抛出
     */
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    /**
     * 释放协议:<br>
     * 1. 取消该协议所有已经暴露和引用的服务。<br>
     * 2. 释放协议所占用的所有资源,比如连接和端口。<br>
     * 3. 协议在释放后,依然能暴露和引用新的服务。<br>
     */
    void destroy();

    /**
     * Get all servers serving this protocol
     *
     * @return
     */
    default List<ProtocolServer> getServers() {
        return Collections.emptyList();
    }

}
// 配置文件 com.alibaba.dubbo.rpc.Protocol 中的内容
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

留意看看这个@Adaptive注解,放在方法上类上都可以
当扩展点的方法被@Adaptive修饰时,在Dubbo初始化扩展点时会自动生成和编译一个动态的Adaptive类。
比如Protocal的refer和export被修饰,DUbbo初始化Protocal拓展点的时候就会生成Protocol$Adaptive类

public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
    public void destroy() {
        throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }
 
    public int getDefaultPort() {
        throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
 
    }
 
    public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
     //一些校验
        if (arg0 == null) 
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
        
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
        
        org.apache.dubbo.common.URL url = arg0.getUrl();
        //拓展点名称从URL的protocal里拿,为null,默认dubbo
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
       //根据拓展点名称获取Protocol的拓展点实例
        org.apache.dubbo.rpc.Protocol extension = 
                (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }
 
    public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
        if (arg1 == null) 
            throw new IllegalArgumentException("url == null");
        
        org.apache.dubbo.common.URL url = arg1;
        
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        
        org.apache.dubbo.rpc.Protocol extension = 
                (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }
}

关键地方就是ExtensionLoader.getExtensionLoader,通过动态代理出一些公共的逻辑,获取URL上的信息,找到并且调用到真正的实现类。

    //构造方法私有的,只能自己内部调用
    private ExtensionLoader(Class<?> type) {
        this.type = type;
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }
    /**
     * 这个方法就是
     * @param type 拓展接口Type
     * @param <T> 比如 Protocol,Filter
     * @return 返回一个拓展点加载器
     */
    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!");
        }
        //如果Type上没有@SPI 那就不是一个拓展接口,报错
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type(" + type +
                    ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
        }
        //直接从拓展点加载器集合里面拿
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));//这里用到了加载器的构造方法
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

当@Adaptive用在类上时,留意上面代码构造方法中的ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()
ExtensionFactory也是一个可拓展接口,但是@SPI注解上并没有指定默认实现类
在这里插入图片描述

实现类中AdaptiveExtensionFactory类上有Adaptive注解,那么他既为默认。一个SPI接口实现类中只能有一个类上用@Adaptive注解。

下面就是重头戏ExtensionLoader,拓展点加载器,上面已经分析了其中的连个方法,但只是非常小的一部分。这是 dubbo 实现 SPI 扩展机制 的核心,几乎所有实现的逻辑都被封装在 ExtensionLoader 中(源码和注释太多,这里只记关键方法和变量)。

/**
 * 拓展加载器,Dubbo使用的扩展点获取
 * <ul>
 *      <li>自动注入关联扩展点。</li>
 *      <li>自动Wrap上扩展点的Wrap类。</li>
 *      <li>缺省获得的的扩展点是一个Adaptive Instance。</li>
 * </ul>
 *
 * 另外,该类同时是 ExtensionLoader 的管理容器,例如 {@link #EXTENSION_INSTANCES} 、{@link #EXTENSION_INSTANCES} 属性。
 * @see <a href="http://java.sun.com/j2se/1.5.0/docs/guide/jar/jar.html#Service%20Provider">Service Provider in Java 5</a>
 * @see com.alibaba.dubbo.common.extension.SPI
 * @see com.alibaba.dubbo.common.extension.Adaptive
 * @see com.alibaba.dubbo.common.extension.Activate
 */
public class ExtensionLoader<T> {
    ing……
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LxyrichBos

老板~坐下喝杯茶啊~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值