文章目录
参考牛客网高级项目教程
尚硅谷kafka教学笔记
功能需求
- 对指定用户评论、点赞、关注等事件触发后,系统要向指定用户发送通知消息
- 因为事件比较多,也为了方便扩展,解耦-使用kafka消息队列,向消息队列中发布数据
- 消费topic,即需要将发布的信息数据存入mysql数据库,采用异步方式,提高响应速度,也起到消峰作用
- 对kafka发送消息做了设计优化,以事件为驱动
- 封装事件对象
- 因要发送的消息内容相似,可以将消息封装成一个事件对象
- 即将触发事件中包含消息的所有数据进行封装,方便扩展
- 开发事件的生产者
- 这样,发送的不只是一条消息,而是一个以topic分类的事件对象,方便消费者统一处理topic中的数据
- 以JSON格式储存对象
- 开发事件的消费者
- 最终消费者会将队列中存入的事件对象数据(消息)取出,创建message对象入库保存
- 封装事件对象
1. 封装事件对象
- 封装事件触发者、触发对象一般均有的属性信息
- 用map来接收其他数据信息-例如对评论和点赞来说,需要帖子的id
- 修改各自的set方法,设置返回类型为Event当前类,方便对当前对象重复设置调用
- map数据,传入key,value,向map中添加数据,返回类型当前对象,方便多次调用添加键值对
- 注意:一定要先初始化一个实例
package com.nowcoder.community.entity;
import java.util.Map;
public class Event {
// kafka服务器要识别的topic事件类型
private String topic;
// 事件触发者信息
private int fromUserId;
// 事件触发对象的信息
private int entityType;
private int entityId;
private int entityUserId;
// 其他信息-用map封装保存
private Map<String, Object> data = new HashMap<>();
public int getFromUserId() {
return fromUserId;
}
/**
* 修改各自的set方法,设置返回类型为Event当前类,方便对当前对象重复设置调用
* @param fromUserId
* @return 返回Event类型对象
*/
public Event setFromUserId(int fromUserId) {
this.fromUserId = fromUserId;
return this;
}
...
/**
* map数据,传入key,value,向map中添加数据,返回类型当前对象,方便多次调用添加键值对
* @param key 传入map的key
* @param value 传入map的value
* @return
*/
public Event setData(String key, Object value) {
this.data.put(key, value);
return this;
}
}
2. 开发事件的生产者
定义事件主题常量
/**
* kafka主题-事件:帖子-评论
*/
String TOPIC_COMMENT = "comment";
/**
* kafka主题-事件:关注
*/
String TOPIC_FOLLOW = "follow";
/**
* kafka主题-事件:点赞
*/
String TOPIC_LIKE = "like";
/**
* 系统用户id
*/
int SYSTEM_USER_ID = 1;
生产者发送消息
JSONObject.toJSONString(event)
- 将消息发送到指定主题上
- 发送的是事件对象,发送时,转为JSON字符串的格式发送
@Component
public class EventProducer implements CommunityConstant {
@Autowired
KafkaTemplate kafkaTemplate;
/**
* 将消息发送到指定主题上
* @param event 要发送的消息封装成的主题对象
*/
public void sendEvent(Event event) {
kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
}
}
3. 消费者获取消息,并异步入库
- 1.边界条件:先检查有无取到消息
- 2.将拿到的消息恢复成Object类型,方便操作
- 3.用拿到的数据创建Message对象,入库
- Message的content,是要通知的内容,由消息中的数据拼接而成
Map.Entry<String, Object> entry : event.getData().entrySet()
- event中的其他封装在map中的数据,也都一一拿出来放进content中
@Component
public class EventConsumer implements CommunityConstant {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
KafkaTemplate kafkaTemplate;
@Autowired
MessageService messageService;
/**
* 被动订阅消息,并将消息入库
* @param record
*/
@KafkaListener(topics = {TOPIC_FOLLOW, TOPIC_COMMENT, TOPIC_LIKE})
public void handleMessage(ConsumerRecord record) {
// 1.边界条件:先检查有无取到消息
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
// 2.将拿到的消息恢复成Object类型,方便操作
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
if(event == null) {
logger.error("消息的格式错了!");
return;
}
// 3.用拿到的数据创建Message对象,入库
Message message = new Message();
message.setFromId(SYSTEM_USER_ID);
message.setToId(event.getEntityUserId());
message.setConversationId(event.getTopic());
message.setCreateTime(new Date());
// Message的content,是要通知的内容,由消息中的数据拼接而成
// xxx了您的xxx
Map<String, Object> content = new HashMap<>();
content.put("userId", event.getFromUserId());
content.put("entityType", event.getEntityType());
content.put("entityId", event.getEntityId());
// event中的其他数据,也都放进content中
if (event.getData() != null) {
for (Map.Entry<String, Object> entry : event.getData().entrySet()) {
content.put(entry.getKey(), entry.getValue());
}
}
message.setContent(JSONObject.toJSONString(content));
// 入库-调用service层业务代码-增加了过滤器
messageService.addMessage(message);
}
}
4. 在controller层更新系统发送通知代码
1. 评论
- 添加完评论后,系统向目标用户发送通知-触发评论事件
- 当前评论所属的帖子id-属于其他信息
- 为了在显示系统通知页面能链接到指定的帖子详情页面
- 事件对象的作者-分情况判定-帖子作者、评论作者
- 当前评论所属的帖子id-属于其他信息
@RequestMapping(value = "/add/{discussPostId}", method = RequestMethod.POST)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
// 除请求中需要写的评论内容外,需提供其他素材
comment.setCreateTime(new Date());
comment.setUserId(hostHolder.getUser().getId());
comment.setStatus(0);
// 将数据交给service处理
commentService.addComment(comment);
// 添加完评论后,系统向目标用户发送通知-触发评论事件
// 封装评论事件信息
Event event = new Event()
.setTopic(TOPIC_COMMENT)
.setFromUserId(hostHolder.getUser().getId())
.setEntityType(comment.getEntityType()) // 评论的可以是帖子,回帖,回复
.setEntityId(comment.getEntityId())
.setData("postId", discussPostId); // 当前评论所属的帖子id
// 事件对象的作者-分情况判定-帖子作者、评论作者
if (comment.getEntityType() == ENTITY_TYPE_POST) {
DiscussPost target = discussPostService.selectPostById(comment.getEntityId());
event.setEntityUserId(target.getUserId());
} else (comment.getEntityType() == ENTITY_TYPE_COMMENT) {
Comment target = commentService.selectCommentById(comment.getEntityId());
event.setEntityUserId(target.getUserId());
}
// 将信息发送到消息队列中
eventProducer.sendEvent(event);
return "redirect:/discuss/detail/" + discussPostId;
}
2.点赞
- 触发点赞事件后-系统向目标用户发送通知
- 为了获取当前点赞实体所属的帖子,需要传入帖子id,因此,模板页面和js也需要相应修改
/**
* 处理点赞的异步请求
* @param entityType
* @param entityId
* @return json字符串,不传递msg,如果有问题,直接在网页alert提示
*/
@RequestMapping(value = "/like", method = RequestMethod.POST)
@ResponseBody
public String like(int entityType, int entityId, int entityUserId, int postId) {
// 用来封装信息的map
Map<String, Object> map = new HashMap<>();
// 权限-统一管理,先获取当前用户
User user = hostHolder.getUser();
// 点赞事件处理
likeService.like(user.getId(), entityType, entityId, entityUserId);
// 点赞数量获取
long likeCount = likeService.findEntityLikeCount(entityType, entityId);
// 点赞状态获取
int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
// 统一封装到map传给前端页面
map.put("likeCount", likeCount);
map.put("likeStatus", likeStatus);
// 触发点赞事件后-系统向目标用户发送通知
if(likeStatus == 1) {
Event event = new Event()
.setTopic(TOPIC_LIKE)
.setFromUserId(hostHolder.getUser().getId())
.setEntityType(entityType)
.setEntityId(entityId)
.setEntityUserId(entityUserId)
.setData("postId", postId);
eventProducer.sendEvent(event);
}
return CommunityUtil.getJSONString(0, null, map);
}
3.关注
- 目前关注的都是人,因此实体类型都是User
/**
* 处理关注的异步请求
* @param entityType 关注对象类型
* @param entityId 关注对象id
* @return
*/
@RequestMapping(value = "/follow", method = RequestMethod.POST)
@ResponseBody
public String follow(int entityType, int entityId) {
User user = hostHolder.getUser();
if(user == null) { // 拦截器已经拦截,若拦截不成功,再次抛出异常
throw new IllegalArgumentException("用户没有登录!");
}
followService.follow(user.getId(), entityType, entityId);
// 触发关注事件-系统向关注对象发送通知
Event event = new Event()
.setTopic(TOPIC_FOLLOW)
.setFromUserId(hostHolder.getUser().getId())
.setEntityType(entityType)
.setEntityId(entityId)
.setEntityUserId(entityId); // 目前关注的都是人,因此实体类型都是User
eventProducer.sendEvent(event);
return CommunityUtil.getJSONString(0, "关注成功!");
}