207、SpringBoot 整合 RabbitMQ 实现消息的发送 与 接收(监听器)

news2025/1/29 14:04:30

目录

  • ★ 发送消息
  • ★ 创建队列的两种方式
  • 代码演示
    • 需求1:发送消息
      • 1、ContentUtil 先定义常量
      • 2、RabbitMQConfig 创建队列的两种方式之一:
        • 配置式:
          • 问题:
      • 3、MessageService 编写逻辑
      • PublishController 控制器
      • application.properties 配置属性
      • 测试:消息发送
  • ★ 接收消息
    • 代码演示:
      • 测试: 消息接收
  • ★ 定制监听器容器工厂
  • 完整代码:
    • application.properties RabbitMQ的连接等属性配置
    • ContentUtil 常量工具类
    • RabbitMQConfig 配置式创建消息队列
    • MessageService 发送消息的业务代码
    • PublishController.java 发送消息的控制层
    • MyRabbitMQListener 监听器,监听消息队列
    • pom.xml


在这里插入图片描述

★ 发送消息

- Spring Boot可以将AmqpAdmin和AmqpTemplate注入任何其他组件,
  接下来该组件即可通过AmqpAdmin来管理Exchange、队列和绑定,还可通过AmqpTemplate来发送消息。 

- Spring Boot还会自动配置一个RabbitMessagingTemplate Bean(RabbitAutoConfiguration负责配置),
  如果想使用它来发送、接收消息,
  可使用RabbitMessagingTemplate代替上面的AmqpTemplate,两个Template的注入方式完全相同。

在这里插入图片描述

★ 创建队列的两种方式

 方式一(编程式):在程序中通过AmqpAdmin创建队列。

 方式二(配置式):在容器中配置 org.springframework.amqp.core.Queue 类型的Bean,
                  RabbitMQ将会自动为该Bean创建对应的队列。

代码演示

需求1:发送消息

1、ContentUtil 先定义常量

在这里插入图片描述

2、RabbitMQConfig 创建队列的两种方式之一:

配置式:

在容器中配置 org.springframework.amqp.core.Queue 类型的Bean,RabbitMQ将会自动为该Bean创建对应的队列。
就是在配置类中创建一个生成消息队列的@Bean。

问题:

用 @Configuration 注解声明为配置类,但是项目启动的时候没有自动生成这个队列。
据了解是因为RabbitMQ使用了懒加载,大概是没有消费者监听这个队列,就没有创建。
但是当我写后面的代码后,这个消息队列就生成了,但是并没有消费者去监听这个队列。
这有点想不通。
不知道后面是哪里的代码让这个配置类能成功声明这个消息队列出来。
在这里插入图片描述
水落石出:
经过测试:
在下面的MessageService 这个类中,依赖注入了 AmqpAdmin 和 AmqpTemplate 这两个对象,当我们通过这两个对象去声明队列、Exchange 和绑定的时候,配置类中的创建消息队列的bean就能成功创建队列。
这张图结合下面的 MessageService 中的代码就可得知:
这是依赖注入 AmqpAdmin 和 AmqpTemplate 这两个对象的有参构造器中声明的。
在这里插入图片描述

3、MessageService 编写逻辑

声明Exchange 、 消息队列 、 Exchange和消息队列的绑定、发送消息的方法等
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

PublishController 控制器

在这里插入图片描述

application.properties 配置属性

在这里插入图片描述
在这里插入图片描述

测试:消息发送

成功生成队列
在这里插入图片描述
发送消息测试
在这里插入图片描述
在这里插入图片描述




★ 接收消息

@RabbitListener 注解修饰的方法将被注册为消息监听器方法。

 【备注】:该注解可通过queues属性指定它要监听的、已有的消息队列,
  它也可使用queuesToDeclare来声明队列,并监听该队列。


 - 如果没有显式配置监听器容器工厂(RabbitListenerContainerFactory),
 Spring Boot会在容器中自动配置一个SimpleRabbitListenerContainerFactory Bean作为监听器容器工厂,
 如果希望使用DirectRabbitListenerContainerFactory,可在application.properties文件中添加如下配置:
  spring.rabbitmq.listener.type=direct

 ▲ 如果在容器中配置了MessageRecoverer或MessageConverter,
   它们会被自动关联到默认的监听器容器工厂。



代码演示:

创建个消息队列的监听器就可以了。

@RabbitListener 注解修饰的方法将被注册为消息监听器方法。

该注解可通过queues属性指定它要监听的、已有的消息队列
它也可使用queuesToDeclare来声明队列,并监听该队列
还可以用 bindings 进行 Exchange和queue的绑定操作。
在这里插入图片描述

测试: 消息接收

在这里插入图片描述
发送消息和监听消息
在这里插入图片描述




★ 定制监听器容器工厂

▲ 如果要定义更多的监听器容器工厂或覆盖默认的监听器容器工厂,

可通过Spring Boot提供的SimpleRabbitListenerContainerFactoryConfigurer
或DirectRabbitListenerContainerFactoryConfigurer来实现,

它们可对SimpleRabbitListenerContainerFactory
或DirectRabbitListenerContainerFactory进行与自动配置相同的设置。 

▲ 有了自定义的监听器容器工厂之后,可通过@RabbitListener注解的containerFactory属性
  来指定使用自定义的监听器容器工厂,
例如如下注解代码:

@RabbitListener(queues = "myQueue1", containerFactory="myFactory")

完整代码:

application.properties RabbitMQ的连接等属性配置

# 配置连接 RabbitMQ 的基本信息------------------------------------------------------
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
# 下面属性可配置多个以逗号隔开的连接地址,一旦配置了该属性,host 和 port 属性就会被忽略
# spring.rabbitmq.addresses=
spring.rabbitmq.username=ljh
spring.rabbitmq.password=123456
# 连接虚拟主机
spring.rabbitmq.virtual-host=my-vhost01

# 配置RabbitMQ的缓存相关信息--------------------------------------------------------
# 指定缓存 connection ,还是缓存 channel
spring.rabbitmq.cache.connection.mode=channel
# 指定可以缓存多少个 Channel
spring.rabbitmq.cache.channel.size=50
# 如果选择的缓存模式是 connection , 那么就可以配置如下属性
# spring.rabbitmq.cache.connection.size=15

# 配置 和 RabbitTemplate 相关的属性--------------------------------------------------
# 指定 RabbitTemplate 发送消息失败时会重新尝试
spring.rabbitmq.template.retry.enabled=true
# RabbitTemplate 发送消息失败后每隔1秒重新尝试发送消息
spring.rabbitmq.template.retry.initial-interval=1s
# RabbitTemplate 发送消息失败时,最多尝试重新发送消息的次数
spring.rabbitmq.template.retry.max-attempts=5
# 设置每次尝试重新发送消息的时间间隔是一个等比数列:1s, 2s, 4s, 8s, 16s
# 第一次等1s后尝试,第二次等2s后尝试,第三次等4s后尝试重新发送消息......
spring.rabbitmq.template.retry.multiplier=2
# 指定发送消息时默认的Exchange名
spring.rabbitmq.template.exchange=""
# 指定发送消息时默认的路由key
spring.rabbitmq.template.routing-key="test"

# 配置和消息监听器的容器工厂相关的属性--------------------------------------------------
# 指定监听器容器工厂的类型
spring.rabbitmq.listener.type=simple
# 指定消息的确认模式
spring.rabbitmq.listener.simple.acknowledge-mode=auto
# 指定获取消息失败时,是否重试
spring.rabbitmq.listener.simple.retry.enabled=true
# 发送消息失败时,最多尝试重新发送消息的次数
spring.rabbitmq.listener.simple.retry.max-attempts=2
# 发送消息失败后每隔1秒重新尝试发送消息
spring.rabbitmq.listener.simple.retry.initial-interval=1s

ContentUtil 常量工具类

package cn.ljh.app.rabbitmq.util;


//常量
public class ContentUtil
{
    //定义Exchange的常量-----fanout:扇形,就是广播类型
    public static final String EXCHANGE_NAME = "boot.fanout";

    //消息队列数组
    public static final String[] QUEUE_NAMES =new String[] {"queue_boot_01","queue_boot_02","queue_boot_03"};


}

RabbitMQConfig 配置式创建消息队列

package cn.ljh.app.rabbitmq.config;


import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


//配置式:在容器中配置 org.springframework.amqp.core.Queue 类型的Bean,RabbitMQ将会自动为该Bean创建对应的队列
//声明这个类为配置类
@Configuration
public class RabbitMQConfig
{

    //用配置式的方式在RabbitMQ中定义队列
    @Bean
    public Queue myQueue()
    {
        //在容器中配置一个 Queue Bean,Spring 就会为它在 RabbitMQ 中自动创建对应的 Queue
        return new Queue("queue_boot",   /* Queue 消息队列名 */
                true,         /* 是否是持久的消息队列 */
                false,       /* 是否是独占的消息队列,独占就是是否只允许该消息消费者消费该队列的消息 */
                false,     /* 是否在没有消息的时候自动删除消息队列 */
                null       /* 额外的一些消息队列的参数 */
        );
    }
}

MessageService 发送消息的业务代码

声明Exchange 、Queue ,Exchange 绑定Queue,发送消息代码

package cn.ljh.app.rabbitmq.service;

import cn.ljh.app.rabbitmq.util.ContentUtil;
import org.springframework.amqp.core.*;
import org.springframework.stereotype.Service;


//业务逻辑:声明Exchange 和 Queue 消息队列,发送消息的方法
@Service
public class MessageService
{
    //AmqpAdmin来管理Exchange、队列和绑定
    private final AmqpAdmin amqpAdmin;

    //AmqpTemplate来发送消息
    private final AmqpTemplate amqpTemplate;

    //通过有参构造器进行依赖注入
    public MessageService(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate)
    {
        this.amqpAdmin = amqpAdmin;
        this.amqpTemplate = amqpTemplate;

        //由于声明 Exchange 、 队列 、 绑定(Exchange绑定队列),都只需要做一次即可,因此放在此处构造器中完成即可
        //创建 fanout 类型的 Exchange ,使用FanoutExchange实现类
        FanoutExchange exchange = new FanoutExchange(
                ContentUtil.EXCHANGE_NAME,
                true,    /* Exchange是否持久化 */
                false, /* 是否自动删除 */
                null   /* 额外的参数属性 */
        );
        //声明 Exchange
        this.amqpAdmin.declareExchange(exchange);


        //此处循环声明 Queue ,也相当于代码式创建 Queue
        for (String queueName : ContentUtil.QUEUE_NAMES)
        {
            Queue queue = new Queue(queueName,   /* Queue 消息队列名 */
                    true,         /* 是否是持久的消息队列 */
                    false,       /* 是否是独占的消息队列,独占就是是否只允许该消息消费者消费该队列的消息 */
                    false,     /* 是否在没有消息的时候自动删除消息队列 */
                    null       /* 额外的一些消息队列的参数 */
            );
            //此处声明 Queue ,也相当于【代码式】创建 Queue
            this.amqpAdmin.declareQueue(queue);

            //声明 Queue 的绑定
            Binding binding = new Binding(
                    queueName,  /* 指定要分发消息目的地的名称--这里是要发送到这个消息队列里面去 */
                    Binding.DestinationType.QUEUE, /* 分发消息目的的类型,指定要绑定 queue 还是 Exchange */
                    ContentUtil.EXCHANGE_NAME, /* 要绑定的Exchange */
                    "x", /* 因为绑定的Exchange类型是 fanout 扇形(广播)模式,所以路由key随便写,没啥作用 */
                    null
                    );
            //声明 Queue 的绑定
            amqpAdmin.declareBinding(binding);
        }
    }

    //发送消息的方法
    public void publish(String content)
    {
        //发送消息
        amqpTemplate.convertAndSend(
                ContentUtil.EXCHANGE_NAME, /* 指定将消息发送到这个Exchange */
                "",  /* 因为Exchange是fanout 类型的(广播类型),所以写什么路由key都行,都没意义 */
                content /* 发送的消息体 */
        );
    }
}

PublishController.java 发送消息的控制层

package cn.ljh.app.rabbitmq.controller;

import cn.ljh.app.rabbitmq.service.MessageService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

//发送消息
@RestController
public class PublishController
{
    private final MessageService messageService;
    //有参构造器进行依赖注入
    public PublishController(MessageService messageService)
    {
        this.messageService = messageService;
    }

    @GetMapping("/publish/{message}")
    //因为{message}是一个路径参数,所以方法接收的时候需要加上注解 @PathVariable
    public String publish(@PathVariable String message)
    {
        //发布消息
        messageService.publish(message);
        return "消息发布成功";
    }

}

MyRabbitMQListener 监听器,监听消息队列

package cn.ljh.app.rabbitmq.listener;


import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

//监听器:监听消息队列并进行消费
@Component
public class MyRabbitMQListener
{
    //queues 指定监听已有的哪个消费队列
    @RabbitListener(queues = "queue_boot_01")
    public void onQ1Message(String message)
    {
        System.err.println("从 queue_boot_01 消息队列接收到的消息:" + message);
    }

    //queues 指定监听已有的哪个消费队列
    @RabbitListener(queues = "queue_boot_02")
    public void onQ2Message(String message)
    {
        System.err.println("从 queue_boot_02 消息队列接收到的消息:" + message);
    }

    //queues 指定监听已有的哪个消费队列
    //还可以用 queuesToDeclare 直接声明并监听该队列,还可以用 bindings 进行Exchange和queue的绑定
    @RabbitListener(queuesToDeclare = @Queue(name = "queue_boot_03"
            ,durable = "true"
            ,exclusive = "false"
            ,autoDelete = "false"),

            admin = "amqpAdmin" /*指定声明Queue,绑定Queue所用的 amqpAdmin,不指定的话就用容器中默认的那一个 */
    )
    public void onQ3Message(String message)
    {
        System.err.println("从 queue_boot_03 消息队列接收到的消息:" + message);
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
    </parent>
    <groupId>cn.ljh</groupId>
    <artifactId>rabbitmq_boot</artifactId>
    <version>1.0.0</version>
    <name>rabbitmq_boot</name>
    <properties>
        <java.version>11</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!-- RabbitMQ 的依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!-- web 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 开发者工具的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!-- lombok 依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

抖音开放平台第三方代小程序开发,授权事件、消息与事件通知总结

大家好&#xff0c;我是小悟 关于抖音开放平台第三方代小程序开发的两个事件接收推送通知&#xff0c;是开放平台代小程序实现业务的重要功能。 授权事件推送和消息与事件推送类型都以Event的值判断。 授权事件推送通知 授权事件推送包括&#xff1a;推送票据、授权成功、授…

java 基础 IO字符流

1.汉字存储占多少字节&#xff1a; public class IoTest {public static void main(String[] args) {String str "abcd";String str1 "吴危险学java";System.out.println("字符串转为byte数组&#xff1a;" Arrays.toString(str.getBytes())…

C# RestoreFormer 图像修复

效果 项目 代码 using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using OpenCvSharp; using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Windows.Forms;namespace 图像修复 {pu…

Python Opencv实践 - 车辆统计(1)读取视频,移除背景,做预处理

示例中的图像的腐蚀、膨胀和闭运算等需要根据具体视频进行实验得到最佳效果。代码仅供参考。 import cv2 as cv import numpy as np#读取视频文件 video cv.VideoCapture("../../SampleVideos/Traffic.mp4") FPS 10 DELAY int(1000 / FPS) kernel cv.getStructu…

【Jenkins使用】Jenkins 与 Git

一、概述 Jenkins 与 Git 的结合使用&#xff0c;可以理解为是 Jenkins 的一个强大之处。为什么要这么说&#xff0c;简要说明一下这个工作模式就能理解&#xff1a; 一个软件项目&#xff0c;开发过程中通常都会使用到一些源码管理工具&#xff0c;来达到团队协作的目的。而 …

C++ --STL

STL STL&#xff08;Standard Template Library,标准模板库&#xff09;STL从广义上分为&#xff1a; 容器&#xff08;container&#xff09;算法 (algorithm)迭代器 (iterator) 容器 和 算法之间通过迭代器进行无缝连接。STL几乎所有的代码都采用模板类或者模板函数 1、ST…

SystemC入门学习-第8章 测试平台的编写

之前的章节&#xff0c;一直把重点放在用SystemC来描述硬件电路上&#xff0c;即如何编写SystemC 的RTL。本章的注意力集中在验证和编写测试平台上。 重点包括&#xff1a; 如何生成时钟信号和激励波形如何编写有响应能力的测试平台如何记录仿真结果 8.1 编写测试平台 测试平…

【Shell】Shell脚本入门

Shell脚本入门 疑问 linux系统是如何操作计算机硬件CPU,内存,磁盘,显示器等? 答: 使用linux的内核操作计算机的硬件 Shell介绍 通过编写Shell命令发送给linux内核去执行, 操作就是计算机硬件. 所以Shell命令是用户操作计算机硬件的桥梁, Shell是命令, 类似于windows系统…

Java 抽象类与接口

一、抽象类 1.1 抽象类的声明 可以这样认为&#xff0c;抽象类就是普通类抽象化的结果&#xff0c;它与普通类相比&#xff0c;同样具有属性、方法等&#xff0c;唯一的区别就是抽象类具有抽象的效果&#xff0c;即无法被实例化&#xff08;如果可以被实例化&#xff0c;就失…

源代码漏洞监测【软件代码缺陷性检测】

本文仅供思路参考、交流 一、题目要求 利用树、图、序列等对软件源代码进行代码表征。利用深度学习实现对代码有无漏洞的分类实现检测漏洞类型调研过程 调研了一些论文,发现目前的一些论文,例如FUNDED、SemVulDet、SEVulDet、SySeVR都只能实现二分类,即有无代码漏洞,但是这…

qgis c++二次开发初始化介绍

前言 上篇文章qgis二次开发环境搭建(qgis-3.28.6qt5.15) 的末尾介绍了如何新建一个Qt工程&#xff0c;链接QGIS库并调用Qgis::releaseName()&#xff0c;工程代码在仓库qgis_cpp_api_apps中。 但是要调用更复杂的QGIS函数需要添加一些初始化函数&#xff0c;下边将介绍如何初…

【JUC】JMM

文章目录 1. 概述2. 三大特性2.1 可见性2.2 原子性2.3 有序性 3. 多线程对变量的读写过程4. 先行发生原则(happens-before) 1. 概述 CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存&#xff0c;而内存的读和写操作的时候就会造成不一致的问题 JVM规范中试图定义…

MoeCTF2023web

01http 打开题目环境 可以看到要求完成所有任务&#xff0c;这里用burp抓个包 按照要求修改可以得到flag moectf{basic_http_knowledge_HJbg427uFuznTqiJdtS1xhZNwpdsOnKU} 02 Web入门指北 直接找到结尾发现乱码&#xff0c;去解码 编码可以试试url编码和base64到16 这里用…

RISCV学习(2)玄铁C910处理器体验

笔者有幸参加了平头哥RISCV开发者大赛&#xff0c;体验了一下基于玄铁C910内核的矽速开发板。 1、开发板介绍 LicheePi 4A 是基于 Lichee Module 4A 核心板的 高性能 RISC-V Linux 开发板。 TH1520 为主控核心&#xff08;4xC9101.85G&#xff0c; RV64GCV&#xff0c;4TOPSi…

毕业图形采集【个人】

毕业图形采集【个人】 前言版权推荐毕业图形采集介绍步骤如何查看个人二维码 最后 前言 2023-10-15 12:09:57 以下内容源自《【个人】》 仅供学习交流使用 版权 禁止其他平台发布时删除以下此话 本文首次发布于CSDN平台 作者是CSDN日星月云 博客主页是https://blog.csdn.ne…

【C++】笔试训练(六)

目录 一、选择题二、编程题1、不要二2、把字符串转换成整数 一、选择题 1、十进制变量i的值为100&#xff0c;那么八进制的变量i的值为&#xff08;&#xff09; A 146 B 148 C 144 D 142 答案&#xff1a;C 2、执行下面语句后的输出为 int I 1; if (I < 0)printf("…

LightGBM-平分卡

文章目录 一、数据集处理二、定义模型训练和画图 三、好人的概率/坏人的概率四、生成报告五、行为评分卡模型表现总结 一、数据集处理 import pandas as pd from sklearn.metrics import roc_auc_score,roc_curve,auc from sklearn.model_selection import train_test_split f…

【无人机】太阳能伪卫星VoLTE无人机设计(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

蓝桥杯(迷宫,C++)

输入&#xff1a; 思路&#xff1a; 1、注意输入用字符串。 2、采用广度搜素的方法来求解。 3、因为最后要求字典序最小且D<L<R<U,所以在遍历四个方向的时候&#xff0c; 先向下&#xff0c;再向左、右&#xff0c;最后向上。 #include<iostream> #include…

“历史性判决:SEC 放弃上诉!灰度赢得比特币ETF转换!“

"这一决定是在一场法律战和数月的预期之后做出的&#xff0c;可能标志着监管格局将转向批准现货比特币ETF。" 据路透社报道&#xff0c;一项重大进展是&#xff0c;美国证券交易委员会 (SEC) 决定不对最近法院支持 Grayscale Investments 比特币 ETF 转换计划的…