Spring Boot 事件处理机制,实现代码解耦

背景

大家好,我是顾,最近 面试鸭 需要开发一个经验值体系的能力,需要对经验值进行增加、减少,有很多的触发情况,比如用户发布题解,用户浏览题目,用户邀请新用户、用户转化为会员、签到等等,为了复用经验系统的代码,以及和业务代码解耦,决定使用Spring Boot 的事件处理机制来处理各种经验值增减的逻辑。

介绍

在 Spring Boot 中,事件监听是一种 Spring 的机制,允许开发者定义和触发自定义事件,并通过注册监听器来响应这些事件。这种机制为应用程序中的事件处理提供了更高的解耦性,使代码更加清晰和灵活。Spring的事件监听机制是在JDK事件监听的基础上进行的扩展,也是使用了观察者模式并在其基础上进行进一步抽象和改进。

流程

创建自定义的事件类

把经验体系的属性封装在自定义的事件类中,通过实现 ApplicationEvent 表示这是经验值事件类

java
复制代码
/** * 经验值事件类 * * @author gulihua * @date 2024-11-25 14:27 */ @Getter public class ExperienceEvent extends ApplicationEvent { /** * 经验类型枚举 */ private UserExperienceEnum userExperience; /** * 用户ID */ private Long userId; /** * 业务ID(操作的对象ID) */ private Long bizId; /** * 描述 */ private String desc; /** * 参数 */ private UserExperienceRecordParamsDTO param; /** * 登录用户 */ private User loginUser; /** * 自定义原因 */ private UserExperienceRecordReasonEnum reason; /** * 是否是取消经验值事件 */ private boolean isCancel; public ExperienceEvent(Object source, UserExperienceEnum userExperience, Long userId, Long bizId, String desc, UserExperienceRecordParamsDTO param, User loginUser, boolean isCancel) { super(source); this.userExperience = userExperience; this.userId = userId; this.bizId = bizId; this.desc = desc; this.param = param; this.loginUser = loginUser; this.isCancel = isCancel; } public ExperienceEvent(Object source, UserExperienceEnum userExperience, Long userId, Long bizId, String desc, UserExperienceRecordParamsDTO param, User loginUser, UserExperienceRecordReasonEnum reason, boolean isCancel) { super(source); this.userExperience = userExperience; this.userId = userId; this.bizId = bizId; this.desc = desc; this.param = param; this.loginUser = loginUser; this.reason = reason; this.isCancel = isCancel; } }

创建事件监听器

创建一个自定义的事件监听器,用来处理经验增加、扣除事件。如果是取消事件,需要把经验值改成负的。

java
复制代码
/** * 经验值事件监听器 * * @author gulihua * @date 2024-11-25 14:28 */ @Slf4j @Service public class ExperienceEventListener implements ApplicationListener<ExperienceEvent> { @Resource private UserExperienceRecordService userExperienceRecordService; @Resource private RedissonClient redissonClient; @Override public void onApplicationEvent(ExperienceEvent experienceEvent) { UserExperienceRecord userExperienceRecord = new UserExperienceRecord(); userExperienceRecord.setExperience(experienceEvent.getUserExperience().getValue()); userExperienceRecord.setDescription(experienceEvent.getDesc()); userExperienceRecord.setReason(experienceEvent.getUserExperience().getAddReason()); userExperienceRecord.setUserId(experienceEvent.getUserId()); userExperienceRecord.setBizId(experienceEvent.getBizId()); userExperienceRecord.setExperienceType(experienceEvent.getUserExperience().getType()); String params = JSONUtil.toJsonStr(experienceEvent.getParam()); userExperienceRecord.setParams(params); // 取消操作 if (experienceEvent.isCancel()) { userExperienceRecord.setExperience(userExperienceRecord.getExperience() * -1); userExperienceRecord.setReason(experienceEvent.getUserExperience().getCancelReason()); } // 自定义原因 if (null != experienceEvent.getReason()) { userExperienceRecord.setReason(experienceEvent.getReason().getValue()); } userExperienceRecordService.addUserExperienceRecord(userExperienceRecord, experienceEvent.getLoginUser()); } }

经验值记录具体数据库的操作,包括添加记录、添加经验值数据

java
复制代码
@Override public int addUserExperienceRecord(UserExperienceRecord userExperienceRecord, User loginUser) { // 参数校验:用户要存在,经验值要合理 Integer experience = userExperienceRecord.getExperience(); Integer reason = userExperienceRecord.getReason(); Long userId = userExperienceRecord.getUserId(); log.info("userExperienceRecord params = {}", userExperienceRecord); if (experience > MAX_ADD_EXPERIENCE || experience == 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } if (UserExperienceRecordReasonEnum.getEnumByValue(reason) == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } if (userId <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User user = userService.getById(userId); if (user == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 往用户经验值记录表插入记录 int insertRows = this.baseMapper.insert(userExperienceRecord); if (insertRows <= 0) { throw new BusinessException(ErrorCode.SYSTEM_ERROR, "数据插入失败"); } // 修改用户表的经验值 UpdateWrapper<User> updateWrapper = new UpdateWrapper<>(); updateWrapper.eq("id", userId); if (experience > 0) { updateWrapper.setSql(String.format("experience = experience + %d", experience)); } else { updateWrapper.setSql(String.format("experience = experience - %d", Math.abs(experience))); } boolean updateResult = userService.update(updateWrapper); if (!updateResult) { throw new BusinessException(ErrorCode.SYSTEM_ERROR, "数据更新失败"); } return experience; }

因为有些经验值一天是有获取上限的,不然用户可能会重复刷,所以,我们需要对有些经验值的类型去做判断,比如刷题,用户每天获取的上限是1个积分,发布题解,上限是6个积分,所以就需要在获取积分时去做判断今天是否达到了上限值。

java
复制代码
/** * 核实是否要增加记录 * * @return 是否执行后续的操作 */ private boolean check(ExperienceEvent experienceEvent) { // 发布 if (!experienceEvent.isCancel()) { // 只能执行一次 if (ExperienceLimitTypeEnum.ONLY_ONE.equals(experienceEvent.getUserExperience().getLimitType())) { // 判断之前有没对目标做过操作 QueryWrapper<UserExperienceRecord> oldRecord = new QueryWrapper<>(); oldRecord.eq("userId", experienceEvent.getUserId()); oldRecord.eq("bizId", experienceEvent.getBizId()); oldRecord.eq("experienceType", experienceEvent.getUserExperience().getType()); if (userExperienceRecordService.count(oldRecord) > 0) { return false; } } else if (ExperienceLimitTypeEnum.DAY.equals(experienceEvent.getUserExperience().getLimitType())) { // 每天经验上限 Date now = new Date(); Date startDate = DateUtil.beginOfDay(now); Date endDate = DateUtil.endOfDay(now); Long sum = userExperienceRecordService.sumUserTodayExperienceRecord(startDate, endDate, experienceEvent.getUserExperience().getType(), experienceEvent.getUserId()); if (sum != null && sum >= experienceEvent.getUserExperience().getDayMaxExperience()) { return false; } } } else { Long sum = userExperienceRecordService.sumUserExperienceRecord(experienceEvent.getBizId(), experienceEvent.getUserExperience().getType(), experienceEvent.getUserId()); if (sum == null || sum <= 0) { return false; } } return true; }

创建事件发布者

这个类主要用来发布具体的事件,包括发布增加经验值事件,扣除经验值事件

java
复制代码
/** * 经验值事件发布者 * * @author gulihua * @date 2024-11-25 14:54 */ @Slf4j @Component @AllArgsConstructor public class ExperienceEventPublisher { private ApplicationEventPublisher publisher; /** * 发布增加经验值事件 * * @param userExperience 经验值枚举 * @param userId 增加经验值的用户ID * @param bizId 业务ID * @param desc 描述 * @param param 参数 * @param loginUser 登录用户 **/ public void publishAddEvent(UserExperienceEnum userExperience, Long userId, Long bizId, String desc, UserExperienceRecordParamsDTO param, User loginUser) { ExperienceEvent customEvent = new ExperienceEvent(this, userExperience, userId, bizId, desc, param, loginUser, false); publisher.publishEvent(customEvent); } /** * 发布扣除经验值事件 * * @param userExperience 经验值枚举 * @param userId 扣除经验值的用户ID * @param bizId 业务ID * @param desc 描述 * @param param 参数 * @param loginUser 登录用户 **/ public void publishCancelEvent(UserExperienceEnum userExperience, Long userId, Long bizId, String desc, UserExperienceRecordParamsDTO param, User loginUser) { ExperienceEvent customEvent = new ExperienceEvent(this, userExperience, userId, bizId, desc, param, loginUser, true); publisher.publishEvent(customEvent); }

经验值类型枚举

在经验值事件处理中,有一个很重要的字段,那就是经验类型的枚举,很多的经验事件的处理都是围绕这个枚举值来判断的,与其说是枚举类,不如说是经验类型的配置类。里面包含了经验值类型,经验值原因,经验值的大小,每天经验值的获取上限等等。

java
复制代码
/** * 用户经验值枚举 * * @author gulihua */ public enum UserExperienceEnum { /** * 题解被设置为推荐答案 */ BEST_ANSWER(2, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getBestAnswer(), UserExperienceRecordReasonEnum.BEST_ANSWER.getValue(), UserExperienceRecordReasonEnum.CANCEL_BEST_ANSWER.getValue(), ExperienceLimitTypeEnum.NOT_LIMIT ), /** * 题解被精选 */ GOOD_ANSWER(3, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getGoodAnswer(), UserExperienceRecordReasonEnum.GOOD_ANSWER.getValue(), UserExperienceRecordReasonEnum.CANCEL_GOOD_ANSWER.getValue(), ExperienceLimitTypeEnum.NOT_LIMIT ), /** * 发布一篇回答讨论 */ ADD_ANSWER(4, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getAddAnswer(), UserExperienceRecordReasonEnum.ADD_ANSWER.getValue(), UserExperienceRecordReasonEnum.CANCEL_ADD_ANSWER.getValue(), ExperienceLimitTypeEnum.DAY, 6, false ), /** * 题解被置顶 */ TOP_ANSWER(8, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getTopAnswer(), UserExperienceRecordReasonEnum.TOP_ANSWER.getValue(), UserExperienceRecordReasonEnum.CANCEL_TOP_ANSWER.getValue(), ExperienceLimitTypeEnum.NOT_LIMIT ), /** * 邀请新用户注册 */ INVITE_USER(5, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getInviteUser(), UserExperienceRecordReasonEnum.INVITE_USER.getValue(), UserExperienceRecordReasonEnum.CANCEL_INVITE_USER.getValue(), ExperienceLimitTypeEnum.NOT_LIMIT ), /** * 邀请用户成为会员 */ INVITE_USER_VIP(6, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getInviteUserVip(), UserExperienceRecordReasonEnum.INVITE_USER_VIP.getValue(), UserExperienceRecordReasonEnum.CANCEL_INVITE_USER_VIP.getValue(), ExperienceLimitTypeEnum.NOT_LIMIT ), /** * 签到 */ SIAN_IN(15, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getSignin(), UserExperienceRecordReasonEnum.SIGN_IN.getValue(), UserExperienceRecordReasonEnum.CANCEL_SYSTEM.getValue(), ExperienceLimitTypeEnum.DAY, true, 1 ), // ... 其他类型的枚举 ; /** * 经验类型 */ private int type; /** * 经验值 */ private int value; /** * 添加经验值原因 */ private int addReason; /** * 扣除经验值原因 */ private int cancelReason; /** * 经验值获取上限类型 */ private ExperienceLimitTypeEnum limitType; /** * 是否限制操作次数(优先级比经验值上限高) */ private boolean isLimitOperation; /** * 限制操作的次数 */ private int operationLimit; /** * 每天经验值上限 */ private int dayMaxExperience; public int getOperationLimit() { return this.operationLimit; } public ExperienceLimitTypeEnum getLimitType() { return this.limitType; } public int getValue() { return this.value; } public int getAddReason() { return this.addReason; } public int getCancelReason() { return this.cancelReason; } public boolean getIsLimitOperation() { return this.isLimitOperation; } public int getType() { return this.type; } public int getDayMaxExperience() { return this.dayMaxExperience; } UserExperienceEnum(int type, int value, int addReason, int cancelReason, ExperienceLimitTypeEnum limitType) { this.type = type; this.value = value; this.addReason = addReason; this.cancelReason = cancelReason; this.limitType = limitType; } UserExperienceEnum(int type, int value, int addReason, int cancelReason, ExperienceLimitTypeEnum limitType, int dayMaxExperience, boolean isLimitOperation) { this.type = type; this.value = value; this.addReason = addReason; this.cancelReason = cancelReason; this.dayMaxExperience = dayMaxExperience; this.limitType = limitType; this.isLimitOperation = isLimitOperation; } UserExperienceEnum(int type, int value, int addReason, int cancelReason, ExperienceLimitTypeEnum limitType, boolean isLimitOperation, int operationLimit) { this.type = type; this.value = value; this.addReason = addReason; this.cancelReason = cancelReason; this.limitType = limitType; this.isLimitOperation = isLimitOperation; this.operationLimit = operationLimit; }

调用

比如在发布题解时,需要增加经验值,直接调用发布事件的方法即可,指定类型枚举

java
复制代码
/** * 每日首次发布题解,增加经验值 * * @param questionAnswer 答案 * @param loginUser 登录用户 **/ private void addAnswerExperience(QuestionAnswer questionAnswer, User loginUser) { String url = String.format("/question/%s?comment=%s", questionAnswer.getQuestionId(), questionAnswer.getId()); String descriptionTemplate = "发布一篇回答讨论,<a href=\"%s\" target=\"_blank\">查看该讨论</a>"; String description = String.format(descriptionTemplate, url); UserExperienceRecordParamsDTO paramsDTO = new UserExperienceRecordParamsDTO(); paramsDTO.setQuestionId(questionAnswer.getQuestionId()); paramsDTO.setAnswerId(questionAnswer.getId()); experienceEventPublisher.publishAddEvent( UserExperienceEnum.ADD_ANSWER, questionAnswer.getUserId(), questionAnswer.getId(), description, paramsDTO, loginUser ); }

在需要扣除经验值的时候,比如题解被删除时,调用扣除经验值的事件,可以指定自定义的原因

java
复制代码
/** * 题解被删除,扣除经验值 * * @param oldQuestionAnswer 答案 * @param loginUser 登录用户 **/ private void deleteAnswerCancelExperience(QuestionAnswer oldQuestionAnswer, User loginUser) { String url = String.format("/question/%s", oldQuestionAnswer.getQuestionId()); UserExperienceRecordParamsDTO paramsDTO = new UserExperienceRecordParamsDTO(); paramsDTO.setQuestionId(oldQuestionAnswer.getQuestionId()); paramsDTO.setAnswerId(oldQuestionAnswer.getId()); UserExperienceEnum userAddAnswerExperienceEnum = UserExperienceEnum.ADD_ANSWER; String descriptionTemplate = "回答讨论被删除,扣除经验值,<a href=\"%s\" target=\"_blank\">查看题目</a>"; String description = String.format(descriptionTemplate, url); experienceEventPublisher.publishCancelEvent( userAddAnswerExperienceEnum, oldQuestionAnswer.getUserId(), oldQuestionAnswer.getId(), description, paramsDTO, UserExperienceRecordReasonEnum.DELETE_ANSWER, loginUser ); }

异步处理

经验值的增加减少并不是用户操作时立马关心的,如果经验值处理的时间过长也会影响请求的响应时间,会阻塞主线程,所以可以把触发事件处理成异步的操作,首先可以创建一个异步的线程池配置,专门用来处理经验值的相关事件,使用@EnableAsync来开启异步支持,它会由Spring创建一个代理,在单独的线程中去执行任务。

java
复制代码
@Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { // 经验值线程池 @Bean("experienceThreadPool") public TaskExecutor experienceThreadPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(10); executor.setKeepAliveSeconds(100); executor.setQueueCapacity(1000); executor.setThreadNamePrefix("experiencePool-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); executor.initialize(); return executor; } }

然后再需要异步处理的方法上加上@Async("experienceThreadPool")注解,这样这个方法会在刚刚自定义的线程池中去执行。

java
复制代码
@Override @Async("experienceThreadPool") public void onApplicationEvent(ExperienceEvent experienceEvent) { UserExperienceRecord userExperienceRecord = new UserExperienceRecord(); userExperienceRecord.setExperience(experienceEvent.getUserExperience().getValue()); userExperienceRecord.setDescription(experienceEvent.getDesc()); userExperienceRecord.setReason(experienceEvent.getUserExperience().getAddReason()); userExperienceRecord.setUserId(experienceEvent.getUserId()); userExperienceRecord.setBizId(experienceEvent.getBizId()); userExperienceRecord.setExperienceType(experienceEvent.getUserExperience().getType()); String params = JSONUtil.toJsonStr(experienceEvent.getParam()); userExperienceRecord.setParams(params); // 取消操作 if (experienceEvent.isCancel()) { userExperienceRecord.setExperience(userExperienceRecord.getExperience() * -1); userExperienceRecord.setReason(experienceEvent.getUserExperience().getCancelReason()); } // 自定义原因 if (null != experienceEvent.getReason()) { userExperienceRecord.setReason(experienceEvent.getReason().getValue()); } userExperienceRecordService.addUserExperienceRecord(userExperienceRecord, experienceEvent.getLoginUser()); }

总结

使用 Spring 的异步事件处理将一些耗时的操作从主线程中分离出来,避免阻塞主线程,从而提高系统的吞吐量。对于不需要立即完成的任务,比如说经验值的处理,操作日志的记录等,可以进行异步处理,从而提高用户体验。它最重要的功能就是解耦,Spring Event 允许模块之间通过事件通信,减少模块之间的直接依赖,提高代码的可维护性和可扩展性,发布者只需要发布事件即可,并不需要关心具体的业务实现。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
我是顾
下载 APP