共计 7114 个字符,预计需要花费 18 分钟才能阅读完成。
大家好,我是 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)));
}
这看着真简洁啊,如同曾经很完满了,原本有好几行的代码,优化了下变成了一行。
但我又思考了下,这个 putIfAbsent
的V
我这边传入的是一个办法,每次这个办法都会执行的(不管我的 Map
里有没有这个 K
), 这又感觉不太优雅了。
我又点进去 computeIfAbsent
看了下,嗯!这就是我想要的了:如果 Map
的V
不存在时,才去执行我生成 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;
}
(这个其实我在学 lambda
和stream
流的时候已经是体验过的,我日常也会简略写点,只是不晓得在 JDK
里Map
也有这样的办法。)于是,最初的代码就成了:
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
来修复 ConcurrentHashMap
的computeIfAbsent
存在性能的问题。呀,不小心又学到了点货色。
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