SpringBoot integra RabbitMQ para lograr el envío y la recepción de mensajes

RabbitMQ es un middleware de mensajes que utiliza el lenguaje Erlang para implementar AMQP (Protocolo de cola de mensajes avanzado, Protocolo de cola de mensajes avanzado), que se utiliza para almacenar y reenviar mensajes en un sistema distribuido. RabbitMQ es favorecido por más y más empresas debido a su alta confiabilidad, fácil expansión, alta disponibilidad y características completas.

[Ejemplo] SpringBoot integra RabbitMQ para realizar el envío y la recepción de mensajes.

Requisitos de ejemplo:

  1. Realice la integración de SpringBoot en el marco RabbitMQ.
  2. Implementar el mecanismo de reconocimiento de mensajes (ACK) de RabbitMQ.
  3. Implementar la función de retardo de la cola de mensajes de RabbitMQ.
  4. Implemente el mecanismo de reintento de RabbitMQ.
  5. Realice el envío y la recepción de JSON, datos en formato de mapa.

Tecnologías relacionadas:

"Tipo de intercambio RabbitMQ y modo de envío / recepción de mensajes"

"Mecanismo de reconocimiento de mensajes (ACK) de RabbitMQ"

"RabbitMQ se da cuenta de la función de retraso de la cola de mensajes"

"Mecanismo de reintento de RabbitMQ"

"RabbitMQ realiza el envío y la recepción de datos en formato JSON y Map"

"SpringBoot integra RabbitMQ para lograr el envío y la recepción de mensajes"

 

1. El remitente del mensaje

El remitente del mensaje implementa funciones de envío de mensajes y confirmación de mensajes (confirmación del intercambiador, confirmación de la cola).

1.1 Crear un proyecto

Cree el primer proyecto SpringBoot (proyecto de mensajería RabbitmqProvider) e integre el marco RabbitMQ. La estructura del proyecto es la siguiente:

En el archivo de información de configuración pom.xml, agregue los archivos dependientes relacionados:

<!-- AMQP客户端 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

Configure el servicio RabbitMQ en el archivo de configuración application.yml:

spring:
  # 项目名称
  application:
    name: rabbitmq-provider
  # RabbitMQ服务配置
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    # 消息确认(ACK)
    publisher-confirm-type: correlated #确认消息已发送到交换机(Exchange)
    publisher-returns: true #确认消息已发送到队列(Queue)

1.2 Clase de configuración (capa de configuración)

En el paquete com.pjb.config, cree una clase RabbitMqConfig (clase de configuración RabbitMQ), el código es el siguiente:

package com.pjb.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * RabbitMQ配置类
 * @author pan_junbiao
 **/
@Configuration
public class RabbitMqConfig
{
    public static final String DIRECT_QUEUE = "direct_queue"; //Direct队列名称
    public static final String DIRECT_EXCHANGE = "direct_exchange"; //交换器名称
    public static final String DIRECT_ROUTING_KEY = "direct_routing_key"; //路由键

    public static final String DELAY_QUEUE = "delay_queue"; //延时队列名称
    public static final String DELAY_EXCHANGE = "delay_exchange"; //交换器名称
    public static final String DELAY_ROUTING_KEY = "delay_routing_key"; //路由键

    @Autowired
    private CachingConnectionFactory connectionFactory;

    @Bean
    public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory)
    {
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);

        //设置Json转换器
        rabbitTemplate.setMessageConverter(jsonMessageConverter());

        //设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
        rabbitTemplate.setMandatory(true);

        //确认消息送到交换机(Exchange)回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback()
        {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause)
            {
                System.out.println("\n确认消息送到交换机(Exchange)结果:");
                System.out.println("相关数据:" + correlationData);
                System.out.println("是否成功:" + ack);
                System.out.println("错误原因:" + cause);
            }
        });

        //确认消息送到队列(Queue)回调
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback()
        {
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage)
            {
                System.out.println("\n确认消息送到队列(Queue)结果:");
                System.out.println("发生消息:" + returnedMessage.getMessage());
                System.out.println("回应码:" + returnedMessage.getReplyCode());
                System.out.println("回应信息:" + returnedMessage.getReplyText());
                System.out.println("交换机:" + returnedMessage.getExchange());
                System.out.println("路由键:" + returnedMessage.getRoutingKey());
            }
        });
        return rabbitTemplate;
    }

    /**
     * Json转换器
     */
    @Bean
    public Jackson2JsonMessageConverter jsonMessageConverter()
    {
        return new Jackson2JsonMessageConverter();
    }

    /**
     * Direct交换器
     */
    @Bean
    public DirectExchange directExchange()
    {
        /**
         * 创建交换器,参数说明:
         * String name:交换器名称
         * boolean durable:设置是否持久化,默认是 false。durable 设置为 true 表示持久化,反之是非持久化。
         * 持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。
         * boolean autoDelete:设置是否自动删除,为 true 则设置队列为自动删除,
         */
        return new DirectExchange(DIRECT_EXCHANGE, true, false);
    }

    /**
     * 队列
     */
    @Bean
    public Queue directQueue()
    {
        /**
         * 创建队列,参数说明:
         * String name:队列名称。
         * boolean durable:设置是否持久化,默认是 false。durable 设置为 true 表示持久化,反之是非持久化。
         * 持久化的队列会存盘,在服务器重启的时候不会丢失相关信息。
         * boolean exclusive:设置是否排他,默认也是 false。为 true 则设置队列为排他。
         * boolean autoDelete:设置是否自动删除,为 true 则设置队列为自动删除,
         * 当没有生产者或者消费者使用此队列,该队列会自动删除。
         * Map<String, Object> arguments:设置队列的其他一些参数。
         */
        return new Queue(DIRECT_QUEUE, true, false, false, null);
    }

    /**
     * 绑定
     */
    @Bean
    Binding directBinding(DirectExchange directExchange, Queue directQueue)
    {
        //将队列和交换机绑定, 并设置用于匹配键:routingKey路由键
        return BindingBuilder.bind(directQueue).to(directExchange).with(DIRECT_ROUTING_KEY);
    }

    /******************************延时队列******************************/

    @Bean
    public CustomExchange delayExchange()
    {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAY_EXCHANGE, "x-delayed-message", true, false, args);
    }

    @Bean
    public Queue delayQueue()
    {
        Queue queue = new Queue(DELAY_QUEUE, true);
        return queue;
    }

    @Bean
    public Binding delaybinding(Queue delayQueue, CustomExchange delayExchange)
    {
        return BindingBuilder.bind(delayQueue).to(delayExchange).with(DELAY_ROUTING_KEY).noargs();
    }
}

1.3 Clase de entidad (capa de entidad)

En el paquete com.pjb.entity, cree una clase UserInfo (clase de entidad de información de usuario).

package com.pjb.entity;

/**
 * 用户信息实体类
 * @author pan_junbiao
 **/
public class UserInfo
{
    private int userId; //用户编号
    private String userName; //用户姓名
    private String blogUrl; //博客地址
    private String blogRemark; //博客信息

    //省略getter与setter方法...
}

1.4 Capa de envío de mensajes (capa de remitente)

Cree la interfaz UserSender (interfaz de servicio de envío de mensajes de usuario) en el paquete com.pjb.sender.

package com.pjb.sender;

import com.pjb.entity.UserInfo;

import java.util.Map;

/**
 * 用户消息发送服务接口
 * @author pan_junbiao
 **/
public interface UserSender
{
    /**
     * 发送用户信息Json格式数据
     * @param userInfo 用户信息实体类
     */
    public void sendUserJson(UserInfo userInfo);

    /**
     * 延时发送用户信息Map格式数据
     * @param userMap 用户信息Map
     */
    public void sendDelayUserMap(Map userMap);
}

En el paquete com.pjb.sender.impl, cree la clase UserSenderImpl (clase de servicio de mensajería de usuario).

package com.pjb.sender.impl;

import com.pjb.config.RabbitMqConfig;
import com.pjb.entity.UserInfo;
import com.pjb.sender.UserSender;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;

/**
 * 用户消息发送服务类
 * @author pan_junbiao
 **/
@Service
public class UserSenderImpl implements UserSender
{
    @Autowired
    RabbitTemplate rabbitTemplate;

    //时间格式
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * 发送用户信息Json格式数据
     * @param userInfo 用户信息实体类
     */
    @Override
    public void sendUserJson(UserInfo userInfo)
    {
        /**
         * 发送消息,参数说明:
         * String exchange:交换器名称。
         * String routingKey:路由键。
         * Object object:发送内容。
         */
        rabbitTemplate.convertAndSend(RabbitMqConfig.DIRECT_EXCHANGE, RabbitMqConfig.DIRECT_ROUTING_KEY, userInfo);

        System.out.println("Json格式数据消息发送成功,发送时间:" + dateFormat.format(new Date()));
    }

    /**
     * 延时发送用户信息Map格式数据
     * @param userMap 用户信息Map
     */
    @Override
    public void sendDelayUserMap(Map userMap)
    {
        rabbitTemplate.convertAndSend(RabbitMqConfig.DELAY_EXCHANGE, RabbitMqConfig.DELAY_ROUTING_KEY, userMap, new MessagePostProcessor()
        {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException
            {
                //消息延迟5秒
                message.getMessageProperties().setHeader("x-delay", 5000);
                return message;
            }
        });

        System.out.println("Map格式数据消息发送成功,发送时间:" + dateFormat.format(new Date()));
    }
}

 

2. El receptor del mensaje

El extremo receptor de mensajes realiza la función de recepción de mensajes y confirmación de recepción de mensajes.

2.1 Crear un proyecto

Cree un segundo proyecto SpringBoot (proyecto de recepción de mensajes RabbitmqConsumer) e integre el marco RabbitMQ. La estructura del proyecto es la siguiente:

En el archivo de información de configuración pom.xml, agregue los archivos dependientes relacionados:

<!-- AMQP客户端 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

Configure el servicio RabbitMQ en el archivo de configuración application.yml:

spring:
  # 项目名称
  application:
    name: rabbitmq-consumer
  # RabbitMQ服务配置
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    listener:
      simple:
        # 重试机制
        retry:
          enabled: true #是否开启消费者重试
          max-attempts: 5 #最大重试次数
          initial-interval: 5000ms #重试间隔时间(单位毫秒)
          max-interval: 1200000ms #重试最大时间间隔(单位毫秒)
          multiplier: 2 #间隔时间乘子,间隔时间*乘子=下一次的间隔时间,最大不能超过设置的最大间隔时间

2.2 Clase de configuración (capa de configuración)

En el paquete com.pjb.config, cree una clase RabbitMqConfig (clase de configuración RabbitMQ), el código es el siguiente:

package com.pjb.config;

import com.pjb.receiver.impl.AckReceiver;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ配置类
 * @author pan_junbiao
 **/
@Configuration
public class RabbitMqConfig
{
    public static final String DIRECT_QUEUE = "direct_queue"; //Direct队列名称
    public static final String DELAY_QUEUE = "delay_queue"; //延时队列名称

    /**
     * 消息接收确认处理类
     */
    @Autowired
    private AckReceiver ackReceiver;

    @Autowired
    private CachingConnectionFactory connectionFactory;

    /**
     * 客户端配置
     * 配置手动确认消息、消息接收确认
     */
    @Bean
    public SimpleMessageListenerContainer simpleMessageListenerContainer()
    {
        //消费者数量,默认10
        int DEFAULT_CONCURRENT = 10;

        //每个消费者获取最大投递数量 默认50
        int DEFAULT_PREFETCH_COUNT = 50;

        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        container.setConcurrentConsumers(DEFAULT_CONCURRENT);
        container.setMaxConcurrentConsumers(DEFAULT_PREFETCH_COUNT);

        // RabbitMQ默认是自动确认,这里改为手动确认消息
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);

        //添加队列,可添加多个队列
        container.addQueues(new Queue(DIRECT_QUEUE,true));
        container.addQueues(new Queue(DELAY_QUEUE,true));

        //设置消息处理类
        container.setMessageListener(ackReceiver);

        return container;
    }
}

2.3 Clase de entidad (capa de entidad)

En el paquete com.pjb.entity, cree una clase UserInfo (clase de entidad de información de usuario).

package com.pjb.entity;

/**
 * 用户信息实体类
 * @author pan_junbiao
 **/
public class UserInfo
{
    private int userId; //用户编号
    private String userName; //用户姓名
    private String blogUrl; //博客地址
    private String blogRemark; //博客信息

    //省略getter与setter方法...
}

1.4 Capa receptora de mensajes (capa receptora)

En el paquete com.pjb.receiver, cree la interfaz UserReceiver (interfaz de recepción de mensajes de usuario).

package com.pjb.receiver;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;

/**
 * 用户消息接收接口
 * @author pan_junbiao
 **/
public interface UserReceiver
{
    /**
     * 接收用户信息Json格式数据
     */
    public void receiverUserJson(Message message, Channel channel) throws Exception;

    /**
     * 延时接收用户信息Map格式数据
     */
    public void receiverDelayUserMap(Message message, Channel channel) throws Exception;
}

En el paquete com.pjb.receiver.impl, cree la clase UserReceiverImpl (clase de recepción de mensajes de usuario).

package com.pjb.receiver.impl;

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pjb.entity.UserInfo;
import com.pjb.receiver.UserReceiver;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.stereotype.Service;

import java.util.Map;

/**
 * 用户消息接收类
 * @author pan_junbiao
 **/
@Service
public class UserReceiverImpl implements UserReceiver
{
    /**
     * 接收用户信息Json格式数据
     */
    @Override
    public void receiverUserJson(Message message, Channel channel) throws Exception
    {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try
        {
            //将JSON格式数据转换为实体对象
            ObjectMapper mapper = new ObjectMapper();
            UserInfo userInfo = mapper.readValue(message.getBody(), UserInfo.class);

            System.out.println("接收者收到JSON格式消息:");
            System.out.println("用户编号:" + userInfo.getUserId());
            System.out.println("用户名称:" + userInfo.getUserName());
            System.out.println("博客地址:" + userInfo.getBlogUrl());
            System.out.println("博客信息:" + userInfo.getBlogRemark());

            /**
             * 确认消息,参数说明:
             * long deliveryTag:唯一标识 ID。
             * boolean multiple:是否批处理,当该参数为 true 时,
             * 则可以一次性确认 deliveryTag 小于等于传入值的所有消息。
             */
            channel.basicAck(deliveryTag, true);

            /**
             * 否定消息,参数说明:
             * long deliveryTag:唯一标识 ID。
             * boolean multiple:是否批处理,当该参数为 true 时,
             * 则可以一次性确认 deliveryTag 小于等于传入值的所有消息。
             * boolean requeue:如果 requeue 参数设置为 true,
             * 则 RabbitMQ 会重新将这条消息存入队列,以便发送给下一个订阅的消费者;
             * 如果 requeue 参数设置为 false,则 RabbitMQ 立即会还把消息从队列中移除,
             * 而不会把它发送给新的消费者。
             */
            //channel.basicNack(deliveryTag, true, false);
        }
        catch (Exception e)
        {
            /**
             * 拒绝消息,参数说明:
             * long deliveryTag:唯一标识 ID。
             * boolean requeue:如果 requeue 参数设置为 true,
             * 则 RabbitMQ 会重新将这条消息存入队列,以便发送给下一个订阅的消费者;
             * 如果 requeue 参数设置为 false,则 RabbitMQ 立即会还把消息从队列中移除,
             * 而不会把它发送给新的消费者。
             */
            channel.basicReject(deliveryTag, false);

            e.printStackTrace();
        }
    }

    /**
     * 延时接收用户信息Map格式数据
     */
    @Override
    public void receiverDelayUserMap(Message message, Channel channel) throws Exception
    {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try
        {
            //将JSON格式数据转换为Map对象
            ObjectMapper mapper = new ObjectMapper();
            JavaType javaType = mapper.getTypeFactory().constructMapType(Map.class, String.class, Object.class);
            Map<String, Object> resultMap = mapper.readValue(message.getBody(),javaType);

            System.out.println("接收者收到Map格式消息:");
            System.out.println("用户编号:" + resultMap.get("userId"));
            System.out.println("用户名称:" + resultMap.get("userName"));
            System.out.println("博客地址:" + resultMap.get("blogUrl"));
            System.out.println("博客信息:" + resultMap.get("userRemark"));

            //确认消息
            channel.basicAck(deliveryTag, true);

            //否定消息
            //channel.basicNack(deliveryTag, true, false);
        }
        catch (Exception e)
        {
            //拒绝消息
            channel.basicReject(deliveryTag, false);

            e.printStackTrace();
        }
    }
}

En el paquete com.pjb.receiver.impl, cree la clase AckReceiver (clase de procesamiento de confirmación de recepción de mensajes).

package com.pjb.receiver.impl;

import com.pjb.config.RabbitMqConfig;
import com.pjb.receiver.UserReceiver;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 消息接收确认处理类
 * 所有的消息,都由该类接收
 * @author pan_junbiao
 **/
@Service
public class AckReceiver implements ChannelAwareMessageListener
{
    /**
     * 用户消息接收类
     */
    @Autowired
    private UserReceiver userReceiver;

    @Override
    public void onMessage(Message message, Channel channel) throws Exception
    {
        //时间格式
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("消息接收成功,接收时间:" + dateFormat.format(new Date()) + "\n");

        //获取队列名称
        String queueName = message.getMessageProperties().getConsumerQueue();

        //接收用户信息Json格式数据
        if (queueName.equals(RabbitMqConfig.DIRECT_QUEUE))
        {
            userReceiver.receiverUserJson(message, channel);
        }

        //延时接收用户信息Map格式数据
        if (queueName.equals(RabbitMqConfig.DELAY_QUEUE))
        {
            userReceiver.receiverDelayUserMap(message, channel);
        }

        //多个队列的处理,则如上述代码,继续添加方法....
    }
}

 

3. Ejecute la prueba

Después de completar los dos códigos de proyecto anteriores, puede ejecutar la prueba. Primero inicie el segundo proyecto SpringBoot (proyecto de recepción de mensajes RabbitmqConsumer) y déjelo correr todo el tiempo.

3.1 Envío y recepción de cola de mensajes

En el primer proyecto SpringBoot (proyecto de mensajería RabbitmqProvider), cree una clase de prueba y escriba un método para enviar información de usuario en formato Json.

package com.pjb.sender;

import com.pjb.entity.UserInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.HashMap;
import java.util.Map;

/**
 * 用户消息发送测试类
 * @author pan_junbiao
 **/
@SpringBootTest
public class UserSenderTest
{
    /**
     * 用户消息发送类
     */
    @Autowired
    private UserSender userSender;

    /**
     * 发送用户信息Json格式数据
     */
    @Test
    public void sendUserJson() throws Exception
    {
        //创建用户信息
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId(1);
        userInfo.setUserName("pan_junbiao的博客");
        userInfo.setBlogUrl("https://blog.csdn.net/pan_junbiao");
        userInfo.setBlogRemark("您好,欢迎访问 pan_junbiao的博客");

        //执行发送
        userSender.sendUserJson(userInfo);

        //由于这里使用的是测试方法,当测试方法结束,RabbitMQ相关的资源也就关闭了,
        //会导致消息确认的回调出现问题,所有加段延时,该延时与业务无关
        Thread.sleep(2000);
    }
}

El resultado de la ejecución del remitente:

Recibiendo el resultado de la ejecución final:

3.2 Función de retardo de la cola de mensajes

Escriba el código para probar la función de retardo del envío de información de usuario, datos de formato de mapa y cola de mensajes.

/**
 * 用户消息发送类
 */
@Autowired
private UserSender userSender;

/**
 * 延时发送用户信息Map格式数据
 */
@Test
public void sendDelayUserMap() throws Exception
{
    //创建用户信息Map
    Map<String, Object> userMap = new HashMap<>();
    userMap.put("userId", "1");
    userMap.put("userName", "pan_junbiao的博客");
    userMap.put("blogUrl", "https://blog.csdn.net/pan_junbiao");
    userMap.put("userRemark", "您好,欢迎访问 pan_junbiao的博客");

    //执行发送
    userSender.sendDelayUserMap(userMap);

    //由于这里使用的是测试方法,当测试方法结束,RabbitMQ相关的资源也就关闭了,
    //会导致消息确认的回调出现问题,所有加段延时,该延时与业务无关
    Thread.sleep(2000);
}

El resultado de la ejecución del remitente:

Recibiendo el resultado de la ejecución final:

Explicación de resultados:

En el código 1.4, el retraso del mensaje se establece en 5 segundos. Como se puede ver en la figura anterior, el tiempo de envío del mensaje y el tiempo de recepción del mensaje están separados exactamente por 5 segundos, lo que indica que la función de retraso de la cola de mensajes de RabbitMQ se estableció correctamente.

 

Supongo que te gusta

Origin blog.csdn.net/pan_junbiao/article/details/113560849
Recomendado
Clasificación