SpringBoot整合RabbitMQ

1项目启动

1.1 配置文件

1.1.1 添加RabbitMQ的用户

windows系统

打开RabbitMQ的sbin目录的cmd终端,执行以下命令,添加账号密码(均为admin),并赋予最高权限

cpp 复制代码
## 添加账号
rabbitmqctl add_user admin admin
## 添加访问权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
## 设置超级权限
rabbitmqctl set_user_tags admin administrator

1.1.2 pom.xml依赖引入

cpp 复制代码
	<parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.7.1</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <!--RabbitMQ-->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.5.1</version>
        </dependency>
        <!-- SLF4J -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.36</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <scope>provided</scope>
        </dependency>

        <!--以下为基础依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

1.1.3 application.yml

cpp 复制代码
rabbitmq:
  host: 127.0.0.1 #主机IP地址
  port: 5672 #端口号(注意15672为UI管理台页面)
  username: admin #用户名
  passport: admin #密码
  virtualhost: / #虚拟主机(我是用的根主机)
  pool_size: 5

1.1.4 配置类读取

cpp 复制代码
@Setter
@Getter
@ConfigurationProperties(prefix = "rabbitmq")
@Component
public class RabbitmqProperties {

    /**
     * 主机
     */
    private String host;

    /**
     * 端口
     */
    private Integer port;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String passport;

    /**
     * 路径
     */
    private String virtualhost;
    
	/**
     * 连接池大小
     */
    private Integer poolSize;
}

1.2 RabbitMQ连接池

RabbitmqConnection 类

java 复制代码
public class RabbitmqConnection {

    private Connection connection;

    public RabbitmqConnection(String host, int port, String userName, String password, String virtualhost) {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost(host);
        connectionFactory.setPort(port);
        connectionFactory.setUsername(userName);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost(virtualhost);
        try {
            connection = connectionFactory.newConnection();
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取链接
     *
     * @return
     */
    public Connection getConnection() {
        return connection;
    }

    /**
     * 关闭链接
     *
     */
    public void close() {
        try {
            connection.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

RabbitmqConnectionPool 类

java 复制代码
public class RabbitmqConnectionPool {

    private static BlockingQueue<RabbitmqConnection> pool;

    public static void initRabbitmqConnectionPool(String host, int port, String userName, String password,
                                             String virtualhost,
                                           Integer poolSize) {
        pool = new LinkedBlockingQueue<>(poolSize);
        for (int i = 0; i < poolSize; i++) {
            pool.add(new RabbitmqConnection(host, port, userName, password, virtualhost));
        }
    }

    public static RabbitmqConnection getConnection() throws InterruptedException {
        return pool.take();
    }

    public static void returnConnection(RabbitmqConnection connection) {
        pool.add(connection);
    }

    public static void close() {
        pool.forEach(RabbitmqConnection::close);
    }
}

1.3 核心代码

RabbitmqService 接口

java 复制代码
public interface RabbitmqService {

    void publishMsg(String exchange,
                    BuiltinExchangeType exchangeType,
                    String toutingKey,
                    String message);

    void consumerMsg(String exchange,
                     String queue,
                     String routingKey) throws IOException, TimeoutException;

    
}

RabbitmqServiceImpl 实现类

核心代码(发送消息逻辑)(接收消息逻辑)

java 复制代码
@Slf4j
@Service
public class RabbitmqServiceImpl implements RabbitmqService {

    /**
     * 向RabbitMQ发送消息
     * @param exchange:交换机名称
     * @param exchangeType:交换机类型
     *                    DIRECT直接交换机(适合一对一通知),
     *                    FANOUT扇形交换机(广播模式),
     *                    TOPIC主题交换机(支持通配符匹配路由键),
     *                    HEADERS头交换机(根据header内容匹配)
     * @param toutingKey:路由键,决定消息发往哪个队列
     * @param message:消息体内容
     */
    @Override
    public void publishMsg(String exchange,
                           BuiltinExchangeType exchangeType,
                           String toutingKey,
                           String message) {
        try {
            //创建连接(快递车出发)
            RabbitmqConnection rabbitmqConnection = RabbitmqConnectionPool.getConnection();
            Connection connection = rabbitmqConnection.getConnection();
            //创建消息通道(确定快递的运输路线)
            Channel channel = connection.createChannel();
            // 声明exchange中的消息为可持久化,不自动删除(设置快递分拣规则)
            /**
             * exchange:声明的交换机名称
             * BuiltinExchangeType.DIRECT:指定交换机类型为DIRECT
             * boolean durable:是否持久化,true表示持久化,false表示非持久化
             * boolean deleteWhenUnused:是否自动删除,true表示没有队列绑定到交换机时自动删除,false表示不自动删除
             * Map<String, Object> arguments:其他参数
             */
            channel.exchangeDeclare(exchange, exchangeType, true, false, null);
            // 发布消息(把包裹交给分拣中心)
            /**
             * exchange:交换机名称
             * toutingKey:消息的路由键
             * BasicProperties:消息属性
             * body:消息内容
             */
            channel.basicPublish(exchange, toutingKey, null, message.getBytes());
            log.info("Publish msg: {}", message);
            //快递车返回公司
            channel.close();
            //车辆入库
            RabbitmqConnectionPool.returnConnection(rabbitmqConnection);
        } catch (InterruptedException | IOException | TimeoutException e) {
            log.error("rabbitMq消息发送异常: exchange: {}, msg: {}", exchange, message, e);
        }
    }

    /**
     * 消费消息
     * @param exchange : 交换机名称
     * @param queueName : 队列名称
     * @param routingKey : 路由键
     */
    @Override
    public void consumerMsg(String exchange,
                            String queueName,
                            String routingKey) {

        try {
            //创建连接
            RabbitmqConnection rabbitmqConnection = RabbitmqConnectionPool.getConnection();
            Connection connection = rabbitmqConnection.getConnection();
            //创建消息信道
            final Channel channel = connection.createChannel();
            //消息队列(声明了一个持久化队列,即使RabbitMQ重启也不会丢失数据)

            //channel.exchangeDeclare(exchange, BuiltinExchangeType.DIRECT, true, false, null);
            /**
             * 这里属于重复交换机声明,否则没有时会报错,第一次执行后,去掉也不会报错
             * queue:队列名称
             * boolean durable:是否持久化,true表示持久化,false表示非持久化
             * boolean exclusive:是否独占队列,true表示独占队列,false表示非独占队列
             * boolean autoDelete:是否自动删除,true表示自动删除,false表示不自动删除
             * Map<String, Object> arguments:其他参数
             */
            channel.queueDeclare(queueName, true, false, false, null);
            //绑定队列到交换机
            /**
             * queueName:队列名称
             * exchange:交换机名称
             * routingKey:队列的路由键
             */
            channel.queueBind(queueName, exchange, routingKey);

            /**
             * 创建消费者
             * channel:消息通道
             */
            Consumer consumer = new DefaultConsumer(channel) {
                /**
                 * 当收到消息时,触发的方法
                 * @param consumerTag:消费者标签(唯一标识)
                 * @param envelope:消息的元信息
                 * @param properties:消息属性
                 * @param body:实际的消息体
                 * @throws IOException
                 */
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                           byte[] body) throws IOException {
                    String message = new String(body, "UTF-8");
                    log.info("Consumer msg: {}", message);

                    // 获取Rabbitmq消息,并保存到DB
                    // 说明:这里仅作为示例,如果有多种类型的消息,可以根据消息判定,简单的用 if...else 处理,复杂的用工厂 + 策略模式
                    log.error("正在消费message: {}", message);
                    //手动确认消息,告诉RabbitMQ这条消息我已经成功处理了,请从队列中删除
                    /**
                     * deliveryTag:每条消息的唯一标识
                     * multiple:是否批量确认,true表示批量确认,false表示单个确认
                     */
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            };
            // 取消自动ack
            /**
             * 监听队列,该方法内部使用RabbitMQ内部的线程池异步执行,不会阻塞后续消息
             * queue:拿取消息的队列名称
             * autoAck:是否自动ack,true表示自动ack,false表示手动ack
             * consumer:消费者
             */
            channel.basicConsume(queueName, false, consumer);

        } catch (InterruptedException | IOException  e) {
            e.printStackTrace();
        }
    }
}

1.4 消费逻辑的入口代码

负责初始化连接池 ,初始化消费逻辑的入口

java 复制代码
@Slf4j
@Configuration
@EnableConfigurationProperties(RabbitmqProperties.class)
public class RabbitMqAutoConfig implements ApplicationRunner {
    @Resource
    private RabbitmqService rabbitmqService;

    @Autowired
    private RabbitmqProperties rabbitmqProperties;


    @Override
    public void run(ApplicationArguments args) throws Exception {
        String host = rabbitmqProperties.getHost();
        Integer port = rabbitmqProperties.getPort();
        String userName = rabbitmqProperties.getUsername();
        String password = rabbitmqProperties.getPassport();
        String virtualhost = rabbitmqProperties.getVirtualhost();
        Integer poolSize = rabbitmqProperties.getPoolSize();
        RabbitmqConnectionPool.initRabbitmqConnectionPool(host, port, userName, password, virtualhost, poolSize);
        Thread thread=new Thread(() -> {
            try {
            	log.info("手动线程开始:{}", Thread.currentThread().getName());
                rabbitmqService.consumerMsg("direct.exchange", "quere.praise","praise");
                log.info("手动线程结束:{}", Thread.currentThread().getName());
            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        });
        /**
        *启动消费程序的入口
        */
        thread.start();
    }
}

1.5发送逻辑的入口代码

我这里是用了controller层,用浏览器控制消息的发送

java 复制代码
@RestController
public class RabbitmqTest{

    @Autowired
    private RabbitmqService rabbitmqService;

    @GetMapping("/test")
    public void testProductRabbitmq(String msg) {
        try {
            rabbitmqService.publishMsg(
                    "direct.exchange",
                    BuiltinExchangeType.DIRECT,
                    "praise",
                    msg);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1.6运行结果

项目启动后

在浏览器输入 http://localhost:8080/test?msg=看看项目运行了没

控制台输出如下:

1.7疑问解答

为什么控制台打印时有很多线程呢?

  1. 你在 run 方法中手动启动的 Thread,核心任务是初始化消费逻辑的 "入口";
  2. 手动线程调用 channel.basicConsume 后,客户端会与 RabbitMQ 建立 长连接,并在后台维护一个 "消息监听循环",这个手动线程的任务就结束了(它不会一直阻塞等待消息),真正的消息消费逻辑由 RabbitMQ 客户端的内部线程池处理。
  3. 当 RabbitMQ 有消息推送给消费者时,客户端不会在你手动启动的线程中处理,而是从 内部线程池 中取出一个空闲线程,执行 DefaultConsumer 的 handleDelivery 方法(即你的消费逻辑);
  4. 控制台日志中 pool-1-thread-4、pool-1-thread-5... 就是这个内部线程池的线程,每处理一条消息可能复用或新建线程(取决于线程池配置);

生产者和消费者是如何实现沟通的呢?

发送消息:拿到连接--->创建通道-->声明交换机-->发送消息-->关闭连接

消费消息:拿到连接-->创建通道-->声明队列-->绑定队列到交换机-->接收并消费消息

java 复制代码
//生产者方法
①channel.exchangeDeclare(exchange, exchangeType, true, false, null);
②channel.queueDeclare(queueName, true, false, false, null);
//消费者方法
③channel.queueBind(queueName, exchange, routingKey);
④channel.basicPublish(exchange, toutingKey, null, message.getBytes());
⑤channel.basicConsume(queueName, false, consumer);
  1. 绑定队列到交换机(queueBind)时,指定的交换机名称相同,即为同一个,由方法①②③实现(③的queueName与②的queueName保持一致,③的exchange与①的exchange保持一致);
  2. 发布消息时,指定发往的交换机名称和路由键 ,由方法④实现(toutingKey);
    绑定队列到交换机时,指定队列的路由键 ,由方法③实现(routingKey);
    根据交换机的类型,若为direct 类型,必须routingKeytoutingKey完全一致时才能收到消息;
  3. 接收消息时,指定从哪个队列接收消息,由方法⑤实现(queueName);

消费者是怎么实现一直监听的呢?

首先 要知道,生产者 一发送完毕消息,立马执行channel.close();关闭通道

和RabbitmqConnectionPool.returnConnection(rabbitmqConnection);归还连接
消费者不关闭通道,也不归还连接,相当于是一个长连接,一直监听着队列内是否有消息,只要有消息就消费消息。

1.8 如何优化

1.8.1 重连机制

问题 :如果因为一些异常,导致消费方法终止,必须有重新启动机制

在consumerMsg方法中增加以下代码:

java 复制代码
	// 设置连接关闭回调
	connection.addShutdownListener(cause -> {
		log.warn("RabbitMQ连接意外关闭,准备重连: {}", cause.getMessage());
		// 启动重连任务
		new Thread(() -> {
        	try {
            	Thread.sleep(5000); // 延迟5秒重试
            	consumerMsg(exchange, queueName, routingKey); // 重新调用消费方法
        	} catch (InterruptedException e) {
            	Thread.currentThread().interrupt();
        	}
    	}).start();
	});

1.8.2 异步处理消息

问题 :如果我们在消息处理中直接执行耗时的业务逻辑(如 DB 操作、远程调用),那么客户端的这个线程就会被阻塞,无法处理新的消息,导致整体消费速率下降。
解决方案:RabbitMQ 客户端的内部线程池负责 "接收消息",而我们自己添加的线程池负责 "处理消息"

java 复制代码
	//这里的线程池只是模拟演示
	ExecutorService executor = Executors.newFixedThreadPool(10);
	Consumer consumer = new DefaultConsumer(channel) {
		@Override
		public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                           byte[] body) throws IOException {
			executor.submit(() -> {
				try {
					// 业务逻辑处理
					//...
					//手动确认
					channel.basicAck(...);
				} catch (Exception e) {
					//发生异常时,不确认,消息重新回到队列中,保证消息不丢失
					channel.basicNack(...);
				}
			});
		}
	};

1.8.3 优雅的关闭

问题 :JVM 退出时会 "回收" 资源,但不会 "优雅关闭" 资源。

在consumeMsg方法中添加以下代码:

java 复制代码
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    try {
        channel.close();
        connection.close();
    } catch (Exception e) {
        // 处理关闭异常
    }
}));
相关推荐
FenceRain3 小时前
spring boot 拦截器增加语言信息
java·spring boot·后端
星月前端3 小时前
idea没法识别springboot项目的一个原因解决及办法
java·spring boot·intellij-idea
weixin_436525073 小时前
Spring Boot 集成 EasyExcel 的最佳实践:优雅实现 Excel 导入导出
java·spring boot·后端
ChinaRainbowSea3 小时前
9. LangChain4j + 整合 Spring Boot
java·人工智能·spring boot·后端·spring·langchain·ai编程
武昌库里写JAVA3 小时前
Mac下Python3安装
java·vue.js·spring boot·sql·学习
Light604 小时前
领码方案|Linux 下 PLT → PDF 转换服务超级完整版:异步、权限、进度(一气呵成)
linux·spring boot·pdf·gpcl6/ghostpcl·s3/oss·权限与审计·异步与进度
召摇5 小时前
Spring Boot 内置工具类深度指南
java·spring boot
Moshow郑锴7 小时前
SpringBootCodeGenerator使用JSqlParser解析DDL CREATE SQL 语句
spring boot·后端·sql
小沈同学呀13 小时前
创建一个Spring Boot Starter风格的Basic认证SDK
java·spring boot·后端