携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第27天,点击查看活动详情
5.3 Spring整合Kafka
1. 引入依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
2. 配置Kafka
在 application.properties 配置Kafka
# KafkaProperties
# 启动哪个服务器的kafka
spring.kafka.bootstrap-servers=localhost:9092
# 配置消费者的分组id(可以在consumer.properties配置文件中找到)
spring.kafka.consumer.group-id=test-consumer-group
# 是否自动提交(是否自动提交消费者的偏移量)
spring.kafka.consumer.enable-auto-commit=true
# 自动提交的频率(3000表示3000ms也就是3s提交一次)
spring.kafka.consumer.auto-commit-interval=3000
3. 访问Kafka
在cmd命令行窗口分别启动 zookeeper 和 Kafka ,然后运行下面测试代码
下面写一段测试代码来看一下怎么去用
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = CommunityApplication.class)
public class KafkaTests {
@Autowired
private KafkaProducer kafkaProducer;
@Test
public void testKafka(){
kafkaProducer.sendMessage("test", "111");
kafkaProducer.sendMessage("test", "222");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Component
class KafkaProducer{
@Autowired
private KafkaTemplate kafkaTemplate;
public void sendMessage(String topic, String context){
kafkaTemplate.send(topic, context);
}
}
@Component
class KafkaConsumer {
@KafkaListener(topics = {"test"})
public void handlerMessage(ConsumerRecord record){
System.out.println(record.value());
}
}
5.4 发送系统通知
定义三个 topic:Comment、Like、Follow
从业务角度,称解决问题的方式是事件驱动的方式(评论是一个事件,点赞是一个事件,关注是一个事件),在解决问题的时候是以事件为目标来解决的,所以在开发的时候是基于事件对代码的逻辑再进一步的封装,而不是消息。
定义事件对象,对事件发生的时候所需的数据进行封装,而不是说就发一条消息拼一个字符串,我们拼一个事件对象,这个事件对象中包含了这条消息所需要的所有的数据,至于说消费者想怎么拼,那就是你的事,这样的话会更具扩展性一些,而不是拼死一个字符串。
封装事件对象之后开发生产者、消费者,生产的是事件,消费的也是事件,最终是要把事件转换为消息插入到数据库里。
使用kafka之前记得在application.properties配置文件中配置一下kafka
# KafkaProperties
# 启动哪个服务器的kafka
spring.kafka.bootstrap-servers=localhost:9092
# 配置消费者的分组id(可以在consumer.properties配置文件中找到)
spring.kafka.consumer.group-id=test-consumer-group
# 是否自动提交(是否自动提交消费者的偏移量)
spring.kafka.consumer.enable-auto-commit=true
# 自动提交的频率(3000表示3000ms也就是3s提交一次)
spring.kafka.consumer.auto-commit-interval=3000
定义一个事件对象
首先定义一个事件对象对事件进行封装,封装事件触发的时候相关的一切的信息。
对于这个事件对象,为了以后调用方便,我们把所有的set方法返回值都不设置为空,而是设置为返回Event事件对象,还有setDate方法,我们修改为往里面存值,返回值还是Event事件对象。
public class Event {
private String topic; // 主题(由事件的类型分别存到不同的位置)
private int userId; // 这个事件是谁发的
private int entityType; // 这个人做了什么操作(点赞/回复/关注)
private int entityId; // 实体id
private int entityUserId; // 这个实体的作者是谁
private Map<String, Object> data = new HashMap<>(); // 其他额外的数据都存到这个map里
public String getTopic() {
return topic;
}
public Event setTopic(String topic) {
this.topic = topic;
return this;
}
public int getUserId() {
return userId;
}
public Event setUserId(int userId) {
this.userId = userId;
return this;
}
public int getEntityType() {
return entityType;
}
public Event setEntityType(int entityType) {
this.entityType = entityType;
return this;
}
public int getEntityId() {
return entityId;
}
public Event setEntityId(int entityId) {
this.entityId = entityId;
return this;
}
public int getEntityUserId() {
return entityUserId;
}
public Event setEntityUserId(int entityUserId) {
this.entityUserId = entityUserId;
return this;
}
public Map<String, Object> getData() {
return data;
}
public Event setData(String key, Object value) {
this.data.put(key, value);
return this;
}
}
开发事件的生产者
@Component
public class EventProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 处理事件
public void fireEvent(Event event) {
// 将事件发布到指定的主题
// 参数1:topic 参数2:事件对象转化成的json字符串
kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
}
}
开发消费者
一个方法可以消费一个主题,也可以一个方法消费多个主题,同时,一个主题也可以被多个方法消费,多对多的关系,因为评论、点赞、关注三种事件的通知逻辑很相似,这里写一个方法把这三个主题都处理掉。
在CommunityConstant常量接口中定义主题常量
/**
* 主题: 评论
*/
String TOPIC_COMMENT = "comment";
/**
* 主题: 点赞
*/
String TOPIC_LIKE = "like";
/**
* 主题: 关注
*/
String TOPIC_FOLLOW = "follow";
/**
* 系统用户ID
*/
int SYSTEM_USER_ID = 1;
message表
CREATE TABLE `message` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`from_id` int(11) DEFAULT NULL,
`to_id` int(11) DEFAULT NULL,
`conversation_id` varchar(45) NOT NULL,
`content` text,
`status` int(11) DEFAULT NULL COMMENT '0-未读;1-已读;2-删除;',
`create_time` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_from_id` (`from_id`),
KEY `index_to_id` (`to_id`),
KEY `index_conversation_id` (`conversation_id`)
) ENGINE=InnoDB AUTO_INCREMENT=357 DEFAULT CHARSET=utf8;
回顾一下message表,
其中conversation_id 是会话id,是form_id和to_id拼到一起的
但是现在发的消息和之前发的私信有所区别,私信是张三发给李四,
是两个用户之间发,而现在发的通知是我们的系统发给用户,后台的话
from_id其实真实不存在,我们可以假设后台也是一个user,假设这个
user的id是1,造一个虚拟的用户,永远都是1向某人发消息,这个时候
conversation_id再去存这个两个id拼在一起就没有意义了,因为肯定
form_id是1固定的,因此conversation_id改存为主题,比如:comment、like、follow
内容content存的就不是一句话,存的是在页面展示出的那句话要依赖的条件json字符串,
这个字符串包含了我们在页面上要拼出的展示的那句话的各种条件
也就是说复用message这张表,或者说message这张表存了两类数据,一类是人与人之间的私信,
一类是系统发的通知,它们存的时候有所变通
消费者:EventConsumer
@Component
public class EventConsumer implements CommunityConstant {
private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);
@Autowired
private MessageService messageService;
@KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
public void handleCommentMessage(ConsumerRecord record) { // 这个参数用来接收相关的数据
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
// 将record的value(json类型字符串)转换为Event类型
Event event = (Event) JSONObject.parseObject(record.value().toString(), Event.class);
if (event == null) {
logger.error("消息格式错误!");
return;
}
// 发送站内通知
Message message = new Message();
message.setFromId(SYSTEM_USER_ID);
message.setToId(event.getEntityUserId());
message.setConversationId(event.getTopic());
// 状态默认就是0,表示未读,不用去设置
message.setCreateTime(new Date());
Map<String, Object> content = new HashMap<>();
content.put("userId", event.getUserId()); // 为了查找用户名
content.put("entityType", event.getEntityType()); // 为了知道类型
content.put("entityId", event.getEntityId()); // 为了传帖子id然后跳转到帖子详情
if (!event.getData().isEmpty()) {
for (Map.Entry<String, Object> entry : event.getData().entrySet()) {
content.put(entry.getKey(), entry.getValue());
}
}
// 将Map类型的content转换为json字符串存到message的content字段
message.setContent(JSONObject.toJSONString(content));
// 将message存入数据库
messageService.addMessage(message);
}
}
接下来我们需要找个地方去调这个程序,什么时候触发事件就去调一下生产者处理事件,消费者是被动触发的,只要队列中有数据就自动执行了,这个不用我们主动去调,我们只要主动去调生产者就行,按照之前的业务,我们应该是在 评论、点赞、关注 这三个地方调的
在CommentMapper中增加根据id查询评论方法
Comment selectCommentById(int id);
/**
* 根据评论id查询评论
*/
然后是comment-mapper.xml
<select id="selectCommentById" resultType="Comment">
select <include refid="selectFields"></include>
from comment
where id = #{id}
</select>
然后是CommentService
public Comment findCommentById(int id) {
return commentMapper.selectCommentById(id);
}
CommentController
所以打开对应的controller,首先是 CommentController
后续消息的发布,就是由消息队列去处理了,可能略有一点点延迟,
LikeController
然后是 LikeController
重构一下方法,要求方法再多接收一个参数 要求点赞的时候把它传进来 新的字段
重构了方法,所以我们要去修改对应的themeleaf模板文件和js
**discuss-detail.html **
,${post.id}
discuss.js
FollowController
还有 FollowController
关注的时候发通知,取消关注的时候不用
测试
在测试时一定要在cmd窗口把zookeeper和kafka启动