微信公众号开发文章目录

1.微信公众号开发 - 环境搭建
2.微信公众号开发 - 配置表设计以及接入公众号接口开发
3.微信公众号开发 - token获取(保证同一时间段内只请求一次)
4.微信公众号开发 - 菜单按钮bean封装
5.微信公众号开发 - 创建菜单
6.微信公众号开发 - 事件处理和回复消息
7.微信公众号开发 - 发送Emoji表情

项目完整代码请访问github:https://github.com/liaozq0426/wx.git

获取微信公众号token的流程

由于微信公众号token在获取后需要过一段时间才会失效,且获取token的接口每日有调用次数限制,因此我们对获取的token需要做缓存处理,避免每次用到token时都访问微信远程服务器。我们获取token的逻辑流程图如下

1)当redis缓存和数据库不存在token时,或者token已经失效时,从微信远程服务器获取token,并将获取的token缓存至数据库中。
2)当redis缓存或数据库存在token时,且token未失效时,直接返回token。

如何保证同一时间段内只请求微信服务器一次?

在考虑高并发的情况下,同一时间段内可能会调用微信服务器接口多次,为了避免这种情况出现,首先我们想到的可能是对方法进行同步处理,也就是使用synchronized关键字

public synchronized String readAccessToken() {}

但是对整个方法同步处理的话,效率较低。我们可以对token的类型(accessType)进行同步,保证获取同一种类型的token是同步行进的,并且同时结合AtomicInteger类型变量的原子性,保证同步的情况下,同一时间段内只请求微信服务器一次。

上图中
1)首先需要先定义一个AtomicInteger类型的变量,初始值 0
2)当开始调用微信远程接口之前,首先需要判断AtomicInteger变量的值是否为1,如果不为1,则说明此时没有其他的线程在请求微信接口,这时我们可以调用微信接口,但调用之前需要设置AtomicInteger的值为1,表示已经有线程在请求微信接口了;如果AtomicInteger变量值为1,说明已经有线程在请求微信接口,此时就不要再次请求微信接口,而是循环读取缓存中的token
3)当有线程获取token成功后,需要将AtomicInteger变量重新设置为0,并将新token更新至缓存中。
4)对于accessType.intern()的理解,accessType为token的类型,在平时微信公众号开发中,常用的有access_tokenjsapi_ticket两种,前者为基础token,后者为使用jsapi时需要用到的token;intern()方法解释起来比较复杂,可以查阅资料了解一下。

wx_token表设计,存放token

CREATE TABLE `wx_token` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',`platform` varchar(20) DEFAULT NULL COMMENT '公众号标识',`token_type` varchar(20) NOT NULL COMMENT 'token类型,access_token:基础token,jsapi_token:jsapi_ticket',`access_token` text NOT NULL COMMENT 'token值',`expires_in` int(11) NOT NULL COMMENT '失效时长',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`last_upd_time` timestamp NULL DEFAULT '0000-00-00 00:00:00' COMMENT '最后一次更新时间',`refresh_count` int(11) DEFAULT NULL COMMENT '刷新次数',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

微信公众号开发一般会用到两个token
1)access_token :是公众号的全局唯一接口调用凭据
2)jsapi_token:jsapi_ticket,是公众号用于调用微信JS接口的临时票据
我们可以将两种不同的token存储在一张表,便于维护

编写获取token的核心代码
由于获取token的代码较多,这里只展示了部分核心代码,如果想看完成代码,请通过github获取

以下是wx_token业务接口和实现类代码,其中WxTokenService中有4个接口,代码如下

package com.gavin.service;import java.util.List;import com.gavin.pojo.AccessToken;
import com.gavin.pojo.WxToken;
public interface WxTokenService {public List<WxToken> select(WxToken token) throws Exception;public WxToken selectOne(WxToken token) throws Exception;public int save(WxToken token) throws Exception;public AccessToken readAccessToken(String accessType , String platform) throws Exception;
}

其中前三个接口为查询和保存wx_token记录,最后一个方法readAccessToken比较复杂,会依次从redis缓存、数据库、微信服务器中获取token,只要任意一个步骤获取成功就返回token。

WxTokenServiceImpl实现类代码如下

package com.gavin.service.impl;import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;import org.apache.commons.lang3.StringUtils;
import org.jboss.logging.Logger;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import com.gavin.cfg.RedisService;
import com.gavin.mapper.WxTokenMapper;
import com.gavin.pojo.AccessToken;
import com.gavin.pojo.Wechat;
import com.gavin.pojo.WxToken;
import com.gavin.service.WxCfgService;
import com.gavin.service.WxTokenService;
import com.gavin.util.WxUtil;@Service
public class WxTokenServiceImpl implements WxTokenService , DisposableBean {private Logger logger = Logger.getLogger(this.getClass());public static final int DEFAULT_ACCESS_TOKEN_EXPIRESIN = 120;public static Map<String , AtomicInteger> tokenSyncMap = new ConcurrentHashMap<>();@Autowiredprivate WxTokenMapper wxTokenMapper;@Autowiredprivate RedisService redisService;@Autowiredprivate WxCfgService wxCfgService;/*** @title 查询token集合* @author gavin* @date 2019年11月27日*/@Overridepublic List<WxToken> select(WxToken token) throws Exception {return wxTokenMapper.select(token);}/*** @title 查询单个token* @author gavin* @date 2019年11月27日*/@Overridepublic WxToken selectOne(WxToken token) throws Exception {List<WxToken> tokenList = select(token);if(tokenList.size() == 1)return tokenList.get(0);logger.info("查询结果集不符合预期");return null;}/*** @title 保存token至数据库* @author gavin* @date 2019年11月27日*/@Overridepublic int save(WxToken token) throws Exception {Integer id = token.getId();if(id != null && id > 0) {// 更新return this.wxTokenMapper.update(token);}else {// 新增return this.wxTokenMapper.insert(token);}}/*** @title 读取微信token* @author gavin* @date 2019年11月27日*/@Overridepublic AccessToken readAccessToken(String accessType, String platform) throws Exception {// 1.尝试从redis中读取AccessToken token = null;try {token = readAccessTokenByRedisAndDb(accessType , platform);if(token != null && !StringUtils.isBlank(token.getAccess_token()))return token;if(tokenSyncMap.get(accessType) != null && tokenSyncMap.get(accessType).get() > 0) {while(tokenSyncMap.get(accessType).get() > 0) {// 此时正在向微信服务器请求token,阻塞等待Thread.sleep(100);logger.info("正在向微信服务器请求token,阻塞等待...");}token = readAccessTokenByRedisAndDb(accessType , platform);if(token != null && !StringUtils.isBlank(token.getAccess_token()))return token;elsereturn null;}else {                  // 3.尝试从微信服务器上获取// 同步intern,保证在同一时间段内仅访问远程服务器一次String intern = accessType.intern();synchronized (intern) {                  tokenSyncMap.put(accessType, new AtomicInteger(1));try {if(AccessToken.TYPE_ACCESS_TOKEN.equals(accessType) || AccessToken.TYPE_JSAPI_TOKEN.equals(accessType)) {           Wechat wechat = wxCfgService.selectWechat(platform);if(AccessToken.TYPE_ACCESS_TOKEN.equals(accessType)) {         logger.info("从微信服务器上获取access_token");token = WxUtil.getAccessToken(wechat.getBase64DecodeAppId(), wechat.getBase64DecodeAppSecret());}if(AccessToken.TYPE_JSAPI_TOKEN.equals(accessType)) {logger.info("从微信服务器上获取js_ticket");AccessToken Atoken = readAccessToken(AccessToken.TYPE_ACCESS_TOKEN , platform);token = WxUtil.getJSTicket(Atoken.getAccess_token());}}if(token != null && !StringUtils.isBlank(token.getAccess_token())) {             token.setAccess_type(accessType);// 缓存tokencacheAccessToken(token , platform);}else {logger.error("从微信服务器上获取access_token失败");}} catch (Exception e) {logger.error("从微信服务器上获取access_token失败");logger.error(e.getMessage() , e);} finally {tokenSyncMap.get(accessType).decrementAndGet();logger.info("tokenSyncMap." + accessType + " count:" + tokenSyncMap.get(accessType).get());}}return token;}} catch (Exception e) {logger.error(e.getMessage(), e);throw new Exception("读取access_token失败");}}/*** @title 从redis和数据库中读取accessToken* @param accessType* @return*/private AccessToken readAccessTokenByRedisAndDb(String accessType , String platform) {if(StringUtils.isBlank(accessType)) return null;String redisKey = null;if(!StringUtils.isBlank(accessType)) {// 生产redisKeyredisKey = makeAccessTokenRedisKey(accessType , platform);}AccessToken token = null;try {Object obj = null;// 1.尝试从redis中读取logger.info("尝试从redis中读取...");obj = redisService.get(redisKey);if(obj != null) token = (AccessToken) obj;if(token != null && !StringUtils.isBlank(token.getAccess_token())) {long tokenCreateTime = token.getCreate_time();logger.info("tokenCreateTime:" + tokenCreateTime);long interval = (System.currentTimeMillis() - tokenCreateTime) / 1000;logger.info("interval:" + interval);if(interval <= (token.getExpires_in() - DEFAULT_ACCESS_TOKEN_EXPIRESIN)) {                    return token;}else {logger.info("redis中的accessToken已经失效");redisService.del(redisKey);}}} catch (Exception e) {logger.error("尝试从redis中读取access_token失败");logger.error(e.getMessage() , e);}// 2.尝试从数据库中获取try {logger.info("尝试从数据库中读取...");WxToken wxTokenParam = new WxToken();wxTokenParam.setTokenType(accessType);wxTokenParam.setPlatform(platform);WxToken wxToken = this.selectOne(wxTokenParam);      if(wxToken != null) {// 判断token是否失效int expiresIn = wxToken.getExpiresIn();Date lastUpdTime = wxToken.getLastUpdTime();logger.info("System.currentTimeMillis:" + System.currentTimeMillis());logger.info("lastUpdTime:" + lastUpdTime.getTime() + ",format:" + lastUpdTime);long interval = (System.currentTimeMillis() - lastUpdTime.getTime()) / 1000;logger.info("interval:" + interval);if(interval <= (expiresIn - DEFAULT_ACCESS_TOKEN_EXPIRESIN)) {token = new AccessToken();token.setAccess_token(wxToken.getAccessToken());token.setAccess_type(wxToken.getTokenType());token.setExpires_in(wxToken.getExpiresIn());token.setCreate_time(wxToken.getLastUpdTime().getTime());// 同步至redis// long redisExpires = System.currentTimeMillis() - wxToken.getLastUpdTime().getTime();long redisExpires = expiresIn - interval;redisService.set(redisKey, token , redisExpires);return token;  }}} catch (Exception e) {logger.error("尝试从数据库中读取access_token失败");logger.error(e.getMessage() , e);}return null;}/*** @title 缓存access token,1.缓存至redis 2.缓存至数据库* @author gavin* @date 2019年5月23日* @param accessToken* @param platform* @throws Exception*/public void cacheAccessToken(AccessToken accessToken , String platform) throws Exception {// 如果token的创建时间为空,则必须设置(从微信服务器获取到token时create_time为空)if(accessToken.getCreate_time() == 0) {           accessToken.setCreate_time(new Date().getTime());logger.info("设置token创建时间");}logger.info("accessToken_createTime:" + accessToken.getCreate_time());logger.info("System.currentTimeMillis:" + System.currentTimeMillis());// 1.缓存至redisString redisKey = null;String accessType = accessToken.getAccess_type();if(!StringUtils.isBlank(accessType)) {          redisKey = makeAccessTokenRedisKey(accessType , platform);redisService.set(redisKey, accessToken, accessToken.getExpires_in());logger.info("缓存" + platform + " " + accessType + "至redis成功");// 2.缓存至数据库WxToken tokenParam = new WxToken();tokenParam.setTokenType(accessType);tokenParam.setAccessToken(accessToken.getAccess_token());tokenParam.setExpiresIn(accessToken.getExpires_in());tokenParam.setPlatform(platform);// 1.先查询数据库中是否存在记录int result = 0;WxToken wxAccessToken = this.selectOne(tokenParam);if(wxAccessToken == null) {// 首次插入tokenParam.setRefreshCount(0);result = this.wxTokenMapper.insert(tokenParam);}else {// 更新if(wxAccessToken.getRefreshCount() == null) {tokenParam.setRefreshCount(1);}else {                   tokenParam.setRefreshCount(wxAccessToken.getRefreshCount() + 1);}tokenParam.setId(wxAccessToken.getId());result = this.wxTokenMapper.update(tokenParam);}if(result == 1) logger.info("缓存" + platform + " " + tokenParam.getTokenType() + "至数据库成功");elselogger.info("缓存" + platform + " " + tokenParam.getTokenType() + "至数据库失败");}}/*** @title access_token redis 缓存 key规则* @author gavin* @date 2019年5月23日* @param accessType* @param platform* @return*/private String makeAccessTokenRedisKey(String accessType , String platform) {String redisKey = platform + "_" + accessType;return redisKey;}/*** @title 销毁时清空缓存* @author gavin* @date 2019年11月27日*/@Overridepublic void destroy() throws Exception {tokenSyncMap.clear();System.out.println("tokenSyncMap清空了,size:" + tokenSyncMap.size());}}

微信公众号开发 - token获取(保证同一时间段内只请求一次)相关推荐

  1. java微信公众号开发token验证失败的问题及解决办法

    java微信公众号开发token验证失败的问题及解决办法 参考文章: (1)java微信公众号开发token验证失败的问题及解决办法 (2)https://www.cnblogs.com/beardu ...

  2. 【微信公众号开发】获取并保存access_token、jsapi_ticket票据(可用于微信分享、语音识别等等)...

    步骤一:首先得开通公众号(目的是 获得appid.AppSecret.设置安全域名)~ [公众号设置]→[功能设置] 设置相应的域名 步骤二:编写帮助类WeixinLuyinHelper中的代码 #r ...

  3. 微信公众号开发之获取用户地理位置

    使用微信的用户地理位置接口就要配置这里. 前端代码: function configWx() {var thisPageUrl = location.href.split('#')[0];$.ajax ...

  4. 微信公众号开发:获取openId和用户信息(完整版)

    注:之前总结怎么进行本地公众号开发调试,时间一长忘记开发配置却忘了,所以这里记录一下公众号开发配置,方便快速上手. 目录 开发前服务器配置 网页授权获取用户基本信息 snsapi_base snsap ...

  5. 微信公众号开发系列-获取微信OpenID

    在微信开发时候在做消息接口交互的时候需要使用带微信加密ID(OpenId),下面讲讲述2中类型方式获取微信OpenID,接收事件推送方式和网页授权获取用户基本信息方式获取. 1.通过接收被动消息方式获 ...

  6. php 公众号token认证,微信公众号开发——Token认证

    公众号开发第一步就是绑定Token,Token认证相当于把我们的公众号和服务器关联起来,只有Token认证成功了我们的服务器才能接收到来自公众号的消息.微信官方回调的地址必须能在公网上访问,后端服务的 ...

  7. Node微信公众号开发 - 定时获取最新文章同步到MySQL数据库

    0.介绍 本文源码:https://github.com/Jameswain/... ​ ​ ​ 最近有一个需求:把5个公众号的所有文章定时同步到小程序的数据库里,10分钟同步一次.实现这个需求当时我 ...

  8. 微信公众号开发之获取用户信息

    微信获取用户信息的方式有两种,静默授权(无需用户同意)和非静默授权(需要用户" 手动点击 "拉取授权,可以用户无需关注公众号即可获取用户信息) 整体的代码请查看最后,前边为原理介绍 ...

  9. 微信公众号开发之获取oppenid和用户基本信息

    前言: 在微信公众号请求用户网页授权之前,开发者需要先在自己的公众平台配置好基本配置,修改授权回调域名JS安全域名.并且需要先获取到全局access_token,这里不对全局access_token的 ...

最新文章

  1. docker安装kibana7.6.1
  2. list clear 2 python,python中怎么将列表的数据清空
  3. 左值、右值、左值引用、右值引用
  4. 推荐 10 个有趣的 Python 项目
  5. 基于Android 虹软人脸、人证对比,活体检测
  6. 二层交换机、三层交换机和路由器的基本工作原理和三者之间的主要区别
  7. [Octotree] 树形展示GitHub项目
  8. 74系列芯片引脚图资料大全
  9. 计算机四级(网络工程师)内容,计算机四级《网络工程师》考试内容
  10. CodeBook 可以自定义字符集的密码本
  11. 有赞搜索系统的技术内幕
  12. BZOJ 1127: [POI2008]KUP 最大子矩阵
  13. 表达式的LenB(123程序设计ABC)的值是
  14. .net ref java_Java URL.getRef方法代碼示例
  15. 大数据剖析 | 北京VS上海: 活着为了工作还是工作为了生活?
  16. linux之pmap命令查看进程的地址空间和占用的内存
  17. 对言语上的自律和真正的自律的一些想法
  18. 西瓜直播弹幕阅读器 python
  19. win10设置微信双开电脑登录多个微信,超级详细教程,小白也可轻松设置
  20. 美团美食板块的token加密

热门文章

  1. 微信小程序_16,组件的生命周期
  2. 应用于RFID医疗试剂防伪管理系统解决方案
  3. 列表生成式(列表解析式、列表推导式)
  4. 【java框架】Maven是干什么的?
  5. 百兆网线和千兆网线做法的区别
  6. Linux定时任务与开机自启动脚本
  7. PLC的顺控继电器(SCR编程)总结
  8. 美国〈国家地理〉镜头中的极致之地
  9. 史上最全的APP推广模式及方法技巧
  10. RNN 、LSTM、 GRU、Bi-LSTM 等常见循环网络结构以及其Pytorch实现