【Java】最新版本SpringCloudStream整合RocketMQ实现单项目中事件的发布与监听

简介: 【Java】最新版本SpringCloudStream整合RocketMQ实现单项目中事件的发布与监听

前言

SpringCloud项目中整合RocketMQ是为了削峰填谷。

这里我使用RocketMQ的作用用于接收项目中产生的消息,然后异步的发送邮件给客户,这是这个项目的产生的背景。

依赖配置

<dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <!-- 引入基于 RocketMQ 的 Spring Cloud Bus 的实现的依赖,并实现对其的自动配置 -->
            <artifactId>spring-cloud-starter-bus-rocketmq</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.rocketmq</groupId>
                    <artifactId>rocketmq-client</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.rocketmq</groupId>
                    <artifactId>rocketmq-acl</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <!-- 引入基于 RocketMQ 的 Spring Cloud Bus 的实现的依赖,并实现对其的自动配置 -->
            <artifactId>spring-cloud-starter-bus-rocketmq</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.9.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-acl</artifactId>
            <version>4.9.4</version>
        </dependency>
    </dependencies>

项目导入上面依赖之后即可开始代码的编写

代码

然后让我们先看一眼配置文件

# Tomcat
server:
  port: 9201
# Spring
spring:
  application:
    # 应用名称
    name: towelove-system
  profiles:
    # 环境配置
    active: dev
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: localhost:8848
      config:
        # 配置中心地址
        server-addr: localhost:8848
        # 配置文件格式
        file-extension: yaml
        # 共享配置
        shared-configs[0]:
          data-id: towelove-base-dev.yaml
          refresh: true
        shared-configs[1]:
          data-id: towelove-mysql-dev.yaml
          refresh: true
        shared-configs[2]:
          data-id: towelove-redis-dev.yaml
          refresh: true
    # Spring Cloud Stream 配置项,对应 BindingServiceProperties 类
    stream:
      function:
        definition: mailSendConsumer;sendSmsToAdmin;sendSmsToUser; # 需要确保消费者类的名称和这里一样
      # Binding 配置项,对应 BindingProperties Map
      bindings:
        sendSmsToAdmin-out-0: # 配置生产者
          destination: admin_sms_send
        sendSmsToAdmin-in-0:
          destination: admin_sms_send
          group: system_sms_send_consumer_group
        sendSmsToUser-out-0: # 配置生产者
          destination: admin_sms_send
        sendSmsToUser-in-0:
          destination: admin_sms_send
          group: system_sms_send_consumer_group
#        smsSendConsumer-in-0: # 配置消费者
#          destination: admin_sms_send
#          group: system_sms_send_consumer_group
#        smsSend-out-1:
#          destination: user_sms_send
#        smsSendConsumer-in-1:
#          destination: user_sms_send
#          group: system_sms_send_consumer_group
        mailSend-out-0:
          destination: system_mail_send
        mailSendConsumer-in-0: # 需要确保消费者类的名称和这里一样
          destination: system_mail_send
          group: system_mail_send_consumer_group
      # Spring Cloud Stream RocketMQ 配置项
      rocketmq:
        # RocketMQ Binder 配置项,对应 RocketMQBinderConfigurationProperties 类
        binder:
          name-server: 192.168.146.115:9876 # RocketMQ Namesrv 地址
        #          access-key: # 用户名
        #          secret-key:  # 密码
        default: # 默认 bindings 全局配置
          producer: # RocketMQ Producer 配置项,对应 RocketMQProducerProperties 类
            group: system_producer_group # 生产者分组
            send-type: SYNC # 发送模式,SYNC 同步
              # 如果你项目里只对接一个中间件,那么不用定义binders
              # 当系统要定义多个不同消息中间件的时候,使用binders定义
              #      binders:
              #        my-rocketmq:
              #          type: rocketmq
              #          environment:
              #            rocketmq:
              #              name-server: 192.168.146.115:9876
              #          access-key: # 用户名
            #          secret-key:  # 密码
    # Spring Cloud Bus 配置项,对应 BusProperties 类
    bus:
      enabled: true # 是否开启,默认为 true
      id: ${spring.application.name}:${server.port} # 编号,Spring Cloud Alibaba 建议使用“应用:端口”的格式
      destination: springCloudBus # 目标消息队列,默认为 springCloudBus

这里我截取了比较重要的配置,然后下面进行配置的讲解

首先就是我写了特别多注释的一个spring.cloud.stream.function.definition

这个东西是什么作用呢?

我的理解是,它用来声明你当前项目中的消费者,以及消费者类中的方法。

然后就是spring.cloud.stream.bindings中的好多个xxx-out-0和xxx-in-0

其中out对应的项目的输出,也就是消息的产生,对应的就是项目中的生产者,生产者发送消息的需要指定对应的信道,也就是你要告诉他往哪里发,其实就是对应的broker(再RocketMQ里面是这样子的),并且设定你发往的这个broker对应的topic,也就是destination。

那么同理,当生产者吧消息发送到broker中对应的topic后,我们就需要消费者去消费这个消息了。

那么此时就是使用in标签。

in标签里面的destination表示的也就是当前消费者需要去消费哪一个topic里面的消息。

你可能有一个疑问就是,那么为什么不用去指定对应的broker呢?

下面就是讲解这个in和out标签的声明的规则。

其实这也是一种约定优于配置的思想。

其中functionName就是你的消费者的类名或者你要提供消费的方法。

在命名规则的最后还有一个 index,它是 input 和 output 的序列,如果同一个 function name 只有一个 output 和一个 input,那么这个 index 永远都是 0。而如果你需要为一个 function 添加多个 input 和 output,就需要使用 index 变量来区分每个生产者消费者了。

Input 信道(消费者):< functionName > - in - < index >;

Output 信道(生产者):< functionName > - out - < index >。

讲解完这些,你大概就理解了这里的代码是为什么这么编写了。

那么下面我引入具体的业务代码。

我们从底层向上。

首先是消息的实体类。

@Data
public class SmsSendMessage {
    /**
     * 邮件日志编号
     */
    @NotNull(message = "邮件日志编号不能为空")
    private Long logId;
    /**
     * 接收邮件地址
     */
    @NotNull(message = "电话号码不能为空")
    private String phonenumber;
    /**
     * 邮件账号编号
     */
    @NotNull(message = "邮件账号编号不能为空")
    private Long accountId;
    /**
     * 邮件发件人
     */
    private String nickname;
    /**
     * 邮件标题
     */
    @NotEmpty(message = "邮件标题不能为空")
    private String title;
    /**
     * 邮件内容
     */
    @NotEmpty(message = "邮件内容不能为空")
    private String content;
    private Boolean isHtml;
    private File[] files;
}

这个是消息的生产者

@Slf4j
@Service
public class SmsProducer {
    @Autowired
    private StreamBridge streamBridge;
    public void sendSmsToAdmin(SmsSendMessage message) {
        log.info("要发送的短信内容为: {}", message);
        streamBridge.send("sendSmsToAdmin-out-0", message);
    }
    public void sendSmsToUser(Long userId,Long accountId) {
        log.info("要发送的短信内容为: {}", "userId:"+userId+"accountId:"+accountId);
        streamBridge.send("sendSmsToUser-out-0",  "userId:"+userId+"  accountId:"+accountId);
    }
}

然后就是控制层

@RestController
@RequestMapping("/sys/sms")
public class SmsController {
    @Autowired
    private SmsProducer smsProducer;
    @PostMapping("/send/admin")
    public R<Boolean> sendSmsToAdmin(@RequestBody @Valid SmsSendMessage message){
        smsProducer.sendSmsToAdmin(message);
        return R.ok();
    }
    @PostMapping("/send/user")
    public R<Boolean> sendSmsToUser(@RequestParam("userId")Long userId,
                                    @RequestParam("accountId")Long accountId){
        smsProducer.sendSmsToUser(userId,accountId);
        return R.ok();
    }
}

然后下面是事件消费者的第一种写法

@Component
@Slf4j
public class SmsSendConsumer //implements Consumer<SmsSendMessage>
{
    //@Override
    //public void accept(SmsSendMessage message) {
    //    System.out.println(message);
    //}
    @Bean
    public Consumer<String> sendSmsToAdmin() {
        return reqest -> {
            log.info("received: {} ", reqest);
        };
    }
    @Bean
    public Consumer<String> sendSmsToUser(){
        return request -> {
            log.info("received: {}", request);
            List<Long> params = Arrays.stream(request.split(","))
                    .map(Long::valueOf)
                    .collect(Collectors.toList());
            System.out.println(params);
        };
    }
}

简单的介绍一下代码的逻辑,

其实就是我们向控制层发送一个请求并且携带上一些参数之后,控制层让生产者发送一个消息到对应的消息队列中。

发现了吗,这里消息的生产者发送的消息的目的地,就是我们设定的out标签。

那么消费者如何知道要去消费消息呢?

这就是为什么上面我说function.definition和in标签的作用了。

in标签这里的前缀就是我们的方法名,也就是对应的broker中的topic有消息后,对应的消费者会把消息拉过来,然后进行消费,而他之所以能知道要去消费哪一个消息也就是因为这里的绑定好的原因。

所以如果你一个类中声明了多个的消费方法,只需要再function.definition这个地方声明出你方法的名称,并且再代码里面使用@Bean的方式去声明出对应的方法即可

也就是如下图一样。

那么好奇的你可能会发现,这样子可以定义多个方法,还挺不错的,就是好像有点麻烦欸,要写的东西一下子就多了。

所以,如果你的消费者类只有一个方法,也就是你当前要消费的消费者只需要提供唯一的方法,那么我们可以把function.definition这里的方法名编写为消费者类的名称。

也就是下面这种代码的方式

而我们的生产者还是一样,只要确保其发送消息的信道是确定的即可

那么以这两种方式,如果你的消费者需要提供多个方法,那么就使用第一种方式,而如果你的消费者是单一的,只需要提供某一种方法,那么直接使用第二种方法去实现某个类即可。

当然,两种方式可以混合在一起实现

如果你在你的代码中出现了下图的问题

可以查看我下面这篇文章

解决上图的问题

类似的springcloudstream整合rocketmq的问题可以私信我一起研究


相关实践学习
消息队列RocketMQ版:基础消息收发功能体验
本实验场景介绍消息队列RocketMQ版的基础消息收发功能,涵盖实例创建、Topic、Group资源创建以及消息收发体验等基础功能模块。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
1月前
|
关系型数据库 MySQL Java
【MySQL+java+jpa】MySQL数据返回项目的感悟
【MySQL+java+jpa】MySQL数据返回项目的感悟
42 1
|
1天前
|
Java Android开发
Eclipse 创建 Java 项目
Eclipse 创建 Java 项目
11 4
|
6天前
|
SQL Java 数据库连接
从理论到实践:Hibernate与JPA在Java项目中的实际应用
本文介绍了Java持久层框架Hibernate和JPA的基本概念及其在具体项目中的应用。通过一个在线书店系统的实例,展示了如何使用@Entity注解定义实体类、通过Spring Data JPA定义仓库接口、在服务层调用方法进行数据库操作,以及使用JPQL编写自定义查询和管理事务。这些技术不仅简化了数据库操作,还显著提升了开发效率。
17 3
|
28天前
|
存储 消息中间件 安全
JUC组件实战:实现RRPC(Java与硬件通过MQTT的同步通信)
【10月更文挑战第9天】本文介绍了如何利用JUC组件实现Java服务与硬件通过MQTT的同步通信(RRPC)。通过模拟MQTT通信流程,使用`LinkedBlockingQueue`作为消息队列,详细讲解了消息发送、接收及响应的同步处理机制,包括任务超时处理和内存泄漏的预防措施。文中还提供了具体的类设计和方法实现,帮助理解同步通信的内部工作原理。
JUC组件实战:实现RRPC(Java与硬件通过MQTT的同步通信)
|
9天前
|
前端开发 Java 数据库
如何实现一个项目,小白做项目-java
本教程涵盖了从数据库到AJAX的多个知识点,并详细介绍了项目实现过程,包括静态页面分析、数据库创建、项目结构搭建、JSP转换及各层代码编写。最后,通过通用分页和优化Servlet来提升代码质量。
23 1
|
1月前
|
JavaScript 前端开发 Java
解决跨域问题大集合:vue-cli项目 和 java/springboot(6种方式) 两端解决(完美解决)
这篇文章详细介绍了如何在前端Vue项目和后端Spring Boot项目中通过多种方式解决跨域问题。
313 1
解决跨域问题大集合:vue-cli项目 和 java/springboot(6种方式) 两端解决(完美解决)
|
15天前
|
Java Linux Windows
如何查看已安装的 Java 版本
要查看已安装的 Java 版本,打开命令提示符或终端,输入 `java -version`,回车后即可显示当前系统中 Java 的版本信息。
|
15天前
|
Ubuntu Java Linux
如何检查 Java 版本是否兼容
要检查Java版本是否兼容,可在命令行输入“java -version”查看当前安装的Java版本,然后对比目标应用所需的Java版本,确保其满足要求。
|
16天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
29天前
|
Java Apache Maven
Java/Spring项目的包开头为什么是com?
本文介绍了 Maven 项目的初始结构,并详细解释了 Java 包命名惯例中的域名反转规则。通过域名反转(如 `com.example`),可以确保包名的唯一性,避免命名冲突,提高代码的可读性和逻辑分层。文章还讨论了域名反转的好处,包括避免命名冲突、全球唯一性、提高代码可读性和逻辑分层。最后,作者提出了一个关于包名的问题,引发读者思考。
Java/Spring项目的包开头为什么是com?