我好像是个老古董

Java3y

共 7436字,需浏览 15分钟

 · 2023-02-13

大家好,我是3y,今天继续来聊我的开源项目austin啊,但实际内容更新不多。这文章主是想吹下水,主要聊聊我在更新项目中学到的小技巧

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

项目更新:access_token存储到Redis

(要是对项目不感兴趣的,也可以跳过,这里主要是想说明我更新了austin的内容,同时引出今天的内容之一Map.computeIfAbsent()的使用)

在接入微信相关渠道时,我就说过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的方法能不能用先

很快啊,我就找到了。

acb2b738ac8a8ec17f1e7480f394d0dd.webp

我首先看的是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

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 数组 相关实现

b664a3f8dfcb10e990750be2435005e6.webp

最后

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

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

不过写开源项目有一大好处是,只要我的项目有人用,能大大提高我获取“优雅”写法的概率,这也是我一直推广自己项目的一个原因之一

推荐项目

如果想学Java项目的,我还是 强烈推荐 我的开源项目消息推送平台Austin,可以用作 毕业设计 ,可以用作 校招 ,可以看看 生产环境是怎么推送消息 的。

仓库地址(可点击阅读原文跳转):https://gitee.com/zhongfucheng/austin

我开通了 股东服务 内容,感兴趣可以点击下方看看,主要针对的是项目哟

VIP服务

浏览 53
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报