SpringAMQP-Basic Queue、Work Queue、Fanout、Direct、Topic 港控/mmm° 2024-03-25 07:21 53阅读 0赞 > 同步通讯、异步通讯、RabbitMQ的基本了解 > [https://blog.csdn.net/weixin\_51351637/article/details/129501470?spm=1001.2014.3001.5502][https_blog.csdn.net_weixin_51351637_article_details_129501470_spm_1001.2014.3001.5502] > 上次跟着老师学完RabbitMQ之后,感觉白学了,也不知道企业中这两个哪个用的比较多,这次再来学学SpringAMQP ## **一、基本信息** ## -------------------- SpringAmqp的官方地址:[https://spring.io/projects/spring-amqp][https_spring.io_projects_spring-amqp] **AMQP:**高级消息队列**协议**,**在应用程序之间传递业务消息的开放标准**,该协议与语言和平台无关,更符合微服务中独立性的要求 **Spring AMQP:**是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,**spring-rabbit是底层的默认实现**。 **Spring AMQP 特征:** * 监听器容器,用于异步处理入站消息 * 用于发送和接收消息的RabbitTemplate * RabbitAdmin,用于自动声明队列,交换和绑定 **SpringAMQP大大简化了消息发送和接收的Api** **要学习的知识** **Basic Queue简单队列模型** **Work Queue工作队列模型** **发布、订阅模型-Fanout** **发布、订阅模型-Direct** **发布、订阅模型-Topic** **消息转换器** ## 二、入门案例的消息发送(Basic Queue简单队列模型) ## ![071674ce9360b2e4de369b2a2828cb4c.png][] ### 2.1 利用SpringAMQP实现HelloWorld中的基础消息队列(Basic Queue)功能 ### #### 2.1.1 父工程中引入spring-amqp的依赖 #### 父亲引入之后,两个儿子也会有 <!--AMQP依赖,包含RabbitMQ--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> #### 2.1.2 在publisher服务中利用RabbitTemplate发送消息到simple.queue这个队列 #### ##### 2.1.2.1 在publisher服务中编写application.yaml,添加mq连接信息 ##### 在RabbitMQ中需要我们自己手写代码,但是在SpringAMQP中,我们直接在配置文件中配置就可以了 spring: rabbitmq: host: localhost #rabbitMQ的IP地址 port: 5672 # 端口 15672是访问页面的端口地址 virtual-host: / # 虚拟主机 username: guest #用户名 password: guest #密码 ##### 2.1.2.2 编写代码 ##### @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendMessage2SimpleQueue(){ String queueName ="simple.queue"; String message ="hello,spring amqp!"; rabbitTemplate.convertAndSend(queueName,message); } } 我们查看一下,确实在这个队列中有内容!! 前提!!必须存在此队列,如果不存在是不行的 ![7d1b1fae80fe14a836538dc6e81de6d3.png][] #### 2.1.3 在consumer中编写消费逻辑,监听simple.queue队列(监听与接收) #### 因为之前我们已经父工程中导入坐标,现在就不需要了,之间看配置文件就行 ##### 2.1.3.1 consumer的配置文件信息 ##### spring: rabbitmq: host: localhost #rabbitMQ的IP地址 port: 5672 # 端口 15672是访问页面的端口地址 virtual-host: / # 虚拟主机 username: guest #用户名 password: guest #密码 ##### 2.1.3.2 编写代码 ##### **在consumer服务中新建一个类,编写消费逻辑** @Component // 注册成bean public class SpringRabbitListener { @RabbitListener(queues = "simple.queue") //监听的是哪个队列 // 将来如果有simple.queue队列的消息,会立即投放到下面的方法中,并且下面的方法还能处理对应的消息 // Spring会自动把消息投递给这个方法,然后通过参数传递给我们,我们拿到消息后就可以进行处理 // 对于参数列表,那边发的什么类型,我们就用什么类型接收 public void listenSimpleQueue(String msg) throws InterruptedException{ System.out.println("接收到的消息:"+msg); } } ![dc4f9a46c787681f9b320638e96ad5fe.png][] 确实没了 ![cb532134775ac59a01c5e325d75105d1.png][] ### **2.2 总结Basic Queue发送和接收消息的步骤** ### **发送消息** 1. 父工程引入amqp的starter依赖 1. 配置RabbitMQ地址 1. 引入RabbitTemplate对象,并使用方法convertAndSend()发送消息 **接收消息(监听)** 1. 父工程引入amqp的starter依赖 1. 配置RabbitMQ地址 1. 定义类,添加@Component注解 1. 类中声明方法,添加@RabbitListener注解,方法参数就是消息 **注意:消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能** ## 三、Work Queue工作队列模型 ## 与Base Queue工作队列模型的差别就是,这个queue后面挂着两个consumer ![9d341795497b309e395dd183f34d03fd.png][] **我们的消息是阅后即焚。** 当我们queue中有50条消息,不可能全部交给consumer1或者consumer2中的其中一个,比如consumer1处理20条,consumer2处理30条。**此时consumer1和consumer2的关系就是合作关系** **有些人会想,为什么要挂在两个消费者?** 因为队列中存放的消息是有上限的,假设queue中只能存储60条,publisher每秒钟发送50条,一个consumer每秒钟只能处理40条,这样随着时间的推移,queue中存储的数据就会堆满。 如果是两个consumer后,加大了消息的处理速度,有效的避免了堆积问题 ### 3.1 模拟Work Queue,实现一个队列绑定多个消费者 ### **实现思路** * 在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue * 在consumer服务中定义两个消息监听者,都监听simple.queue队列 * consumer1每秒处理50条消息,consumer2每秒处理10条消息 > **说明:消息预取** > > **当有大量的消息到达队列时,队列queue就会把消息进行投递,consumer1和consumer2的通道会先把消息拿过来(不管能不能处理的了,先拿过来),于是两边平均拿了所有的消息。** > **但是consumer1处理的速度快,很快就把消息处理了,consumer2处理的慢,处理需要一段时间** **发送消息** @Test public void testSendMessage2WorkQueue() throws InterruptedException { String queueName ="simple.queue"; String message ="hello message__"; // 循环发送50 for(int i=0;i<50;i++){ rabbitTemplate.convertAndSend(queueName,message+i); Thread.sleep(20); } } **接收消息** @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendMessage2SimpleQueue(){ String queueName ="simple.queue"; String message ="hello,spring amqp!"; rabbitTemplate.convertAndSend(queueName,message); } @Test public void testSendMessage2WorkQueue() throws InterruptedException { String queueName ="simple.queue"; String message ="hello message__"; // 循环发送50 for(int i=0;i<50;i++){ rabbitTemplate.convertAndSend(queueName,message+i); Thread.sleep(20); } } 我们看到最后,consumer1很快就把消息处理完了,但是consumer2还在继续处理消息 ![7eb4adfc66f06aa2cc5e7218a8886436.png][] #### 3.1.1 控制consumer预取消息的上限 #### 如果不设置的话,默认值是无线,有多少拿多少 spring: rabbitmq: host: localhost #rabbitMQ的IP地址 port: 5672 # 端口 15672是访问页面的端口地址 virtual-host: / # 虚拟主机 username: guest #用户名 password: guest #密码 listener: simple: prefetch: 1 # 每次只能获取一条下下哦i,处理完成才能获取下一个消息 ## 四、发布(Publish)、订阅(Subscribe) ## ### 4.1 基本介绍 ### exchange会将消息发送给与其绑定的所有队列 **发布订阅模式与之前案例的区别就是****允许将同一消息发送给多个消费者****。实现方式是加入exchange(交换机)** 消费者和队列之间依然会有一个绑定。 之前是publisher直接发送给queue,但是现在publisher先把消息发送给exchange(交换机),再有exchange将消息发送给queue,那这样来说,**publisher不用知道queue是否存在** ![006e18b791abac785ce3220e336af684.png][] **那到底交换机是给一个队列发信息还是给多个队列发信息呢?** 那这就要看交换机的类型了 * Fanout Exchange:广播 * Direct Exchange:路由 * Topic Exchange:主题 **备注:exchange交换机负责消息路由而不是存储,路由失败则消息丢失!!!!** ### 4.2 发布订阅-Fanout Exchange ### 发布订阅的第一种交换机Fanout Exchange **Fanout Exchange 会将接受到的消息路由到每一个跟其绑定的queue** ![769800b7b08fb499ac9ce27d1f6b7903.png][] #### 4.2.1 利用SpringAMQP演示FanoutExchange的使用 #### **实现思路:** 1. 在consumer服务中,利用代码声明队列、交换机,并将两者绑定 1. 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2 1. 在publisher中编写测试方法,向itcast.fanout(交换机)发送消息 ![b29b62574c46f721fcd3e7564a30d2f6.png][] #### 4.2.2 在consumer服务声明Exchange(交换机)、Queue(队列)、Binding(绑定关系) #### ![859508265fdfdcf38baf5da914443406.png][] 在consumer服务中添加一个类,添加@Configuration注解,并声明FanoutExchange、Queue和绑定关系对象Binding @Configuration public class FanoutConfig { // 创建交换机 @Bean public FanoutExchange fanoutExchange(){ // 创建交换机,交换机的名字叫itcast.fanout return new FanoutExchange("itcast.fanout"); } // 第一个队列 @Bean public Queue fanoutQueue1(){ // fanout.queue1 队列名称 return new Queue("fanout.queue1"); } // 绑定队列1和交换机 // 将队列1和交换机当作参数传递进来了 @Bean public Binding bindingQueue1(Queue fanoutQueue1,FanoutExchange fanoutExchange){ // 绑定队列1和交换机 return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange); } // 第一个队列 @Bean public Queue fanoutQueue2(){ // fanout.queue1 队列名称 return new Queue("fanout.queue2"); } // 绑定队列1和交换机 // 将队列1和交换机当作参数传递进来了 @Bean public Binding bindingQueue2(Queue fanoutQueue2,FanoutExchange fanoutExchange){ // 绑定队列1和交换机 return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange); } } 我们在页面上也确实能**看到交换机icast.fanout** ![da0c6dd6558d4fcec1b72984705b1929.png][] 也可以点进去查看一下这个交换器 ![7bc60d29195732a6204f810d5f3d0d5a.png][] **也确实能看到两个队列** ![c5abe1bb82b9255629f0bf8c9ae8bd2f.png][] #### 4.2.3 消息的发送 #### 我们以前是发送的队列,现在是发送到交换机 @Test public void testSendFanoutExchange(){ // 交换机名称 String exchangeName = "itcast.fanout"; // 消息 String message ="hello everyone"; rabbitTemplate.convertAndSend(exchangeName,"",message); } #### 4.2.4 消息的订阅(接收) #### @RabbitListener(queues = "fanout.queue1") public void listenFanoutQueue1(String msg) { System.out.println("消费者1接收到消息fanout.queue1:~~~~~~~~~"+msg); } @RabbitListener(queues = "fanout.queue2") public void listenFanoutQueue2(String msg) { System.out.println("消费者1接收到消息fanout.queue2:~~~~~~~~~"+msg); } 明显是可以收到的,两个队列都可以收到 ![1eaeb1a31c2daa2f1ec36f2b08d5afe6.png][] #### 4.2.5 总结 #### **交换机的作用** * 接收publisher发送的消息 * 将消息按照规则路由到与之绑定的队列 * 不能缓存消息,路由失败,消息丢失 * FanoutExchange的会将消息路由到每个绑定的队列 **声明队列、交换机、绑定关系的Bean是什么?** * Queue * Exchange的子类FanoutExchange * Binding ### 4.3 发布订阅-DirectExchange(路由模式) ### DirectExchange会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes) * **发布者发送消息时,指定消息的RoutingKey** * **每一个Queue都与Exchange设置一个BindingKey**(可以理解为约定的暗号) * **Exchange将消息路由到BingingKey与消息RoutingKey一致的队列** ![f7d95ce7ab9bbd152da8999aebcfbd75.png][] **队列在和交换机绑定的时候可以指定多个key,如下图所示** 那这样看来DirectExchange比FanoutExchange更灵活一点,DirectExchange也可以模拟FanoutExchange ![f3c61aec76e75937270bc9b711cd8f41.png][] #### 4.3.1 利用SpringAMQP演示DirectExchange的使用 #### **实现思路** * 利用@RabbitListener声明Exchange、Queue、RoutingKey(之前的交换机和队列都是@Bean的方式创建出来的) * 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2 * 在publisher中编写测试方法,向itcast.direct发送消息 ![d56ef8cc9fdc773d99f3468cabf1e73d.png][] #### 4.3.2 消息的发送 #### 发送给blue @Test public void testSendDirectExchange(){ // 交换机名称 String exchangeName = "itcast.direct"; // 消息 String message ="hello blue"; // "blue"指定的是BindingKey 当与RoutingKey一样时,consumer便可以收到消息 rabbitTemplate.convertAndSend(exchangeName,"blue",message); } 此时确实只有队列一收到消息 ![4618ec520e8e940fbc8c36243477dea7.png][] #### 4.3.3 消息的订阅(接收) #### 利用@RabbitListener声明Exchange、Queue、RoutingKey 在consumer包下 @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue1"), exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT), key = {"red","blue"} )) public void listenDirectQueue1(String msg){ System.out.println("消费者1接收到direct.queue1:~~~~~~~~~"+msg); } // key 指定routingKey @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue2"), exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT), key = {"red","yellow"} )) public void listenDirectQueue2(String msg){ System.out.println("消费者2接收到direct.queue2:~~~~~~~~~"+msg); } 运行之后发送Queue被正确的声明 ![1c7c93a79fb11f2217f6951356d12a2f.png][] 交换机也被正确的声明 ![c2c89c76cdae0d49b9873e2f991f25c2.png][] 我们也可以从下面看出来队列1和队列2各绑定了两个key ![0b844073040ab921c821fddf48dce007.png][] #### 4.3.4 总结 #### **描述下Direct交换机与Fanout交换机的差异?** * · Fanout交换机将消息路由给每一个与之绑定的队列 * Direct交换机根据RoutingKey判断路由给哪个队列 * 如果多个队列具有相同的RoutingKey,则与Fanout功能类似 **基于@RabbitListener注解声明队列和交换机有哪些常见注解?** @Queue @Exchange @QueueBinding ### 4.4 发布订阅-TopicExchange ### TopicExchange与DirectExchange类似,**区别在于routingKey必须是多个单词的列表,并且以****.****分割** **Queue与Exchange指定BindingKey时可以使用通配符** * \# 代指0个或多个单词 * \* 代指一个单词 ![444d79963b5ad782b333c19ae907eb92.png][] china.news 代表中国的新闻消息 china.weather 代表中国的天气消息 jepan.weather 代表日本的天气消息 #### 4.4.1 利用SpringAMQP演示TopicExchange的使用 #### **实现思路:** * 并利用@RabbitListener声明Exchange、Queue、RoutingKey * 在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2 * 在publisher中编写测试方法,向itcast.topic发送消息 ![c729e1b88e3fb003ee2f5aaee31f80c2.png][] #### 4.4.2 声明队列和交换机(包括消息的接收-订阅) #### @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue1"), exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC), key = {"china.#"} )) public void listenTopicQueue1(String msg) { System.out.println("消费者接收到topic.queue1:~~~~~~~~~" + msg); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue2"), exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC), key = {"#.news"} )) public void listenTopicQueue2(String msg) { System.out.println("消费者接收到topic.queue2:~~~~~~~~~" + msg); } **交换机** ![1bec96cf76a65a7ef425978964a5427d.png][] **绑定关系** ![120f38087c108bba0a3f4a48cc5f195a.png][] **两个队列也成功声明** ![befd120e905dd806a1e3a9680408f936.png][] #### 4.4.3 消息的发送 #### @Test public void testSendTopicExchange(){ // 交换机名称 String exchangeName = "itcast.topic"; // 消息 String message ="张靖奇最牛逼"; rabbitTemplate.convertAndSend(exchangeName,"china.news",message); } 当我们点击发送之后,我们也看到consumer都可以从对应的队列中取出我们想要的数据 ![33d7059d6c0778ba23f35a61950fe2f3.png][] #### 4.4.4 总结Direct交换机与Topic交换机的差异 #### topic交换机中的BandingKey支持通配符,RoutingKey是多个单词以点分割 ### 4.5 消息转换器-测试发送Object类型的消息 ### 在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说**我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送** #### 4.5.1 声明队列 #### // 声明队列 @Bean public Queue objectQueue(){ return new Queue("object.queue"); } #### 4.5.2 向队列发布消息 #### @Test public void testSendObjectQueue() { Map<String,Object> map = new HashMap<>(); map.put("name","柳岩"); rabbitTemplate.convertAndSend("object.queue",map); } 确实是有消息的 ![781c4fd5dd353e947568d1fdc811ba8d.png][] 我们查看一下消息的内容,没有发现柳岩,而且类型是 content\_type: application/x-java-serialized-object,将我们的对象做序列化,也就是jdk的序列化方式(性能比较差,安全性有问题,数据长度过长) **所以我们非常不推荐我们用这种默认的方式** ![e7090831621a11055fe5b8ea21fc5f9d.png][] #### 4.5.3 修改JDK序列化使得数据是JSON形式发送 #### Spring对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的 而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。 如果修改只需要定义一个MessageConverter类型的Bean即可。**推荐使用JSON方式序列化** ##### **4.5.3.1 依赖:** ##### <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> 也可以用下面这个 <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency> ##### 4.5.3.2 覆盖默认bean ##### 在publisher服务声明MessageConverter的Bean,我们一旦声明。就会把默认的给覆盖掉,这是spring自动装配的原理 > 注意!!! 一定是下面的两个类,不要覆盖错了 > import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; > import org.springframework.amqp.support.converter.MessageConverter; @Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); } 在继续4.5.2发送新的消息,然后在页面的队列中查看,发现这次是汉字了 ![3306c076435114f1d831d77a7e6df061.png][] #### 4.5.4 消息订阅(接收-JSON串) #### ##### 4.5.4.1 依赖 ##### 在consumer中引入Jackson依赖 ##### 4.5.4.2 定义消息转换器覆盖默认转换器 ##### @Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); } ##### 4.5.4.3 消息订阅-接收-消息的监听 ##### @RabbitListener(queues = "object.queue") public void listenObjectQueue(Map<String, Object> msg){ System.out.println("收到消息"+msg); } ![2f878903ec1b4145e16109dbc5b64d31.png][] #### 4.5.5 总结 #### **SpringAMQP中消息的序列化和反序列化是怎么实现的?** * 利用MessageConverter实现的,默认是JDK的序列化 * 注意发送方和接收方必须使用相同的MessageConverter [https_blog.csdn.net_weixin_51351637_article_details_129501470_spm_1001.2014.3001.5502]: https://blog.csdn.net/weixin_51351637/article/details/129501470?spm=1001.2014.3001.5502 [https_spring.io_projects_spring-amqp]: https://spring.io/projects/spring-amqp [071674ce9360b2e4de369b2a2828cb4c.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/3ae70b07b9704f4282baead62f5ba768.png [7d1b1fae80fe14a836538dc6e81de6d3.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/e76ce127b8f44f72adfa0c4dffa9459f.png [dc4f9a46c787681f9b320638e96ad5fe.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/94aab7f068454bc7b2800e5123d0de9e.png [cb532134775ac59a01c5e325d75105d1.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/299b913273154afe8ecac14763e6728e.png [9d341795497b309e395dd183f34d03fd.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/3b0c2adccd134470afe5f2839fd69c8f.png [7eb4adfc66f06aa2cc5e7218a8886436.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/9a52b44e8b39414db4d1aff086ed754f.png [006e18b791abac785ce3220e336af684.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/b89e927319ab4a20ae90ec4cad11a806.png [769800b7b08fb499ac9ce27d1f6b7903.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/6f50917c934c4542989f68afc4166446.png [b29b62574c46f721fcd3e7564a30d2f6.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/d13ba2162a424f5b871b5f919aedb8e3.png [859508265fdfdcf38baf5da914443406.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/b51f704a0489473bbe8598c5b9522f8d.png [da0c6dd6558d4fcec1b72984705b1929.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/e9e07ae5e1ee4e91b67e1e7ff1797739.png [7bc60d29195732a6204f810d5f3d0d5a.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/3855a3d24a414a688878be0e1372449e.png [c5abe1bb82b9255629f0bf8c9ae8bd2f.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/f36aa630544740fd98af420a99a6aabe.png [1eaeb1a31c2daa2f1ec36f2b08d5afe6.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/ac3d57d51dee46dcbdabb0b33c285da0.png [f7d95ce7ab9bbd152da8999aebcfbd75.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/b4a8134b501748dab45a1acbe842e2b7.png [f3c61aec76e75937270bc9b711cd8f41.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/49ce85286fd14ad78d70cee6a7605ada.png [d56ef8cc9fdc773d99f3468cabf1e73d.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/e3360f32a6a74499ba41296ebd12ced2.png [4618ec520e8e940fbc8c36243477dea7.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/31b2ead660da450d9de12fdf2c948be3.png [1c7c93a79fb11f2217f6951356d12a2f.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/9f120d296a644ee9951beaef37e457c0.png [c2c89c76cdae0d49b9873e2f991f25c2.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/4c7a4a76766649329a05536942126b68.png [0b844073040ab921c821fddf48dce007.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/f9f921f37c0242039f94af8ccc65905c.png [444d79963b5ad782b333c19ae907eb92.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/283bafc6b5be4a9b93539b38bcf6427e.png [c729e1b88e3fb003ee2f5aaee31f80c2.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/aded2f5a2047405eb0dfdaafdf74a902.png [1bec96cf76a65a7ef425978964a5427d.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/8fcd621faa26472a8663126f4a01385a.png [120f38087c108bba0a3f4a48cc5f195a.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/fba1dc242ba344378e9b17c78d62c789.png [befd120e905dd806a1e3a9680408f936.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/b0958a8de6334e7881f561533fbf854c.png [33d7059d6c0778ba23f35a61950fe2f3.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/e62e515384aa42efa1728b2398ed56bf.png [781c4fd5dd353e947568d1fdc811ba8d.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/f622fada99e3477eb9753010098b8ef1.png [e7090831621a11055fe5b8ea21fc5f9d.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/bd3593a529e043e98f956a95ebed2e88.png [3306c076435114f1d831d77a7e6df061.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/214ba8b0244d47f5bc49d6881c8161fe.png [2f878903ec1b4145e16109dbc5b64d31.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/25/512c3c990304450abd54408d86a80c6d.png
还没有评论,来说两句吧...