从源码全面解析 dubbo 服务端服务调用的来龙去脉

  • 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
  • 📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列、duubo源码系列
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
  • 📝联系方式:hls1793929520,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

在这里插入图片描述

文章目录

    • 一、引言
    • 二、服务端调用流程
      • 1、启动封装
        • 1.1 过滤器的封装
        • 1.2 Hander的封装
      • 2、服务调用
        • 2.1 Handler调用
          • 2.1 MultiMessageHandler
          • 2.2 HeartbeatHandler
          • 2.3 AllChannelHandler
          • 2.4 DecodeHandler
          • 2.5 HeaderExchangeHandler
        • 2.2 过滤器调用
        • 2.3 方法调用
      • 三、流程图
    • 四、总结

一、引言

对于 Java 开发者而言,关于 dubbo ,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。

但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。

本期 dubbo 源码解析系列文章,将带你领略 dubbo 源码的奥秘

本期源码文章吸收了之前 SpringKakfaJUC源码文章的教训,将不再一行一行的带大家分析源码,我们将一些不重要的部分当做黑盒处理,以便我们更快、更有效的阅读源码。

虽然现在是互联网寒冬,但乾坤未定,你我皆是黑马!

废话不多说,发车!

二、服务端调用流程

对于整个服务端调用来说,主要分为两部分:

  • 服务端启动时:封装的 HandlerFilter
  • 服务端调用时:经过 Handler,然后经过 Filter,最后调用目标方法

1、启动封装

我们启动的时候,会经过 Exporter<?> exporter = protocolSPI.export(invoker) ,走 Dubbo SPI 的扩展机制

1.1 过滤器的封装

首先是我们的过滤器的封装:ProtocolFilterWrapper

public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    FilterChainBuilder builder = getFilterChainBuilder(invoker.getUrl());
    // buildInvokerChain:
    return protocol.export(builder.buildInvokerChain(invoker, SERVICE_FILTER_KEY, CommonConstants.PROVIDER));
}

public <T> Invoker<T> buildInvokerChain(final Invoker<T> originalInvoker, String key, String group) {
    // 获取所有的过滤器
    filters = ScopeModelUtil.getExtensionLoader(Filter.class, moduleModels.get(0)).getActivateExtension(url, key, group);
    // 如果当前的过滤器不为空
    if (!CollectionUtils.isEmpty(filters)) {
        // 将所有的过滤器封装成链表(last)的形式
        for (int i = filters.size() - 1; i >= 0; i--) {
            final Filter filter = filters.get(i);
            final Invoker<T> next = last;
            last = new CopyOfFilterChainNode<>(originalInvoker, next, filter);
        }
        // 将过滤器链表封装成CallbackRegistrationInvoker类型
        return new CallbackRegistrationInvoker<>(last, filters);
    } 
}

到这里,我们将过滤器封装成一个链表并且将其封装成 CallbackRegistrationInvoker 形式

我们直接跳到 DubboProtocol.export 中:

public <T> Exporter<T> export(Invoker<T> invoker){
    // 得到当前注册Zookeeper的URL
    URL url = invoker.getUrl();
    // 根据URL得到唯一的key:cn/com.common.service.IUserService:1.0.0.test:20883
    // 分组(group) + 接口(intfenerce) + 版本(1.0.0.test) + 端口号(20883)
    String key = serviceKey(url);
    // 
    DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
}

public DubboExporter(Invoker<T> invoker, String key, Map<String, Exporter<?>> exporterMap) {
    super(invoker);
    this.key = key;
    this.exporterMap = exporterMap;
    // 将当前的过滤器放至exporterMap中,方便我们后面的获取
    exporterMap.put(key, this);
    
    // 打开服务
    openServer(url);
    optimizeSerialization(url);
    return exporter;
}

到这里,我们的过滤器就封装到了 exporterMap 里面,后面会用到,我们后面再聊

1.2 Hander的封装

在我们上面封装完过滤器之后,我们会进行 openServer 打开服务这个操作,该操作会进行 Handler 的封装并启动我们的 Netty 服务

这里的Handler 一共封装成下面的流程:

NettyServerhandler -> NettyServer -> MultiMessageHandler--->HeartbeatHandler---->AllChannelHandler -> DecodeHandler  -> HeaderExchangeHandler -> ExchangeHandlerAdapter

最终会走到 NettyServerdoOpen 方法:这里对 Netty 不太清楚的,可以看博主的 Netty 源码文章:【Netty 从成神到升仙系列 大结局】全网一图流死磕解析 Netty 源码

// 典型的Netty启动的流程
protected void doOpen() throws Throwable {
    bootstrap = new ServerBootstrap();

    bossGroup = createBossGroup();
    workerGroup = createWorkerGroup();

    final NettyServerHandler nettyServerHandler = createNettyServerHandler();
    channels = nettyServerHandler.getChannels();

    // 初始化我们的服务端启动器
    initServerBootstrap(nettyServerHandler);

    ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
    channelFuture.syncUninterruptibly();
    channel = channelFuture.channel();
}

protected void initServerBootstrap(NettyServerHandler nettyServerHandler) {
    boolean keepalive = getUrl().getParameter(KEEP_ALIVE_KEY, Boolean.FALSE);

    bootstrap.group(bossGroup, workerGroup)
        .channel(NettyEventLoopFactory.serverSocketChannelClass())
        .option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
        .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
        .childOption(ChannelOption.SO_KEEPALIVE, keepalive)
        .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
                NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
                if (getUrl().getParameter(SSL_ENABLED_KEY, false)) {
                    ch.pipeline().addLast("negotiation", new SslServerTlsHandler(getUrl()));
                }
                // 这里添加Netty的Handler责任链
                ch.pipeline()
                    // 解码器
                    .addLast("decoder", adapter.getDecoder())
                    // 编码器
                    .addLast("encoder", adapter.getEncoder())
                    .addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
                    // 把上面封装的Handler放到Netty中,便于我们的调用执行
                    .addLast("handler", nettyServerHandler);
            }
        });
}

这里如果对 Netty 责任链不熟悉的可参考:【Netty 从成神到升仙系列 五】Netty 的责任链真有这么神奇吗?

2、服务调用

我们上篇文章剖析了 消费端 是如何进行的服务调用:从源码全面解析 dubbo 消费端服务调用的来龙去脉

这篇我们来看下服务端是如何进行服务调用的

2.1 Handler调用

我们直接跳到 NettyServerHandlerchannelRead 的方法

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
    // 主要看这个Handler做的事情
 
    handler.received(channel, msg);
    ctx.fireChannelRead(msg);
}

image-20230627221354346

我们从上图看到,重点是这五个 Handler

  • MultiMessageHandler:处理多个数据包发送的消息,也称为“多包消息”
  • HeartbeatHandler:定期发送“心跳”消息,以确保连接仍然存在并响应正常
  • AllChannelHandler:管理所有通道的打开和关闭
  • DecodeHandler:将二进制数据解码为消息对象。
  • HeaderExchangeHandler:负责协议头部的交换和处理

我们挨个去看看他们实际的作用

2.1 MultiMessageHandler
  • 如果当前的请求是多个的话,需要进行切割成单个请求往下传递
  • 如果是单个请求的话,直接向下传递即可
public void received(Channel channel, Object message) throws RemotingException {
    if (message instanceof MultiMessage) {
        MultiMessage list = (MultiMessage) message;
        for (Object obj : list) {
            try {
                handler.received(channel, obj);
            }
        }
    } else {
        handler.received(channel, message);
    }
}
2.2 HeartbeatHandler
  • 服务端:判断当前的请求是不是心跳请求,如果是心跳请求的话,发送心跳请求
  • 消费端:判断当前的请求是不是心跳请求,处理心跳请求
public void received(Channel channel, Object message) throws RemotingException {
    // 记录最近读取的时间
    setReadTimestamp(channel);
    // 判断当前的请求是不是心跳请求
    if (isHeartbeatRequest(message)) {
        Request req = (Request) message;
        if (req.isTwoWay()) {
            // 如果当前是同一个心跳检测则返回同一个响应
            Response res = new Response(req.getId(), req.getVersion());
            res.setEvent(HEARTBEAT_EVENT);
            channel.send(res);
            if (logger.isDebugEnabled()) {
                int heartbeat = channel.getUrl().getParameter(Constants.HEARTBEAT_KEY, 0);
            }
        }
        return;
    }
    // 消费端使用:处理心跳响应
    if (isHeartbeatResponse(message)) {
        return;
    }
    handler.received(channel, message);
}
2.3 AllChannelHandler
  • 为每一个服务端的请求从线程池中分配一个线程执行
public void received(Channel channel, Object message) throws RemotingException {
    // 获取线程
    ExecutorService executor = getPreferredExecutorService(message);
    try {
        // 将当前的Handler丢进线程池里面执行
        executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
    }
}

image-20230627224241967

2.4 DecodeHandler
  • 根据当前的响应请求进行解码操作
public void received(Channel channel, Object message) throws RemotingException {
    if (message instanceof Decodeable) {
        decode(message);
    }

    if (message instanceof Request) {
        decode(((Request) message).getData());
    }

    if (message instanceof Response) {
        decode(((Response) message).getResult());
    }

    handler.received(channel, message);
}
2.5 HeaderExchangeHandler
  • 调用我们的过滤器并等待数据的返回
  • 将数据通过 Channel 返回至客户端
public void received(Channel channel, Object message) throws RemotingException {
    final ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);
    // 服务端:当前的消息是请求
    if (message instanceof Request) {
        Request request = (Request) message;
        if (request.isEvent()) {
            handlerEvent(channel, request);
        } else {
            // 进行请求的解析
            if (request.isTwoWay()) {
                handleRequest(exchangeChannel, request);
            } else {
                handler.received(exchangeChannel, request.getData());
            }
        }
    } else if (message instanceof Response) {
        handleResponse(channel, (Response) message);
    } else {
        handler.received(exchangeChannel, message);
    }
}

void handleRequest(final ExchangeChannel channel, Request req) throws RemotingException {
    Response res = new Response(req.getId(), req.getVersion());
    // Request [id=17, version=2.0.2, twoWay=true, event=false, broken=false, data=RpcInvocation [methodName=getUserById, parameterTypes=[class java.lang.Long]]]
    Object msg = req.getData();
    // 执行过滤器的操作
    CompletionStage<Object> future = handler.reply(channel, msg);
    // 等待数据的返回
    future.whenComplete((appResult, t) -> {
        try {
            if (t == null) {
                res.setStatus(Response.OK);
                res.setResult(appResult);
            } else {
                res.setStatus(Response.SERVICE_ERROR);
                res.setErrorMessage(StringUtils.toString(t));
            }
            // 没有问题的话,将我们的数据返回
            channel.send(res);
        } 
    });
}

服务端解码之后的消息:

image-20230627225422895

2.2 过滤器调用

我们直接跳到 DubboProtocolreply 方法

public CompletableFuture<Object> reply(ExchangeChannel channel, Object message){
    Invocation inv = (Invocation) message;
    // 得到过滤器的invoker
    Invoker<?> invoker = getInvoker(channel, inv);
    // 执行过滤器
    Result result = invoker.invoke(inv);
    // 返回结果
    return result.thenApply(Function.identity());
}

Invoker<?> getInvoker(Channel channel, Invocation inv){
    // 根据组+版本号+接口确定唯一的key
    String serviceKey = serviceKey(port,path,(String) inv.getObjectAttachmentWithoutConvert(VERSION_KEY),(String) inv.getObjectAttachmentWithoutConvert(GROUP_KEY));
    // 得到过滤器(我们在上面进行过对应的添加)
    DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);
    // 返回过滤器
    return exporter.getInvoker();
}

这里的过滤器总共十个,这里不带大家细看每一个的源码了,了解意思即可

  • ContextFilter:负责将请求上下文传递给请求处理流程中的其他组件
  • ProfilerServerFilter:负责性能分析和监视
  • EchoFilter:将请求消息作为响应消息返回
  • ClassLoaderFilter:负责加载和管理类加载器
  • GenericFilter:提供一种通用的请求处理机制
  • ExceptionFilter:负责处理异常并生成错误响应消息
  • MonitorFilter:负责监视请求处理过程中的状态和信息
  • TimeoutFilter:负责处理请求处理超时
  • TraceFilter:跟踪请求处理流程中的各个阶段和信息
  • ClassLoaderCallbackFilter:提供了回调函数的机制

2.3 方法调用

最终我们会到 AbstractProxyInvokerinvoke 方法

public Result invoke(Invocation invocation){
    // 执行我们动态代理的方法
     Object value = doInvoke(proxy, invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
    // 等待返回的结果
     CompletableFuture<Object> future = wrapWithFuture(value, invocation);
    // 封装请求
    CompletableFuture<AppResponse> appResponseFuture = future.handle((obj, t) -> {
        AppResponse result = new AppResponse(invocation);
        if (t != null) {
            if (t instanceof CompletionException) {
                result.setException(t.getCause());
            } else {
                result.setException(t);
            }
        } else {
            result.setValue(obj);
        }
        // 返回数据
        // AppResponse [value=User(id=2, name=天涯, age=12), exception=null]
        return result;
    });
    return new AsyncRpcResult(appResponseFuture, invocation);
}

这里可以参考这篇文章:从源码全面解析 dubbo 服务暴露的来龙去脉

public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    try {
        final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
        return new AbstractProxyInvoker<T>(proxy, type, url) {
            @Override
            // proxy:实现类
            // methodName:getUserById
            // parameterTypes:class java.lang.Long
            // arguments:2
            // 执行相对应的方法即可
            protected Object doInvoke(T proxy, String methodName,
                                      Class<?>[] parameterTypes,
                                      Object[] arguments) throws Throwable {
                return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
            }
        };
    }
}

三、流程图

高清图片可私信博主

在这里插入图片描述

四、总结

鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。

其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。

如果你也对 后端架构和中间件源码 有兴趣,欢迎添加博主微信:hls1793929520,一起学习,一起成长

我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,喜欢后端架构和中间件源码。

我们下期再见。

我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。

往期文章推荐:

  • 美团二面:聊聊ConcurrentHashMap的存储流程
  • 从源码全面解析Java 线程池的来龙去脉
  • 从源码全面解析LinkedBlockingQueue的来龙去脉
  • 从源码全面解析 ArrayBlockingQueue 的来龙去脉
  • 从源码全面解析ReentrantLock的来龙去脉
  • 阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似
  • 从源码全面解析 ThreadLocal 关键字的来龙去脉
  • 从源码全面解析 synchronized 关键字的来龙去脉
  • 阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试

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

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

相关文章

Docker自学记录笔记

安装联系Docker命令 1. 搜索镜像 docker search nagin 2. 下载镜像 3. 启动nginx 强调文本 强调文本 加粗文本 加粗文本 标记文本 删除文本 引用文本 H2O is是液体。 210 运算结果是 1024. 插入链接与图片 链接: link. 图片: 带尺寸的图片: 居中的图片: 居中并…

OpenCV的remap实现图像垂直翻转

以下是完整的代码: #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <iostream>int main() {

Redis10大性能优化点(上)

1.Redis真的变慢了吗&#xff1f; 对 Redis 进行基准性能测试 例如&#xff0c;我的机器配置比较低&#xff0c;当延迟为 2ms 时&#xff0c;我就认为 Redis 变慢了&#xff0c;但是如果你的硬件配置比较高&#xff0c;那么在你的运行环境下&#xff0c;可能延迟是 0.5ms 时就…

flutter:实现一个简单的appBar上的搜索框、一个简单的搜索历史

搜索框 效果图 代码 import package:flutter/material.dart;class NovelSearch extends StatefulWidget {overrideState<StatefulWidget> createState() > _NovelSearchState(); }class _NovelSearchState extends State<NovelSearch> {String searchVal ;o…

Debian 12 静态IP / 固定IP的设置

环境&#xff1a;Debian 12 amd64-lxde 局域网&#xff1a;PT925E电信光猫 手机APP 网络管家 一般用动态IP就可以了&#xff0c;但如果软件环境比较小众&#xff0c;问题就随之而来。起始问题&#xff1a;路由器无法解析设备名和IP&#xff0c;网络管家也不让设置固定IP&…

基于深度学习的高精度水果检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度水果&#xff08;苹果、香蕉、葡萄、橘子、菠萝和西瓜&#xff09;检测识别系统可用于日常生活中或野外来检测与定位水果目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的水果目标检测识别&#xff0c;另外支持结果可视…

Python教程(2)——开发python常用的IDE

为什么需要IDE 在理解IDE之前&#xff0c;我们先做以下的实验&#xff0c;新建一个文件&#xff0c;输入以下代码 total_sum 0 for x in range(1,101):total_sum x print(total_sum)非常非常简单的一个程序&#xff0c;主要就是计算1加到100的值&#xff0c;我们将它重命名…

SpringBoot第19讲:SpringBoot 如何保证接口幂等

SpringBoot第19讲&#xff1a;SpringBoot 如何保证接口幂等 在以SpringBoot开发Restful接口时&#xff0c;如何防止接口的重复提交呢&#xff1f; 本文是SpringBoot第19讲&#xff0c;主要介绍接口幂等相关的知识点&#xff0c;并实践常见基于Token实现接口幂等。 文章目录 Spr…

动态规划之343 整数拆分(第6道)

题目&#xff1a; 给定一个正整数 n &#xff0c;将其拆分为 k 个 正整数 的和&#xff08; k > 2 &#xff09;&#xff0c;并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。 示例&#xff1a; 解法&#xff1a; 其实可以从1开始遍历 j &#xff0c;然后有两种…

通用分页【下】(将分页封装成标签)

目录 一、debug调试 1、什么是debug调试&#xff1f; 2、debug调试步骤 3、实践 二、分页的核心 三、优化 分页工具类 编写servlet jsp代码页面&#xff1a; 分页工具类PageBean完整代码 四、分页标签 jsp代码 编写标签 tld文件 助手类 改写servlet 解析&…

CTFshow-pwn入门-栈溢出pwn49(静态链接pwn-mprotect函数的应用)

pwn49 首先我们先将pwn文件下载下来&#xff0c;然后赋上可执行权限&#xff0c;再来查看pwn文件的保护信息。 chomd x pwn checksec pwn file pwn我们可以看到这是一个32位的pwn文件&#xff0c;并且保护信息开启了NX和canary&#xff0c;也就是堆栈不可执行且有canary。最最…

html案例2

效果 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-width, initia…

redis如何实现持久化

RDB快照 RDB是一种快照存储持久化方式&#xff0c;具体就是将Redis某一时刻的内存数据保存到硬盘的文件当中&#xff0c;默认保存的文件名为dump.rdb&#xff0c;而在Redis服务器启动时&#xff0c;会重新加载dump.rdb文件的数据到内存当中恢复数据。 开启RDB持久化方式 开启…

缓存更新策略,先更新数据库还是缓存呢?

学了这么多&#xff0c;相信大家对缓存更新的策略都已经有了清晰的认识。最后稍稍总结一下。 缓存更新的策略主要分为三种&#xff1a; Cache aside Cache aside Cache aside也就是旁路缓存&#xff0c;是比较常用的缓存策略。 &#xff08;1&#xff09;读请求常见流程 应…

亿级日活业务稳如磐石 华为云发布性能测试服务CodeArts PerfTest

HDC期间可参与华为云PaaS生态抽奖活动&#xff0c;活动链接在文末 计算机软件作为人类逻辑智慧的伟大结晶之一&#xff0c;已经渗透到了人类社会的各个角落。早期的计算机发展对硬件有很强的依赖性&#xff0c;只有少数的个人或者机构才能拥有软件这种“奢侈品”。但随着软件行…

java 并发 随笔7 ThreadLocal源码走读

0. 刚刚见了下老朋友&#xff0c;桌球撞起来的感觉很爽 可以看到 Thread 是内部是维护了局部变量的(thread-local-map) 1. 源码走读 很多的细节都在代码块中备注了 package java.lang;// 现在回来起来&#xff0c;很多经验不太丰富的人之所以在接触、学习java.lang.thread的…

TiDB(1):TiDB简介

1 从MySQL到TiDB 1.1 场景引入 假设现在有一个高速发展的互联网公司,核心业务库MySQL的数据量已经近亿行,且还在不断增长中,公司对于数据资产较为重视,所有数据要求多副本保存至少5年,且除了有对历史数据进行统计分析的离线报表业务外,还有一些针对用户数据实时查询的需求,如用…

通信算法之171: LTE 不同带宽参数

转载&#xff1a; LTE不同带宽配置下的对应的采样率&#xff1a; < Sampling Time > 20 Mhz BW Case : Ts 1 sec / 30.72 Mhz 1s/30,720,000 Hz 0.0326 us 32.6 ns 15 Mhz BW Case : T15 sec / 23.04 Mhz 1s/23,040,000 Hz 0.0434 us 43.4 ns 10 Mhz BW Case :…

记一次 Visual Studio 2022 卡死分析

一&#xff1a;背景 1. 讲故事 最近不知道咋了&#xff0c;各种程序有问题都寻上我了&#xff0c;你说 .NET 程序有问题找我能理解&#xff0c;Windows 崩溃找我&#xff0c;我也可以试试看&#xff0c;毕竟对 Windows 内核也知道一丢丢&#xff0c;那 Visual Studio 有问题找…

使用JMX管理Spring Bean

1.使用JMX管理Spring Bean 2.spring通过annotation注解注册MBean到JMX实现监控java运行状态 3.Spring与JMX集成