如何优雅地使用Spring事件监听机制实现解耦——基于观察者模式(发布订阅模式)

39 阅读5分钟

接上个场景,用户签到后可以获取金币奖励

也可以看看我往期的文章

女朋友说没找到好用的画ER图工具,于是我们自己手搓了一个!🚀🚀🚀

作为我这种屎级开发者想都不用想 用户签到嘛,那肯定是签到之后,接着写签到的奖励呗。然后写到后面又想到可以给签到奖励在搞点东西,于是又直接着用户签到的那个地方继续写其他的奖励,在后面有新的奖励的时候还是继续往后面加,最后发现一个方法里面写了一大堆屎山

没错,这就是我们一般程序员的思维。如果新加的代码都很短的话还好,比较都是签到这个事件触发的事情嘛,肯定要直接写在这里啊,对吧。可是,如果生成待办事项的代码比较长,那我们一般会咋办?嗯,没错,我们就会觉得这段代码很臃肿,然后就会尝试去封装每个功能的代码,单独抽象出一个方法来,一个方法对应一个触发事件,然后先定义好入参和出参,确定自己要实现什么样的功能。这里就要注意传参的时候对对象的影响,不要影响下一个功能。

回到这个签到的场景这是我之前写的代码。把有关所有的奖励措施都写在了签到方法中。现在看起并不多,但是不利于后期进行扩展。

    /**
     * 用户签到操作
     *
     * @param userId 用户ID
     * @return BaseResponse<String> 返回签到成功或失败信息
     */
    @Transactional
    public BaseResponse<CurrencyUpdateForm> userSign(Long userId) {
        // ------------------1. 参数校验------------------

        // ------------------2. 业务处理------------------
        LocalDate now = LocalDate.now();
        int dayOfMonth = now.getDayOfMonth();
        String monthKey = RedisKey.SIGN_IN_KEY_PREFIX + userId + ":" + now.getYear() + ":" + now.getMonthValue();

        // todo 发布签到事件
        applicationEventPublisher.publishEvent(new UserSignEvent(userId));

        RBitSet bitSet = redissonClient.getBitSet(monthKey);

        // 检查今天是否已签到
        ThrowUtils.throwIf(bitSet.get(dayOfMonth), Code.PARAM_VERIFY_FAILED, UserConstant.SIGN_IN_TODAY);

        // 签到操作
        bitSet.set(dayOfMonth, true);

        // 获取当前连续签到天数
        int continuousDays = getMonthlyContinuousSignInDays(userId, bitSet);

        // 计算金币奖励逻辑
        int coins = 1;  // 签到成功获得1个金币

        if (continuousDays >= 7 && continuousDays < 30) {
            coins += 3;  // 连续签到7天额外获得3个金币
        } else if (continuousDays >= 30) {
            coins += 15; // 连续签到30天额外获得15个金币
        } else if (continuousDays > 1) {
            coins += 1;  // 连续签到超过1天额外获得1个金币
        }

        // 更新用户金币数量
        CurrencyUpdateForm currencyLogAddForm = new CurrencyUpdateForm();
        currencyLogAddForm.setUserId(userId);
        currencyLogAddForm.setCurrencyType(UserConstant.SIGN_IN_REWARD);
        currencyLogAddForm.setCurrency(coins);
        currencyLogAddForm.setCurrencyType("金币");
        currencyLogAddForm.setDescription(UserConstant.SIGN_IN_REWARD);
        currencyService.updateCurrency(currencyLogAddForm);
        // ------------------3. 返回结果------------------
        return ResultUtils.success(currencyLogAddForm);
    }

现在把有关签到的奖励都采用订阅者的模式进行抽取,首先我们创建一个事件:这里的source是我们发布者发布的信息

@Getter
public class UserSignEvent extends ApplicationEvent {


    public UserSignEvent(Object source) {
        super(source);
    }

    public UserSignEvent(Object source, Clock clock) {
        super(source, clock);
    }
}

然后是创建这个事件对应的多个观察者,即一对多的关系

@Slf4j
@Component
public class UserSignListener {

    @Resource
    private CurrencyService currencyService;
    @Resource
    private RedissonClient redissonClient;
    /**
     * 订阅用户签到事件,使用@Async注解实现异步处理
     * 1. 计算金币奖励
     * 2. 更新货币信息
     *
     * @param event 用户签到事件
     */
    @Async
    @EventListener(classes = UserSignEvent.class)
    public void doUserSign(UserSignEvent event) {
        Long userId = (Long) event.getSource();
        log.info("用户签到事件,用户ID:{}", userId);

        LocalDate now = LocalDate.now();
        String monthKey = RedisKey.SIGN_IN_KEY_PREFIX + userId + ":" + now.getYear() + ":" + now.getMonthValue();

        // 获取签到的连续天数,可以将此方法提取到公共服务中
        RBitSet bitSet = redissonClient.getBitSet(monthKey);
        int continuousDays = getMonthlyContinuousSignInDays(userId, bitSet);

        // 计算金币奖励
        int coins = calculateCoins(continuousDays);

        // 更新用户金币数量
        CurrencyUpdateForm currencyLogAddForm = new CurrencyUpdateForm();
        currencyLogAddForm.setUserId(userId);
        currencyLogAddForm.setCurrencyType(UserConstant.SIGN_IN_REWARD);
        currencyLogAddForm.setCurrency(coins);
        currencyLogAddForm.setCurrencyType("金币");
        currencyLogAddForm.setDescription(UserConstant.SIGN_IN_REWARD);

        // 更新货币信息
        currencyService.updateCurrency(currencyLogAddForm);
    }

    /**
     * 计算金币奖励的逻辑
     * @param continuousDays 连续签到天数
     * @return 奖励的金币数
     */
    private  int calculateCoins(int continuousDays) {
        // 签到成功获得1个金币
        int coins = 1;

        if (continuousDays >= 7 && continuousDays < 30) {
            // 连续签到7天额外获得3个金币
            coins += 3;
        } else if (continuousDays >= 30) {
            // 连续签到30天额外获得15个金币
            coins += 15;
        } else if (continuousDays > 1) {
            // 连续签到超过1天额外获得1个金币
            coins += 1;
        }

        return coins;
    }

    /**
     * 获取本月的连续签到天数
     *
     * @param userId 用户ID
     * @param bitSet 位图签到数据
     * @return 连续签到天数
     */
    private int getMonthlyContinuousSignInDays(Long userId, RBitSet bitSet) {
        LocalDate now = LocalDate.now();
        int dayOfMonth = now.getDayOfMonth();

        // 获取本月的签到记录,从位图中读取
        long signInData = 0;
        for (int i = 1; i <= dayOfMonth; i++) {
            if (bitSet.get(i)) {
                signInData |= (1L << (dayOfMonth - i)); // 逐天记录签到
            }
        }

        // 记录连续签到天数
        int continuousDays = 0;

        // 逐位检查
        while (signInData > 0) {
            if ((signInData & 1) == 0) {
                break; // 如果当前位为0,表示未签到,结束
            }
            continuousDays++;
            signInData >>= 1;
        }

        return continuousDays;
    }
}

最后是抽取签到服务逻辑

    /**
     * 用户签到操作
     *
     * @param userId 用户ID
     * @return BaseResponse<String> 返回签到成功或失败信息
     */
    @Transactional
    public BaseResponse<String> userSign(Long userId) {
        // ------------------1. 参数校验------------------

        // ------------------2. 业务处理------------------
        LocalDate now = LocalDate.now();
        int dayOfMonth = now.getDayOfMonth();
        String monthKey = RedisKey.SIGN_IN_KEY_PREFIX + userId + ":" + now.getYear() + ":" + now.getMonthValue();

        RBitSet bitSet = redissonClient.getBitSet(monthKey);

        // 检查今天是否已签到
        ThrowUtils.throwIf(bitSet.get(dayOfMonth), Code.PARAM_VERIFY_FAILED, UserConstant.SIGN_IN_TODAY);

        // 签到操作
        bitSet.set(dayOfMonth, true);
        applicationEventPublisher.publishEvent(new UserSignEvent(userId));

        // ------------------3. 返回结果------------------
        return ResultUtils.success(UserConstant.SIGN_IN_SUCCESS);
    }

我们可以看到这样其实就是spring给我们封装好的一个观察者模式的框架,我们就不用自己去手动搭建外部框架了。 我们这里是异步通知观察者的模式。这样的话签到逻辑这里面只有签到有关的逻辑了。当签到成功后,观察者会订阅到发布者的信息,所以就进行异步处理签到奖励的逻辑。后期如果我们需要进行新增有关奖励的逻辑只需要在增加一个监听者就行了。只增加不修改符合开闭原则(OCP)

交流学习

最后,如果这篇文章对你有所启发,请帮忙转发给更多的朋友,让更多人受益!如果你有任何疑问或想法,欢迎随时留言与我讨论,我们一起学习、共同进步。别忘了关注我,我将持续分享更多有趣且实用的技术文章,期待与你的交流!