乐趣区

关于后端:原来Spring能注入集合和Map的computeIfAbsent是这么好用

大家好,我是 3y,明天持续来聊我的开源我的项目 austin 啊,但理论内容更新不多。这文章主是想吹上水,次要聊聊我在更新我的项目中学到的 小技巧

明天所说的小技巧可能有很多人都会,但必定也会有跟我一样之前没用过的。

音讯推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等音讯类型

  • https://gitee.com/zhongfucheng/austin/
  • https://github.com/ZhongFuCheng3y/austin

Spring 注入汇合

之前我始终不晓得,原来 Spring 是能注入汇合的,直到一个 pull request 被提了过去。

https://gitee.com/zhongfucheng/austin/pulls/31

我之前写了一个 自定义注解 ,它的作用就是收集自定义注解所标识的Bean,而后最初把这些Bean 放到 Map

@Component
public class SmsScriptHolder {private Map<String, SmsScript> handlers = new HashMap<>(8);

    public void putHandler(String scriptName, SmsScript handler) {handlers.put(scriptName, handler);
    }
    public SmsScript route(String scriptName) {return handlers.get(scriptName);
    }
}


/**
 * 标识 短信渠道
 *
 * @author 3y
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Component
public @interface SmsScriptHandler {

    /**
     * 这里输出脚本名
     *
     * @return
     */
    String value();}

/**
 * sms 发送脚本的抽象类
 *
 * @author 3y
 */
@Slf4j
public abstract class BaseSmsScript implements SmsScript {

    @Autowired
    private SmsScriptHolder smsScriptHolder;

    @PostConstruct
    public void registerProcessScript() {if (ArrayUtils.isEmpty(this.getClass().getAnnotations())) {log.error("BaseSmsScript can not find annotation!");
            return;
        }
        Annotation handlerAnnotations = null;
        for (Annotation annotation : this.getClass().getAnnotations()) {if (annotation instanceof SmsScriptHandler) {
                handlerAnnotations = annotation;
                break;
            }
        }
        if (handlerAnnotations == null) {log.error("handler annotations not declared");
            return;
        }
        // 注册 handler
        smsScriptHolder.putHandler(((SmsScriptHandler) handlerAnnotations).value(), this);
    }
}

后果,pull request提的代码过去特地简略就代替了我的代码了。只有在应用的时候,间接注入Map

@Autowired
private Map<String, SmsScript> smsScripts;

这一行代码就可能实现,把 SmsScript 的实现类都注入到这个 Map 里。同样的,咱们亦能够应用List<Interface> 把该接口下的实现类都注入到这个 List 里。

这好奇让我去看看 Spring 到底是怎么实现的,但实际上并不难。入口在org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject

接着定位到:org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency

深刻 org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency

最初实现注入的地位:org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveMultipleBeans 数组 相干实现

access_token 存储到 Redis

在接入微信相干渠道时,我就说过 austin 借助了 wxjava 这个开源组件库(该组件库对接微信相干api,使调用变得尤其简略)。

比方,咱们调用微信的 api 是须要 access_token 的参数的。如果是咱们 本人编写 代码调用微信 api,那咱们须要先获取access_token,而后把该access_token 拼接在 url 上。此时,咱们又须要思考 access_token 会不会生效了,生效了咱们要有 重试的策略

wxjava 把这些都封装好了,屏蔽了外部实现细节。只有咱们把微信渠道的账号信息写到 WxMpConfigStorage 里,那该组件就会帮咱们去拿到access_token,外部也会有相应的重试策略。

第一版我为了图不便,我是应用 WxMpDefaultConfigImpl 实现类把渠道相干信息存储在 本地内存 里(包含 access_token),而 在上周 我把渠道相干信息转都存储至Redis

次要是获取 access_token 它的 调用次数是无限 的,如果我的项目集群部署,而 access_token 又存储在本地内存中,那就很大概率不到一天工夫调用获取 access_token 次数就满了,要是拿不到access_token,那就没方法调用微信的接口了。

https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html

对于 wxjava 这个组件库,调用微信的 api 都是通过 Wx(xx)Service 来应用的,而我是想把 Wx(xx)Service 做成是 单例 的。那在实现 access_token 存储到 Redis 的时候,我就很天然就要对旧代码进行一波重构(因为第一版写进去的代码,多多少少都有点不称心)。

历史背景:

1、WxServiceUtils的逻辑是 我的项目启动 的时检索数据库里所有的 微信渠道账号 信息,将 Wx(xx)Service 写入到 Map 里。Wx(xx)Service要做成单例天然就会想到用 Map 存储(因为音讯推送平台很可能会对接很多个服务号或者小程序,这里数据结构必定优先是 Map 啦)

如果渠道的账号 通过后盾 有存在变更行为,那程序外部会执行 refresh() 刷新。但这个仅仅是在程序内能监听到的变更,如果是间接通过 SQL 批改表的记录,目前是没有机制刷新 Map 的内容的。

2、AccountUtils的逻辑是 程序运行时 失去发送账号的 Id,通过Id 去数据库检索账号配置,实时返回账号最新的内容。(除了微信渠道账号,其余所有的渠道账号都是在这里获取信息)

更新:把原有治理微信账号信息的 WxServiceUtils 类给弃用了,将所有的发送渠道账号信息都归到 AccountUtils 进行治理。

Map.computeIfAbsent 应用

在重构下面所讲的逻辑时,我很快地写出以下的代码:

if (clazz.equals(WxMaService.class)) {if (Objects.nonNull(miniProgramServiceMap.get(channelAccount))) {return (T)miniProgramServiceMap.get(channelAccount);
    }
    WxMaService wxMaService = initMiniProgramService(JSON.parseObject(channelAccount.getAccountConfig(), WeChatMiniProgramAccount.class));
    miniProgramServiceMap.put(channelAccount, wxMaService);
    return (T) wxMaService;
} else if (clazz.equals(WxMpService.class)) {if (Objects.nonNull(officialAccountServiceMap.get(channelAccount))) {return (T)officialAccountServiceMap.get(channelAccount);
    }
    WxMpService wxMpService = initOfficialAccountService(JSON.parseObject(channelAccount.getAccountConfig(), WeChatOfficialAccount.class));
    officialAccountServiceMap.put(channelAccount, wxMpService);
    return (T) wxMpService;
}

等我写完,而后简略做了下自测,发现这代码咋这么丑啊 ,两个if 的逻辑实际上是一样的。

我想,这肯定会有什么工具类能帮我去优化下这个代码的,我正筹备翻 Hutool/Guava 这种工具包时,我忽然想起:JDK 在 1.8 如同就提供了 putIfXXX 的办法啦,我还找个毛啊,间接看看 JDK 的办法能不能用先

很快啊,我就找到了。

我首先看的是 putIfAbsent,发现它实现 很简略,就是做了一层封装。

default V putIfAbsent(K key, V value) {V v = get(key);
    if (v == null) {v = put(key, value);
    }

    return v;
}

但却很适宜 用来优化我下面的代码。于是,很快啊,我就改成了这样:

if (clazz.equals(WxMaService.class)) {return (T) miniProgramServiceMap.putIfAbsent(channelAccount, initMiniProgramService(JSON.parseObject(channelAccount.getAccountConfig(), WeChatMiniProgramAccount.class)));
} else if (clazz.equals(WxMpService.class)) {return (T) officialAccountServiceMap.putIfAbsent(channelAccount, initOfficialAccountService(JSON.parseObject(channelAccount.getAccountConfig(), WeChatOfficialAccount.class)));
}

这看着真简洁啊,如同曾经很完满了,原本有好几行的代码,优化了下变成了一行。

但我又思考了下,这个 putIfAbsentV我这边传入的是一个办法,每次这个办法都会执行的(不管我的 Map 里有没有这个 K), 这又感觉不太优雅了

我又点进去 computeIfAbsent 看了下,嗯!这就是我想要的了:如果 MapV不存在时,才去执行我生成 V 的逻辑

default V computeIfAbsent(K key,
                          Function<? super K, ? extends V> mappingFunction) {Objects.requireNonNull(mappingFunction);
    V v;
    if ((v = get(key)) == null) {
        V newValue;
        if ((newValue = mappingFunction.apply(key)) != null) {put(key, newValue);
            return newValue;
        }
    }
    return v;
}

(这个其实我在学 lambdastream流的时候已经是体验过的,我日常也会简略写点,只是不晓得在 JDKMap也有这样的办法。)于是,最初的代码就成了:

if (clazz.equals(WxMaService.class)) {return (T) miniProgramServiceMap.computeIfAbsent(channelAccount, account -> initMiniProgramService(JSON.parseObject(account.getAccountConfig(), WeChatMiniProgramAccount.class)));
} else if (clazz.equals(WxMpService.class)) {return (T) officialAccountServiceMap.computeIfAbsent(channelAccount, account -> initOfficialAccountService(JSON.parseObject(account.getAccountConfig(), WeChatOfficialAccount.class)));
}

又起初,等我公布到 Git 仓库后,有人提了 pull request 来修复 ConcurrentHashMapcomputeIfAbsent存在性能的问题。呀,不小心又学到了点货色。

https://bugs.openjdk.java.net/browse/JDK-8161372

微信扫码登录实现

我在生产环境下是没有写过「用户登录」的,导致有些业务性能我也不晓得线上是怎么实现的。而「用户登录注册」这个性能之前会听过和见识过一些技术栈「Shiro」、「JWT」、「Spring Security」、「CAS」、「OAuth2.0」等等。

然而,我的需要只是用来做简略的校验,不须要那么简单。如果就给我设计一张 user 表,对其简略的增删改查如同也满足,但我又不想写这样的代码,因为我在大学的时候实现过相似的。

当初不都 风行扫码登录 嘛?我不是曾经接入了微信服务号的模板音讯了吗,不正好有一个测试号给我去做吗?于是就开干了。

首先看看人家是怎么写的,于是被我找到了一篇博客:https://blog.51cto.com/cxhit/4924932

过程挺好懂的,就按着他给出的时序图对着实现就完了。后端对我来说实现并不难,花的工夫最长的还是在前端的交互上。毕竟我这过后选用的是低代码平台啊,不能轻易实现各种逻辑的啊。

在前端,就一个「轮询」性能,要轮询查看用户是否曾经订阅登录,就消耗了我很多工夫在官网文档上。起初,写了不少的奇淫技巧,最初也就被我实现进去了。实现过程很蹩脚,也不值一提,反正你们也不会从中学到什么好货色,因为我也没有。

过程还是简略复述下吧,前期可能也会有同学去实现这个性能。

1、首先咱们要有一个接口,给到微信 回调 ,所以咱们个别会称该接口为回调接口。微信的一些重要的事件都会回调给咱们,咱们做响应的逻辑解决。就比方, 用户关注了服务号,这种音讯微信就调用咱们的接口。

2、在微信后盾 配置 咱们的定义好的回调接口,给到微信进行回调。

(如果接口是通的,按失常的走,那就会配置胜利)

3、编写一个获取 微信带参数的二维码 给到前端做展现。

4、前端拿到二维码做展现,并且失去随机生成的参数 轮询 查看是否已登录。

5、编写 查看是否已登录的接口给到前端进行判断。(如果能从 Redis 里拿到随机参数,阐明曾经登录了)

6、当用户扫码 关注了 服务号,则失去微信的回调。当用户关注服务号时,会把随机参数和 openId 传给服务器,我则将信息存入 Redis。

7、前端得悉已登录后,将用户信息写入 localStorage

最初

每次代码存在遇到“优雅”的写法时,我都会烦恼本人怎么不会,还吭哧吭哧地写这破代码这么多年了。特地是 Map.computeIfAbsent 这个,我感觉没理由我不晓得呀。我从初学到当初工作次要用JDK 1.8,没道理我当初才晓得写这个玩意。

有的时候都感觉我是不是曾经是老古董了,新世界曾经没有承载我的船了。

不过写开源我的项目有一大益处是,只有我的我的项目有人用,能大大提高我获取“优雅”写法的概率,这也是我始终推广本人我的项目的一个起因之一。

如果想学 Java 我的项目的,强烈推荐 我的开源我的项目 音讯推送平台 Austin(8K stars),能够用作 毕业设计 ,能够用作 校招 ,能够看看 生产环境是怎么推送音讯 的。开源我的项目音讯推送平台 austin 仓库地址:

音讯推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等音讯类型

  • https://gitee.com/zhongfucheng/austin/
  • https://github.com/ZhongFuCheng3y/austin
退出移动版