Phase 7: 通知系统集成
学习目标: 掌握事件驱动架构、跨模块通信、异步处理
本阶段将评论模块与系统通知模块集成,实现 @mention 和回复通知的自动创建,学习如何在模块化单体架构中实现跨模块协作。
📋 功能需求
通知场景
- @mention 通知: 当用户在评论中 @提及某个用户时,被提及用户收到通知
- 回复通知: 当评论被回复时,原评论作者收到通知
业务规则
- ✅ 用户 @自己不创建通知
- ✅ 回复自己的评论不创建通知
- ✅ 通知默认未读状态
- ✅ 异步处理,不阻塞主流程
🏗️ 架构设计
跨模块通信
graph LR
A[Comment Module] -->|依赖| B[System API]
C[System Service] -->|实现| B
A -->|发布事件| D[Spring Events]
D -->|异步处理| E[Event Listener]
E -->|调用| B
B -->|实际执行| C
技术选型
| 组件 | 技术 | 说明 |
|---|---|---|
| 事件发布 | Spring ApplicationEventPublisher | 领域事件发布 |
| 异步处理 | @Async + ThreadPoolTaskExecutor | 非阻塞异步执行 |
| 跨模块调用 | *-api 接口 | 松耦合设计 |
| 类型安全 | 类型转换工具 | 处理 Jackson 反序列化问题 |
💻 实现步骤
Step 1: 创建跨模块服务接口
在 blog-system-api 模块创建通知服务接口:
文件: blog-system-api/src/main/java/com/blog/system/api/service/INotificationService.java
package com.blog.system.api.service;
import java.util.Set;
/**
* 通知服务接口(供其他模块调用)
*
* @author liusxml
* @since 1.6.0
*/
public interface INotificationService {
/**
* 创建@提及通知
*
* @param commentId 评论ID
* @param userIds 被提及的用户ID集合
* @param mentionerId 提及人ID
*/
void createMentionNotifications(Long commentId, Set<Long> userIds, Long mentionerId);
/**
* 创建回复通知
*
* @param commentId 评论ID
* @param parentUserId 父评论作者ID
* @param replierId 回复人ID
*/
void createReplyNotification(Long commentId, Long parentUserId, Long replierId);
}
设计要点:
- 接口位于
*-api模块,定义契约 - 参数简单明确,避免跨模块传递复杂对象
- 方法命名清晰,表达业务意图
Step 2: 实现通知服务
在 blog-system-service 模块实现接口:
文件: blog-system-service/src/main/java/com/blog/system/service/NotificationService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class NotificationService implements INotificationService {
private final NotificationMapper notificationMapper;
@Override
public void createMentionNotifications(Long commentId, Set<Long> userIds, Long mentionerId) {
for (Object userIdObj : userIds) {
// 安全地将元素转换为 Long(处理 Jackson 反序列化可能导致的类型问题)
Long userId = convertToLong(userIdObj);
if (userId.equals(mentionerId)) {
continue; // 不给自己发通知
}
NotificationEntity notification = new NotificationEntity();
notification.setUserId(userId);
notification.setType(NotificationType.USER_MENTION);
notification.setTitle("您被@提及了");
notification.setContent("有人在评论中@了你");
notification.setSourceId(commentId);
notification.setSourceType("COMMENT");
notification.setIsRead(false);
notificationMapper.insert(notification);
}
}
/**
* 安全地将对象转换为 Long
* 处理 Jackson 反序列化可能导致的类型问题(Integer、String 等)
*/
private Long convertToLong(Object obj) {
if (obj instanceof Long) {
return (Long) obj;
} else if (obj instanceof Number) {
return ((Number) obj).longValue();
} else if (obj instanceof String) {
return Long.parseLong((String) obj);
} else {
throw new IllegalArgumentException("Cannot convert to Long: " + obj);
}
}
@Override
public void createReplyNotification(Long commentId, Long parentUserId, Long replierId) {
if (parentUserId.equals(replierId)) {
return; // 回复自己不通知
}
NotificationEntity notification = new NotificationEntity();
notification.setUserId(parentUserId);
notification.setType(NotificationType.COMMENT_REPLY);
notification.setTitle("您的评论收到了新回复");
notification.setSourceId(commentId);
notification.setSourceType("COMMENT");
notification.setIsRead(false);
notificationMapper.insert(notification);
log.info("创建回复通知: commentId={}, userId={}", commentId, parentUserId);
}
}
关键点:
- ✅ 实现了业务规则(自我通知过滤)
- ✅ 添加类型安全转换,处理 Jackson 反序列化问题
- ✅ 日志记录,便于调试
Step 3: 更新领域事件
修改 CommentRepliedEvent,添加 replierId 字段:
文件: blog-comment-service/src/main/java/com/blog/comment/domain/event/CommentRepliedEvent.java
@Getter
@AllArgsConstructor
public class CommentRepliedEvent {
private final Long commentId;
private final Long parentCommentId;
private final Long replierId; // 新增:回复人ID
}
Step 4: 集成通知服务到事件监听器
修改 CommentEventListener,注入通知服务并实现通知创建:
文件: blog-comment-service/src/main/java/com/blog/comment/domain/event/CommentEventListener.java
@Slf4j
@Component
@RequiredArgsConstructor
public class CommentEventListener {
private final INotificationService notificationService; // 注入服务接口
@Async
@EventListener
public void handleUserMentioned(UserMentionedEvent event) {
log.info("处理@提及事件: commentId={}, 提及用户数={}, mentionerId={}",
event.getCommentId(), event.getMentionedUserIds().size(), event.getMentionerId());
// 调用通知服务创建通知
notificationService.createMentionNotifications(
event.getCommentId(),
event.getMentionedUserIds(),
event.getMentionerId()
);
}
@Async
@EventListener
public void handleCommentReplied(CommentRepliedEvent event) {
log.info("处理评论回复事件: commentId={}, parentId={}, replierId={}",
event.getCommentId(), event.getParentCommentId(), event.getReplierId());
// 查询父评论作者
// ... (省略查询逻辑)
// 创建回复通知
notificationService.createReplyNotification(
event.getCommentId(),
parentUserId,
event.getReplierId()
);
}
}
设计亮点:
- ✅ 使用
@Async异步处理,提升性能 - ✅ 依赖接口而非实现,遵循依赖倒置原则
- ✅ 事件监听器只负责协调,业务逻辑在服务层
Step 5: 更新事件发布逻辑
修改 CommentServiceImpl.saveByDto() 方法:
文件: blog-comment-service/src/main/java/com/blog/comment/service/impl/CommentServiceImpl.java
@Override
public Object saveByDto(CommentDTO dto) {
// 提前解析 @mention,避免依赖 getById 可能的缓存问题
Set<Long> mentionedUserIds = mentionParser.parseMentions(dto.getContent());
Long currentUserId = SecurityUtils.getCurrentUserId();
// 调用父类保存
Long commentId = (Long) super.saveByDto(dto);
CommentEntity entity = getById(commentId);
// 根评论需要设置 path 和 rootId
if (dto.getParentId() == null && entity != null) {
entity.setPath("/" + commentId + "/");
entity.setRootId(commentId);
updateById(entity);
}
// 发布 @mention 事件
if (!mentionedUserIds.isEmpty()) {
applicationEventPublisher.publishEvent(
new UserMentionedEvent(commentId, mentionedUserIds, currentUserId));
log.info("发布@提及事件: commentId={}, mentionedUserIds={}, mentionerId={}",
commentId, mentionedUserIds, currentUserId);
}
// 发布回复事件
if (dto.getParentId() != null) {
applicationEventPublisher.publishEvent(
new CommentRepliedEvent(commentId, dto.getParentId(), currentUserId));
log.info("发布回复事件: commentId={}, parentId={}, replierId={}",
commentId, dto.getParentId(), currentUserId);
}
return commentId;
}
优化点:
- ✅ 提前解析:在
super.saveByDto()之前解析@mention - ✅ 直接使用:使用解析结果发布事件,避免依赖
getById()重新查询 - ✅ 一致性:使用
currentUserId作为事件参数
Step 6: 添加模块依赖
在 blog-comment-service/pom.xml 中添加依赖:
<dependency>
<groupId>com.blog</groupId>
<artifactId>blog-system-api</artifactId>
<version>${project.version}</version>
</dependency>
🔧 问题解决
问题 1: ClassCastException
现象:
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Long
at com.blog.system.service.NotificationService.createMentionNotifications
原因:
Jackson 在反序列化 JSON 数组时,将数字解析为 Integer 或 String,而不是 Long。
解决方案:
添加类型安全转换方法 convertToLong(),安全处理不同类型的转换:
private Long convertToLong(Object obj) {
if (obj instanceof Long) {
return (Long) obj;
} else if (obj instanceof Number) {
return ((Number) obj).longValue();
} else if (obj instanceof String) {
return Long.parseLong((String) obj);
} else {
throw new IllegalArgumentException("Cannot convert to Long: " + obj);
}
}
问题 2: 事件未触发
现象:
日志显示事件已发布,但通知未创建。
原因:
mentionedUserIds 从 entity.getMentionedUserIds() 获取时为空或null。
解决方案:
提前解析 @mention,直接使用解析结果发布事件,不依赖数据库查询:
Set<Long> mentionedUserIds = mentionParser.parseMentions(dto.getContent());
// ... save ...
if (!mentionedUserIds.isEmpty()) {
applicationEventPublisher.publishEvent(
new UserMentionedEvent(commentId, mentionedUserIds, currentUserId));
}
✅ 功能测试
测试步骤
- 注册测试用户:
curl -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"username": "alice",
"password": "Test123456",
"email": "alice@example.com",
"nickname": "Alice"
}'
- 登录获取 Token (testuser):
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "Test123456"}'
- 创建评论并 @alice:
curl -X POST http://localhost:8080/api/v1/comments \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"targetType": "ARTICLE",
"targetId": 1,
"content": "@alice Hello! 这是一条测试通知的评论"
}'
- 查询通知表:
SELECT id, user_id, type, title, source_id, is_read, create_time
FROM sys_notification
ORDER BY create_time DESC;
预期结果
id: 2001606552418619393
user_id: 2001606423930310658 (alice)
type: USER_MENTION
title: 您被@提及了
content: 有人在评论中@了你
source_id: 2001606552364093442 (评论ID)
is_read: 0
create_time: 2025-12-18 18:52:45
📊 架构优势
1. 松耦合
- Comment 模块只依赖 System API 接口
- 不依赖具体实现
- 便于微服务拆分
2. 异步处理
- 使用
@Async不阻塞主流程 - 提升用户体验
- 降低响应延迟
3. 事件驱动
- 业务解耦,易于扩展
- 新增监听器无需修改发送方
- 符合开闭原则
4. 类型安全
- 处理 JSON 反序列化类型问题
- 兼容多种数值类型
- 增强系统健壮性
💡 最佳实践
- 接口优先: 跨模块调用通过
*-api接口,避免直接依赖实现 - 事件解耦: 领域事件用于模块间通信,降低耦合
- 异步优化: 非核心流程使用异步处理,提升性能
- 类型安全: 处理外部数据时添加类型检查和转换
- 日志完善: 关键节点添加日志,便于排查问题
🎯 学习要点
- ✅ 理解事件驱动架构在模块化单体中的应用
- ✅ 掌握
@Async异步处理机制和线程池配置 - ✅ 学习跨模块通信的接口设计原则
- ✅ 了解 Jackson 反序列化的类型处理
- ✅ 体验从单体到微服务的平滑迁移路径
版本: 1.7.0
作者: liusxml
完成日期: 2025-12-18