微信公众号开发文章目录
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_token
和jsapi_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<>();
@Autowired
private WxTokenMapper wxTokenMapper;
@Autowired
private RedisService redisService;
@Autowired
private WxCfgService wxCfgService;
/**
* @title 查询token集合
* @author gavin
* @date 2019年11月27日
*/
@Override
public List<WxToken> select(WxToken token) throws Exception {
return wxTokenMapper.select(token);
}
/**
* @title 查询单个token
* @author gavin
* @date 2019年11月27日
*/
@Override
public 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日
*/
@Override
public 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日
*/
@Override
public 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;
else
return 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);
// 缓存token
cacheAccessToken(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)) {
// 生产redisKey
redisKey = 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.缓存至redis
String 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() + "至数据库成功");
else
logger.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日
*/
@Override
public void destroy() throws Exception {
tokenSyncMap.clear();
System.out.println("tokenSyncMap清空了,size:" + tokenSyncMap.size());
}
}