大家好,我是3y,今天继续来聊我的开源项目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仓库地址:

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

原来Spring能注入集合和Map的computeIfAbsent是这么好用!的更多相关文章

  1. Java集合的实现细节—Set集合和Map集合

    Set:代表无序.不可重复的集合 Map:代表key-value对集合,也称为关联数组 从表面上看,Set和Map相似性很少,但实际上可以说Map集合时Set集合的扩展. 1.Set集合和Map集合的 ...

  2. 【读书笔记】【深入理解ES6】#7-Set集合和Map集合

    ES6新标准中将Set集合和Map集合添加到JS中. ES5中Set集合和Map集合 在ES5中,开发者们用对象属性来模拟这两种集合. var set = Object.create(null); s ...

  3. 【spring set注入 注入集合】 使用set注入的方式注入List集合和Map集合/将一个bean注入另一个Bean

    Dao层代码: package com.it.dao; public interface SayHell { public void sayHello(); } Dao的Impl实现层: packag ...

  4. Java List集合和Map集合的综合应用

    public static void main(String[] args) { //--------------------------------------------------------- ...

  5. 编写Java程序,使用List集合和Map集合输出 市和区

    如图: 代码: import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java ...

  6. Map集合和Map常用子类

    Map集合 java.util.Map<K,V>集合 Map集合的特点: 1.Map集合是一个双列集合,一个元素包含两个值(Key,Value) 2.Map集合中的元素,key和value ...

  7. Scala集合和Java集合对应转换关系

    作者:Syn良子 出处:http://www.cnblogs.com/cssdongl 转载请注明出处 用Scala编码的时候,经常会遇到scala集合和Java集合互相转换的case,特意mark一 ...

  8. Spring面试题集

    一.Spring简介       *  Spring框架有哪几部分组成? Spring框架有七个模块组成组成,这7个模块(或组件)均可以单独存在,也可以与其它一个或多个模块联合使用,主要功能表现如下: ...

  9. Spring依赖注入 --- 简单使用说明

    Spring依赖注入 --- 简单使用说明 本文将对spring依赖注入的使用做简单的说明,enjoy your time! 1.使用Spring提供的依赖注入 对spring依赖注入的实现方法感兴趣 ...

  10. Spring依赖注入 --- 模拟实现

    Spring依赖注入 --- 模拟实现 面向接口编程,又称面向抽象编程, 数据库如果发生更改,对应的数据访问层也应该改变多写几个实现,需要用谁的时候在service里new谁就可以了面向抽象编程的好处 ...

随机推荐

  1. Ubuntu 22.04 安装 VMWare 16.2.3 后无法启动

    异常日志: 2022-06-13T03:49:56.019Z In(05) host-29676 In file included from /tmp/modconfig-XR2GVI/vmmon-o ...

  2. useBean类属性[javaChuLi.LoginBean]的值无效

    今天遇到了一个错误,如图 参见:JSP 中给定的 useBean 标签的 class 属性的值无效_dkawskawx的博客-CSDN博客

  3. Qt 5.15.2 QTextEdit无法设置新字体的处理方式

    首发于我的个人博客:xie-kang.com 博客内有更多文章,欢迎大家访问 原文地址 在使用QT 5.15.2 开发的过程中碰到了件怪事,下列代码无法给QTextEdit选中的文字设置字体: QTe ...

  4. Anaconda与conda、pip与conda的区别 - 搬运

    Anaconda与conda.pip与conda的区别 风影忍着   转自:https://zhuanlan.zhihu.com/p/379321816     作为一个Python初学者,在请教资深 ...

  5. (Winform程序带源码) 弹出输入框和获取输入框的值

    弹出输入框和获取输入框的值: private void button1_Click(object sender, EventArgs e) { string returnValue = Microso ...

  6. c#获取文本中的内容

    string path = HttpContext.Current.Server.MapPath("/文件夹/名称.txt"); string ss = File.ReadAllT ...

  7. 使用MyBatis时需要注意到的事情------执行添加、修改和删除操作时,一定要记得提交事务

    今天在重写添加操作代码时,发现自己写的代码没有任何报错,使用断点进行查询,发现一切正常,但是注册使用的数据就是无法添加到数据库里面 然后就去之前看过的视频里面去找错误,就发现这样一个小细节: 在视频里 ...

  8. CSS 高阶小技巧 - 角向渐变的妙用!

    本文将介绍一个角向渐变的一个非常有意思的小技巧! 我们尝试使用 CSS 绘制如下图形: 在之前,类似的图案,其实我们有尝试过,在 单标签实现复杂的棋盘布局 一文中,我们用单标签实现了这样一个棋盘布局: ...

  9. 20个值得收藏的实用JavaScript技巧

    1.确定对象的数据类型 function myType(type) { return Object.prototype.toString.call(type).slice(8, -1); 使用Obje ...

  10. JVM 重点知识归纳

    JVM(Java Virtual Machine:译为 Java虚拟机)内核: 通常指通过软件模拟的具有完整硬件系统功能的运行在一个完全隔离环境汇总的完整计算机系统.如下:  ■  Mware/Vis ...