Dubbo 自定义 Filter 编码实例

Dubbo的Filter机制为我们做应用的扩展设计提供了很多可能性,这里的Filter也是“责任链”机制的一种实现场景,作为Java码农,我们也经常接触到很多责任链的实现场景,如Tomcat进入Servlet前的filter,如Spring Aop代理的链式调用等。本篇文章通过一个实例来看看Dubbo自定义Filter的代码具体如何编写。

我们为Dubbo实现Filter扩展的时候,一般是分为两个方向来做

1.服务消费者发起调用的流程

2.服务提供者接收请求的流程

如上图所示,无论当服务消费者发起调用时,还是服务提供者接收请求时,都会走过一条filter链,我们要做的就是往这个filter链中加入我们自己的filter,也就是图中的“自定义FIlter”这块内容。

本次编码目标

实现功能:

  • 服务消费者发起调用时,打印请求信息,收到返回结果后,打印返回信息与整体调用耗时。
  • 服务提供者收到请求时,打印请求信息,Service方法执行完后,打印返回信息与Service方法执行耗时。
  • 服务消费者在发起调用时,带上trace_id,服务提供者收到请求后,拿到trace_id并通过MDC加到本次执行线程中,打印日志时打印出trace_id。以便通过日志跟踪一次请求的完整链路。

编码实现

项目结构如下,一个consumer服务,一个provider服务,外加一个api接口的定义包。

api中定义了一个接口,用来根据userId获取用户信息

public interface IUserService {
    UserInfoDTO getUserInfo(Integer uerId);
}

provider项目中有这个接口的实现类,并向外暴露作为Dubbo的一个服务提供者。

@DubboService
@Slf4j
public class UserServiceImpl implements IUserService {

    @Override
    public UserInfoDTO getUserInfo(Integer userId) {
        log.info("进入到getUserInfo方法了");

        RoleDTO rolePO1 = new RoleDTO(1,"质检员");
        RoleDTO rolePO2 = new RoleDTO(2,"初审员");
        return new UserInfoDTO(userId,"张三",Arrays.asList(rolePO1,rolePO2));
    }

}

涉及到的pojo类就不再展示了,Service功能不是本次的编码的关注点,所以简单点,所有的userId都返回同样的用户数据,都叫“张三”,角色都是“质检员、初审员”。

接口定义及实现类有了,接下来就开始编写Filter。

Filter类需要实现接口org.apache.dubbo.rpc.Filter,实现invoke接口。

1.消费端编码

trace_id相关的Filter实现

这个Filter实现的功能为,将当前线程中的trace_id加入到RpcContext的附加参数中,当RPC请求发出后,服务端也能够从RpcContext的附加参数中获取到这个trace_id。

public class DubboConstant {
    public static final String TRACE_ID = "trace_id";
}
@Slf4j
@Activate(group = CommonConstants.CONSUMER, order = 1)
public class ConsumerTraceFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext rpcContext = RpcContext.getContext();
        // 首先获取当前线程中的traceId,如果为空的话就new一个
        String traceId = MDC.get(DubboConstant.TRACE_ID);
        if(StringUtils.isBlank(traceId)) {
            traceId = UUID.fastUUID().toString();
        }
        rpcContext.setAttachment(DubboConstant.TRACE_ID,traceId);
        return invoker.invoke(invocation);
    }
}

日志相关的Filter实现

这个Filter实现的功能为,在发起RPC请求前,将远程服务的ip端口及本次请求的参数打印出来。在收到返回结果后,打印本次调用耗时及结果信息。

@Slf4j
@Activate(group = CommonConstants.CONSUMER, order = 2)
public class ConsumerLogFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String host = invoker.getUrl().getHost();
        int port = invoker.getUrl().getPort();

        String serviceName = invocation.getTargetServiceUniqueName();
        String methodName = invocation.getMethodName();
        Object[] arguments = invocation.getArguments();

        // 这里的traceId是在TraceFilter放进到RpcContext里的
        String traceId = RpcContext.getContext().getAttachment(DubboConstant.TRACE_ID);

        log.info("[send request] trace_id:{}, host:{}, port:{}, serviceName:{}, methodName:{}, arguments:{}", traceId, host, port, serviceName, methodName, arguments);

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Result result = invoker.invoke(invocation);
        stopWatch.stop();
        long totalTimeMillis = stopWatch.getTotalTimeMillis();

        log.info("[receive response] trace_id:{}, elapsed time:{}ms, response:{}", traceId, totalTimeMillis, result.getValue().toString());
        return result;
    }
}

 SPI配置

要想使自定义的Filter生效,需要将Filter声明到一个文件中,dubbo会自动去这个文件中加载你设置的Filter,如果你写了Filter却没有声明到这个文件中,那么它也是不生效的。

这个文件就是  resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter ,这是一个纯文本文件,内容如下

rpcLogFilter=com.hml.consumer.filter.ConsumerLogFilter
traceFilter=com.hml.consumer.filter.ConsumerTraceFilter

每个Filter都单独占一行,每行的格式为    [FIlter名字]:[自定义Filter的全限定名]。 其中Filter名字可以随便定义,没有固定格式。

这里我们将刚才写的两个FIlter加进去了。

另外,有一点要注意,这是我的实际踩坑经验,那就是META-INF.dubbo这个东西,它是两个文件夹,在使用idea建文件夹的时候最好一个一个建,先建META-INF,再建dubbo。否则,如果只建一个文件,文件名叫“META-INF.dubbo”的话,idea不会替你分成两个文件夹,它就仍然只是一个文件夹,文件名就叫“META-INF.dubbo”,Dubbo项目启动时也不会去找这个文件夹,注意注意!!!

配置Filter的生效范围

Filter的生效范围 首先分为两类:1.在服务消费者发起调用时生效,2.在服务提供者接收请求时生效

而每个大类里有分为两种生效范围:1.针对某个接口类下的接口生效 2.针对所有接口生效

官方提供了两种方式来配置FIlter的生效范围:通过配置文件的方式 和 通过注解的方式

目前大家暴露或调用dubbo接口的方式一般是使用注解了,几乎不用xml形式的配置了,所以对于1,3 两种针对单个接口的方式,一般是直接声明在 @DubboReference 或 @DubboService 注解中,例如

@DubboReference(filter = {"traceFilter"})

@DubboService(filter = "xxxFilter")

对于2,4 两种全局Filter,目前两种方式都有见到(我们本次编码的FIlter都属于全局Filter)

通过配置文件的方式

在application.properties配置文件中加入如下配置

// 配置服务消费者调用时生效的Filter

dubbo.consumer.filter=traceFilter,rpcLogFilter

// 配置服务提供者接收请求时生效的FIlter

dubbo.provider.filter=xxxFilter

traceFilter与rpcLogFilter就是我们在resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter 这个文件中给FIlter设置的名字。

两个Filter的调用顺序就是上面的配置配好的顺序,traceFilter在前,rpcLogFilter在后。

通过注解的方式

使用@Activate注解同样也可以声明FIlter的生效范围及排序,我本次编码使用的就是该注解,在展示Filter代码时已经展示出来了。

例如

group代表了该FIlter的生效范围

对于group的值,我们一般使用org.apache.dubbo.common.constants.CommonConstants中设置好的值,常用到的就是这两个值

  • CommonConstants.CONSUMER 代表服务消费者发起调用时生效
  • CommonConstants.PROVIDER 代表服务提供者接收请求时生效
order代表了该Filter在自定义FIlter链中的顺序

order是个int值,值越小,顺序越靠前,越先被执行

Consumer编码总结

上面描述的自定义Filter编码过程其实一共分成了三步,三步完成后,Consumer端的Filter就配置好了

  1. 首先编码自定义FIlter类。
  2. 在resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter这个配置文件中声明自定义Filter。
  3. 通过注解或配置文件的方式,声明Filter的生效范围及执行顺序。

2.服务提供端编码

服务提供端的FIlter编码同消费端编码,也是分为三步。第二步和第三步没什么可说的了,我们这里仅看FIlter的实现即可。

trace_id相关的Filter实现

消费端通过ConsumerTraceFilter已经将trace_id放到RpcContext里了,服务提供端要干的事就是从RpcContext中将 trace_id 取出来,并通过MDC放到本次执行线程中。

@Slf4j
@Activate(group = CommonConstants.PROVIDER, order = 1)
public class ProviderTraceFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext rpcContext = RpcContext.getContext();
        String traceId = rpcContext.getAttachment(DubboConstant.TRACE_ID);
        if(StringUtils.isNotBlank(traceId)) {
            MDC.put(DubboConstant.TRACE_ID,traceId);
        }
        Result result = invoker.invoke(invocation);

        // 因为线程可能会被复用,所以在一次请求结束后要移除该线程ThreadLocal中的trace_id
        MDC.remove(DubboConstant.TRACE_ID);
        return result;
    }
}

将服务提供端的日志格式设置一下,打印出 trace_id

logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] trace_id:[%X{trace_id}] %logger : %msg%n

日志相关的Filter实现

这个Filter实现的功能为,在收到RPC请求后,将本次请求的参数打印出来。在拿到service方法的返回结果后,打印本次service方法执行耗时及返回结果。

@Slf4j
@Activate(group = CommonConstants.PROVIDER, order = 2)
public class ProviderLogFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String host = invoker.getUrl().getHost();
        int port = invoker.getUrl().getPort();

        String serviceName = invocation.getTargetServiceUniqueName();
        String methodName = invocation.getMethodName();
        Object[] arguments = invocation.getArguments();

        String traceId = RpcContext.getContext().getAttachment(DubboConstant.TRACE_ID);

        log.info("[receive request] trace_id:{}, host:{}, port:{}, serviceName:{}, methodName:{}, arguments:{}", traceId, host, port, serviceName, methodName, arguments);

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Result result = invoker.invoke(invocation);
        stopWatch.stop();
        long totalTimeMillis = stopWatch.getTotalTimeMillis();

        log.info("[send response] trace_id:{}, elapsed time:{}ms, response:{}", traceId, totalTimeMillis, result.getValue().toString());
        return result;
    }
}

效果展示

启动provider服务,consumer通过测试类的形式启动服务并发起调用

测试类代码

@SpringBootTest
public class UserTest {

    @DubboReference
    IUserService userService;

    @Test
    public void userTest() {
        UserInfoDTO userInfo = userService.getUserInfo(1);
        System.out.println(JSON.toJSONString(userInfo));
    }
}

测试方法成功执行完毕,日志展示

consumer端日志

2024-06-03 15:47:17.853  INFO 52493 --- [           main] c.hml.consumer.filter.ConsumerLogFilter  : [send request] trace_id:41005892-cdd1-4f0c-af13-c7d3aef4cdf0, host:192.168.40.24, port:20880, serviceName:com.hml.api.user.IUserService, methodName:getUserInfo, arguments:[1]
2024-06-03 15:47:17.900  INFO 52493 --- [           main] c.hml.consumer.filter.ConsumerLogFilter  : [receive response] trace_id:41005892-cdd1-4f0c-af13-c7d3aef4cdf0, elapsed time:46ms, response:UserInfoDTO(id=1, name=张三, roles=[RoleDTO(roleId=1, roleName=质检员), RoleDTO(roleId=2, roleName=初审员)])
{"id":1,"name":"张三","roles":[{"roleId":1,"roleName":"质检员"},{"roleId":2,"roleName":"初审员"}]}

由于consumer我没有设置日志格式,所以还是默认格式。

provider端日志

2024-06-03 15:47:17 INFO  [DubboServerHandler-192.168.40.24:20880-thread-2] trace_id:[41005892-cdd1-4f0c-af13-c7d3aef4cdf0] com.hml.provider.filter.ProviderLogFilter : [receive request] trace_id:41005892-cdd1-4f0c-af13-c7d3aef4cdf0, host:192.168.40.24, port:20880, serviceName:com.hml.api.user.IUserService:0.0.0, methodName:getUserInfo, arguments:[1]
2024-06-03 15:47:17 INFO  [DubboServerHandler-192.168.40.24:20880-thread-2] trace_id:[41005892-cdd1-4f0c-af13-c7d3aef4cdf0] com.hml.provider.user.UserServiceImpl : 进入到getUserInfo方法了
2024-06-03 15:47:17 INFO  [DubboServerHandler-192.168.40.24:20880-thread-2] trace_id:[41005892-cdd1-4f0c-af13-c7d3aef4cdf0] com.hml.provider.filter.ProviderLogFilter : [send response] trace_id:41005892-cdd1-4f0c-af13-c7d3aef4cdf0, elapsed time:1ms, response:UserInfoDTO(id=1, name=张三, roles=[RoleDTO(roleId=1, roleName=质检员), RoleDTO(roleId=2, roleName=初审员)])

可以看到,trace_id 的传递机制已经实现,consumer与provider的请求、返回 日志也打印出来了,本次编码目标-实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/675385.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

性能飙升50%,react-virtualized-list如何优化大数据集滚动渲染

在处理大规模数据集渲染时&#xff0c;前端性能常常面临巨大的挑战。本文将探讨 react-virtualized-list 库如何通过虚拟化技术和 Intersection Observer API&#xff0c;实现前端渲染性能飙升 50% 的突破&#xff01;除此之外&#xff0c;我们一同探究下该库还支持哪些新的特性…

自友科技破解走班教育排课难题

新高考后&#xff0c;校园教务都面临着晋级&#xff0c;其中走班教育的分班排课是个巨大的挑战。 所以在分班排课的时候要清楚一下几个问题 一是&#xff1a;清楚的核算学生的选考科目。学生选科提交后做好并承认&#xff0c;最好是在分班后不要改或很少的一部分人改动。 二是…

手写防抖debounce

手写防抖debounce 应用场景 当需要在事件频繁触发时&#xff0c;只执行最后一次操作&#xff0c;可以使用防抖函数来控制函数的执行频率,比如窗口resize事件和输入框input事件&#xff1b; 这段代码定义了一个名为 debounce 的函数&#xff0c;它接收两个参数&#xff1a;fn…

linux中最基础使用的命令

小白学习记录&#xff1a; 前情提要&#xff1a;Linux命令基础格式!查看 ls看目录的小技巧 进入指定目录 cd查看当前工作目录 pwd创建一个新的目录(文件夹&#xff09; mkdir创建文件 touch查看文件内容 cat、more操作文件、文件夹- 复制 cp- 移动 mv- 删除【危险操作&#xff…

Scrum 的速度如何衡量和提高

了解你的 Scrum 团队的实际开发速度是非常多敏捷团队的诉求&#xff0c;而速度&#xff08;Velocity&#xff09;作为敏捷项目的度量工具&#xff0c;为管理者提供了对团队工作能力深入了解的机会。 这份指南将深入探讨 Scrum 中速度的概念&#xff0c;指导你如何进行计算&…

cURL error 60: SSL certificate problem: unable to get local issuer certifica

本地小程序把接口换到本地的服务器接口&#xff0c;然后就报错了&#xff1a; cURL error 60: SSL certificate problem: unable to get local issuer certificate (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) 经查询查到&#xff1a;此问题的出现是由于没有配…

5月更新!优维EasyOps®平台7大新功能上线~

5月&#xff0c;优维EasyOps全平台产品能力又升级啦&#xff01;&#x1f44f; 快来看看都有新增的功能与优化吧&#xff01;&#x1f447; 重点升级 架构可观测 1.系统监控态势感知 过去&#xff0c;用户在使用监控平台的过程中&#xff0c;存在如下问题&#xff1a; 告警…

基于单片机的超声波倒车雷达设计

摘 要&#xff1a;文 章设计了一种基于单片机的超声波倒车雷达系统&#xff0c;以 AT89C51 型单片机作为控制核心&#xff0c;集距离测量、显示&#xff0c;方位显示和危险报警于一体&#xff0c;以提高驾驶者在倒车泊车时的安全性和舒适性。本设计采用 Keil 软件对系统程序…

详解:重庆耶非凡的选品师项目有哪些优势?

在竞争激烈的电商市场中&#xff0c;重庆耶非凡科技有限公司凭借其独特的选品师项目&#xff0c;成功地在众多企业中脱颖而出。这一项目不仅体现了公司对市场趋势的敏锐洞察力&#xff0c;更彰显了其专业的选品能力和对消费者需求的深刻理解。 首先&#xff0c;耶非凡的选品师项…

军用电源性能测试有哪些测试项目?需要遵循什么标准?

为了确保军用电源在极端条件下能够正常工作&#xff0c;必须对其进行一系列严格的性能测试。这些测试不仅包括效率、电压调整率和负载调整率等基本参数的测试&#xff0c;还包括动态响应能力、绝缘电阻、耐压测试、温度系数以及高低温循环等综合性能的评估。 测试项目 效率 电压…

【Python Cookbook】S01E15 将名称映射到序列的元素中

目录 问题解决方案讨论 问题 对于访问列表或元组中的元素&#xff0c;我们通常使用索引或者下标的方法。但是这明显会降低代码的可阅读性。如果我们想通过命名来提高代码的可阅读性&#xff0c;减少结构中对位置的依赖&#xff0c;怎么做&#xff1f; 解决方案 python 提供 …

vscode运行命令报错:标记“”不是此版本中的有效语句分隔符。

1. 报错问题 标记“&&”不是此版本中的有效语句分隔符。 2. 解决办法 将 terminal 中的 owershell 改成 cmd 就 ok

我们如何收到卫星信号?(导航电文,载波与测距码)

卫星信号 在介绍所有卫星信号之前&#xff0c;首先要明确一些概念&#xff1a; 所有的卫星信号&#xff0c;都是一段电磁波&#xff0c;用户接收的&#xff0c;也是一段电磁波。 但是我们认知中的电磁波&#xff0c;就是一段波&#xff0c;就像我们打出去的交一样&#xff0c…

Vue——监听器简单使用与注意事项

文章目录 前言编写简单demo注意事项 前言 监听器&#xff0c;在官网中称为侦听器&#xff0c;个人还是喜欢称之为监听器。官方文档如下&#xff1a; vue 官网 侦听器 编写简单demo 侦听器在项目中通常用于监听某个属性变量值的变化&#xff0c;并根据该变化做出一些处理操作。…

ENVI 5.3/6.0打开Landsat 8/9 C2L2级别数据(带有Metadata),附常见问题

ENVI 5.3/6.0打开Landsat 8/9 C2L2级别数据&#xff08;带有Metadata&#xff09; 文章目录 ENVI 5.3/6.0打开Landsat 8/9 C2L2级别数据&#xff08;带有Metadata&#xff09;前言数据下载ENVI 5.3打开Landsat 8 C2L2级别数据ENVI 5.3打开Landsat 9 C2L2级别数据ENVI 6.0打开La…

vscode 默认终端(Terminal) 为CMD,但是新建是powerShell

☆ 问题描述 vscode 默认终端&#xff08;Terminal&#xff09; 为CMD&#xff0c;但是新建是powerShell ★ 解决方案 随便设置其他为默认&#xff0c;然后再设置回来CMD为默认就行了&#xff0c;实在不行就重装vscode吧… ✅ 总结 应该是vscode的小bug

海量消息下王者荣耀在 TDMQ Pulsar 的实践

关于王者荣耀 《王者荣耀》是由腾讯游戏开发的一款运营在Android、IOS平台上的MOBA类手游&#xff0c;属于多人联机在线竞技类游戏&#xff0c;于2015年11月26日在Android、IOS平台上正式公测。上线以来受到广大手游玩家的热爱&#xff0c;目前该游戏在手游排行中处于TOP 1的位…

【IDEA】-使用IDEA查看类之间的依赖关系

1、父子类的继承、实现关系 1.1、使用CTRL Alt U 选择 java class 依据光标实际指向的类位置 用实心箭头表示泛化关系 是一种继承的关系&#xff0c;指向父类 可以提前设置需要显示的类的属性、方法等信息 快捷键 Ctrl Alt S &#xff0c;然后搜索 Diagrams 1.2、使用…

LeetCode刷题 | Day 1 最大子序列求和(Largest K Subsequence Sum)

LeetCode刷题 | Day 1 最大子序列求和(Largest K Subsequence Sum) 文章目录 LeetCode刷题 | Day 1 最大子序列求和(Largest K Subsequence Sum)前言一、题目概述二、解题方法2.1 贪心思路2.1.1 思路讲解2.1.2 伪代码 + 逐步输出示例2.1.3 Python代码如下2.1.4 C++代码如下…

【数据密集型系统设计】软件系统的可靠性、可伸缩性、可维护性

文章目录 一. 数据密集型程序的特点以及遇到的问题二. 可靠性 : 即使出现问题&#xff0c;也能继续正确工作1 硬件故障2. 软件错误3. 人为错误 二. 可伸缩性1. 描述负载与推特的例子2. 描述性能-延迟和响应时间3. 应对负载的方法 四. 可维护性1. 可操作性&#xff1a;人生苦短&…