austin啊,但实际内容更新不多。这文章主是想吹下水,主要聊聊我在更新项目中学到的小技巧。
消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型。
https://gitee.com/zhongfucheng/austin/
- https://github.com/ZhongFuCheng3y/austin
Spring注入集合
pull request被提了过来。
我之前写了一个自定义注解,它的作用就是收集自定义注解所标识的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
,那就没办法调用微信的接口了。
对于wxjava
这个组件库,调用微信的api
都是通过Wx(xxService
来使用的,而我是想把Wx(xxService
做成是单例的。那在实现access_token
存储到Redis
的时候,我就很自然就要对旧代码进行一波重构(因为第一版写出来的代码,多多少少都有点不满意)。
1、WxServiceUtils
的逻辑是项目启动的时检索数据库里所有的微信渠道账号信息,将Wx(xxService
写入到Map
里。Wx(xxService
要做成单例自然就会想到用Map
存储(因为消息推送平台很可能会对接很多个服务号或者小程序,这里数据结构肯定优先是Map
啦)
通过后台有存在变更行为,那程序内部会执行refresh(
刷新。但这个仅仅是在程序内能监听到的变更,如果是直接通过SQL
修改表的记录,目前是没有机制刷新Map
的内容的。
AccountUtils的逻辑是程序运行时得到发送账号的Id
,通过Id
去数据库检索账号配置,实时返回账号最新的内容。(除了微信渠道账号,其他所有的渠道账号都是在这里获取信息)
WxServiceUtils类给弃用了,将所有的发送渠道账号信息都归到AccountUtils
进行管理。
Map.computeIfAbsent使用
if (clazz.equals(WxMaService.class {
if (Objects.nonNull(miniProgramServiceMap.get(channelAccount {
return (TminiProgramServiceMap.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 (TofficialAccountServiceMap.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
存在性能的问题。呀,不小心又学到了点东西。
微信扫码登录实现
我在生产环境下是没有写过「用户登录」的,导致有些业务功能我也不知道线上是怎么实现的。而「用户登录注册」这个功能之前会听过和见识过一些技术栈「Shiro」、「JWT」、「Spring Security」、「CAS」、「OAuth2.0」等等。
现在不都流行扫码登录嘛?我不是已经接入了微信服务号的模板消息了吗,不正好有一个测试号给我去做吗?于是就开干了。
在前端,就一个「轮询」功能,要轮询查看用户是否已经订阅登录,就耗费了我很多时间在官方文档上。后来,写了不少的奇淫技巧,最后也就被我实现出来了。实现过程很糟糕,也不值一提,反正你们也不会从中学到什么好东西,因为我也没有。
1、首先我们要有一个接口,给到微信回调,所以我们一般会称该接口为回调接口。微信的一些重要的事件都会回调给我们,我们做响应的逻辑处理。就比如,用户关注了服务号,这种消息微信就调用我们的接口。
2、在微信后台配置我们的定义好的回调接口,给到微信进行回调。
3、编写一个获取微信带参数的二维码给到前端做展示。
4、前端拿到二维码做展示,并且得到随机生成的参数轮询查看是否已登录。
5、编写检查是否已登录的接口给到前端进行判断。(如果能从Redis里拿到随机参数,说明已经登录了)
6、当用户扫码关注了服务号,则得到微信的回调。当用户关注服务号时,会把随机参数和openId传给服务器,我则将信息存入Redis。
7、前端得知已登录后,将用户信息写入localStorage
最后
Map.computeIfAbsent这个,我感觉没理由我不知道呀。我从初学到现在工作主要用JDK 1.8
,没道理我现在才知道写这个玩意。
不过写开源项目有一大好处是,只要我的项目有人用,能大大提高我获取“优雅”写法的概率,这也是我一直推广自己项目的一个原因之一。
强烈推荐我的开源项目消息推送平台Austin(8K stars),可以用作毕业设计,可以用作校招,可以看看生产环境是怎么推送消息的。开源项目消息推送平台austin仓库地址:
消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型。
https://gitee.com/zhongfucheng/austin/
- https://github.com/ZhongFuCheng3y/austin