- 触发事件
- 评论后,发布通知
- 点赞后,发布通知
- 关注后,发布通知。
- 处理事件
- 封装事件对象
- 开发事件的生产者
- 开发事件的消费者
消费者线程:从队列里读消息,并做处理;
生产者线程:往线程中存入数据;
不同的主题存放不同的业务;
事件驱动的业务逻辑,封装。
定义一个事件对象,包含所有的基本操作
package com.nowcoder.community.entity;
import java.util.HashMap;
import java.util.Map;
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<>();
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);//改成key value传入
return this;//可以连续调用
}
}
定义事件的生产者和事件的消费者
package com.nowcoder.community.event;
import com.alibaba.fastjson.JSONObject;
import com.nowcoder.community.entity.Event;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
@Component
public class EventProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 处理事件//外界调用的方法,将事件(消息)发出去即可,但后续的处理不管,归消费者管
public void fireEvent(Event event) {//外界调用的方法
// 将事件发布到指定的主题
kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));//字符串包含事件对象的所有数据
}
}
定义主题的常量:communityContant接口
/**
* 主题: 评论
*/
String TOPIC_COMMENT = "comment";
/**
* 主题: 点赞
*/
String TOPIC_LIKE = "like";
/**
* 主题: 关注
*/
String TOPIC_FOLLOW = "follow";
/**
* 系统用户ID
*/
int SYSTEM_USER_ID = 1;
EventConsumer.java
package com.nowcoder.community.event;
import com.alibaba.fastjson.JSONObject;
import com.nowcoder.community.entity.Event;
import com.nowcoder.community.entity.Message;
import com.nowcoder.community.service.MessageService;
import com.nowcoder.community.util.CommunityConstant;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class EventConsumer implements CommunityConstant {
//声明记录事件的组件
private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);
@Autowired//处理事件的意思就是,要给某个人发送一个消息;往message表中发送数据
private MessageService messageService;//注入message对象
//消费者的方法,一个方法,消费多个主题,一个主题也可以被多个方法消费
@KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
public void handleCommentMessage(ConsumerRecord record) {//record用来接受数据
if (record == null || record.value() == null) {//公共组件,要尽可能严谨,判断空的操作
logger.error("消息的内容为空!");
return;
}
//解析字符串的类型,转化为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);//系统用户:1;消息的发布者,userId;//触发事件的某个用户;EntityUserId()被触发后,接受消息的用户;
message.setToId(event.getEntityUserId());//消息的接收者
message.setConversationId(event.getTopic());//存的是主题的内容
message.setCreateTime(new Date());
//设置content的内容,要拼“用户xxx评论了你的(帖子等),点击查看(url)”
Map<String, Object> content = new HashMap<>();
content.put("userId", event.getUserId());//触发事件的用户id
content.put("entityType", event.getEntityType());//实体的类型
content.put("entityId", event.getEntityId());//实体的id
if (!event.getData().isEmpty()) {
for (Map.Entry<String, Object> entry : event.getData().entrySet()) {//遍历存储key value等额外的数据。
content.put(entry.getKey(), entry.getValue());
}
}
message.setContent(JSONObject.toJSONString(content));//存到message表里。
messageService.addMessage(message);
}
}
消费者是被动调用的,生产者是主动调用的,在评论,点赞和关注后,会调用生产者类,因此要在相应的controller里面调
在CommentMapper.java; commentService.java里面补充方法:
<select id="selectCommentById" resultType="Comment">
select <include refid="selectFields"></include>
from comment
where id = #{id}
</select>
Comment selectCommentById(int id);
public Comment findCommentById(int id) {
return commentMapper.selectCommentById(id);
}
commentController.java
package com.nowcoder.community.controller;
import com.nowcoder.community.entity.Comment;
import com.nowcoder.community.entity.DiscussPost;
import com.nowcoder.community.entity.Event;
import com.nowcoder.community.event.EventProducer;
import com.nowcoder.community.service.CommentService;
import com.nowcoder.community.service.DiscussPostService;
import com.nowcoder.community.util.CommunityConstant;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.Date;
@Controller
@RequestMapping("/comment")
public class CommentController implements CommunityConstant {
@Autowired
private CommentService commentService;
@Autowired
private HostHolder hostHolder;
@Autowired
private EventProducer eventProducer;
@Autowired
private DiscussPostService discussPostService;
@RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
comment.setUserId(hostHolder.getUser().getId());
comment.setStatus(0);
comment.setCreateTime(new Date());
commentService.addComment(comment);
// 触发评论事件
Event event = new Event()
.setTopic(TOPIC_COMMENT)
.setUserId(hostHolder.getUser().getId())
.setEntityType(comment.getEntityType())
.setEntityId(comment.getEntityId())//返回的是event类型
.setData("postId", discussPostId);//帖子id存到map里面
if (comment.getEntityType() == ENTITY_TYPE_POST) {//对帖子做评论,查帖子
DiscussPost target = discussPostService.findDiscussPostById(comment.getEntityId());
event.setEntityUserId(target.getUserId());//存到event里
} else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) {//评论的是一个评论
Comment target = commentService.findCommentById(comment.getEntityId());//查评论,在 commentService里面补充该方法
event.setEntityUserId(target.getUserId());
}
eventProducer.fireEvent(event);
return "redirect:/discuss/detail/" + discussPostId;
}
}
LikeController.java
package com.nowcoder.community.controller;
import com.nowcoder.community.entity.Event;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.event.EventProducer;
import com.nowcoder.community.service.LikeService;
import com.nowcoder.community.util.CommunityConstant;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
@Controller
public class LikeController implements CommunityConstant {
@Autowired
private LikeService likeService;
@Autowired
private HostHolder hostHolder;
@Autowired
private EventProducer eventProducer;
@RequestMapping(path = "/like", method = RequestMethod.POST)
@ResponseBody
public String like(int entityType, int entityId, int entityUserId, int postId) {//方法多接受一个参数,在帖子详情页面 点赞,这个参数容易找到
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<String, Object> map = new HashMap<>();
map.put("likeCount", likeCount);
map.put("likeStatus", likeStatus);
// 触发点赞事件
if (likeStatus == 1) {//当是点赞时,触发事件,取消点赞不触发,逻辑与评论的逻辑相同
Event event = new Event()
.setTopic(TOPIC_LIKE)
.setUserId(hostHolder.getUser().getId())
.setEntityType(entityType)
.setEntityId(entityId)
.setEntityUserId(entityUserId)
.setData("postId", postId);//需要得到帖子ID
eventProducer.fireEvent(event);
}
return CommunityUtil.getJSONString(0, null, map);
}
}
FollowController.java
package com.nowcoder.community.controller;
import com.nowcoder.community.entity.Event;
import com.nowcoder.community.entity.Page;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.event.EventProducer;
import com.nowcoder.community.service.FollowService;
import com.nowcoder.community.service.UserService;
import com.nowcoder.community.util.CommunityConstant;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
import java.util.Map;
@Controller
public class FollowController implements CommunityConstant {
@Autowired
private FollowService followService;
@Autowired
private HostHolder hostHolder;
@Autowired
private UserService userService;
@Autowired
private EventProducer eventProducer;
@RequestMapping(path = "/follow", method = RequestMethod.POST)
@ResponseBody
public String follow(int entityType, int entityId) {
User user = hostHolder.getUser();
followService.follow(user.getId(), entityType, entityId);
// 触发关注事件
Event event = new Event()
.setTopic(TOPIC_FOLLOW)
.setUserId(hostHolder.getUser().getId())
.setEntityType(entityType)
.setEntityId(entityId)
.setEntityUserId(entityId);
eventProducer.fireEvent(event);
return CommunityUtil.getJSONString(0, "已关注!");
}
@RequestMapping(path = "/unfollow", method = RequestMethod.POST)
@ResponseBody
public String unfollow(int entityType, int entityId) {
User user = hostHolder.getUser();
followService.unfollow(user.getId(), entityType, entityId);
return CommunityUtil.getJSONString(0, "已取消关注!");
}
@RequestMapping(path = "/followees/{userId}", method = RequestMethod.GET)
public String getFollowees(@PathVariable("userId") int userId, Page page, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user", user);
page.setLimit(5);
page.setPath("/followees/" + userId);
page.setRows((int) followService.findFolloweeCount(userId, ENTITY_TYPE_USER));
List<Map<String, Object>> userList = followService.findFollowees(userId, page.getOffset(), page.getLimit());
if (userList != null) {
for (Map<String, Object> map : userList) {
User u = (User) map.get("user");
map.put("hasFollowed", hasFollowed(u.getId()));
}
}
model.addAttribute("users", userList);
return "/site/followee";
}
@RequestMapping(path = "/followers/{userId}", method = RequestMethod.GET)
public String getFollowers(@PathVariable("userId") int userId, Page page, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user", user);
page.setLimit(5);
page.setPath("/followers/" + userId);
page.setRows((int) followService.findFollowerCount(ENTITY_TYPE_USER, userId));
List<Map<String, Object>> userList = followService.findFollowers(userId, page.getOffset(), page.getLimit());
if (userList != null) {
for (Map<String, Object> map : userList) {
User u = (User) map.get("user");
map.put("hasFollowed", hasFollowed(u.getId()));
}
}
model.addAttribute("users", userList);
return "/site/follower";
}
private boolean hasFollowed(int userId) {
if (hostHolder.getUser() == null) {
return false;
}
return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
}
由于更新了点赞的方法,于是再传参时,需要修改帖子详情页面的代码
启动的时候,要把kafka的服务器启动,但windows里面的kafka客服端服务有时候不稳定,会报错,但linux比较稳定;
万一在kafka的客户端在windows里面报错锁死时,将kafka-logs这个文件夹删除即可。
验证过程中报错:
所有对service的访问都是通过controller访问的,但刚才写的consumer中调用的service,没有通过ocntroller,所以取到了空指针异常。(修改完毕后,重新编译)