基于spring Boot的微信开发————AccessToken的缓存方案(一)
通过微信授权拿到的token有一个7200秒的有效期,并且获取token的次数也有一定限制,所以token不能“随用随取”,通过网络资源获取,解决方案有①储存在数据库中,每次使用时都查询一次数据库,②定义静态全局变量(单例模式),开启线程监控变量是否“过期”,③结合数据库做缓存,开启定时任务,每7200秒从微信服务器中获取一次token并保存到自己的数据库中
各方法的优缺:
①:简单粗暴,对数据库的操作过多,适合访问量小,调用微信API次数少的项目
②:(单例模式)全局变量在服务器重启时会清除,不使用数据库的方式,变量设置不当可能会混淆(微信有两个token,一个用于(粉丝)用户授权,一个用于开发者授权)
③:灵活,易结合到各类开发中,spring也对各种缓存方案有很好的支持
以下为方法③的实现方式之一(未开启定期任务,缓存方案使用ehcache)
基本配置
ehcache.xml配置
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<defaultCache
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU" />
<cache
name="TokenCache"
eternal="false"
maxElementsInMemory="100"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="7200"
timeToLiveSeconds="7200"
memoryStoreEvictionPolicy="LRU" />
<!-- eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false -->
<!-- maxEntriesLocalHeap:堆内存中最大缓存对象数,0没有限制 -->
<!-- timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,
如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清空。
只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地处于空闲状态 -->
</ehcache>
spring boot配置
#端口
server.port=8080
#数据源
spring.datasource.name=test
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true&zeroDateTimeBehavior=convertToNull
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
##连接池属性配置
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.filters=stat,wall,log4j
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
#mybatis.config
mybatis.mapper-locations=classpath:mapping/*xml
mybatis.type-aliases-package=cn.vision.springbootdemo.model
#分页插件
pagehelper.helper-dialect=mysql
pagehelper.reasonable=true
pagehelper.support-methods-arguments=true
pagehelper.params=count=countSql
#日志
logging.level.cn.vision.weixindemo.mapper = debug
# spring 设置缓存方案为ehcache
spring.cache.type=ehcache
spring.cache.ehcache.config=classpath:cache/ehcache.xml
微信工具类———微信post、get请求工具类
package cn.vision.weixindemo.utils.base;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.ParseException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class BaseRequest {
/**
* get请求
* @param url
* @return
* @throws ParseException
* @throws IOException
*
* @author vision
*/
public static JSONObject doGet(String url) throws ParseException, IOException {
CloseableHttpClient client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
JSONObject jsonObject = null;
HttpResponse httpResponse = client.execute(httpGet);
HttpEntity entity = httpResponse.getEntity();
if(entity != null){
String result = EntityUtils.toString(entity,"UTF-8");
jsonObject = JSONObject.parseObject(result);
}
// 释放连接
httpGet.releaseConnection();
client.close();
return jsonObject;
}
/**
* POST请求
* @param url
* @param outStr
* @return
* @throws ParseException
* @throws IOException
*/
public static JSONObject doPost(String url, String outStr) throws ParseException, IOException{
CloseableHttpClient client = HttpClients.createDefault();
HttpPost httpost = new HttpPost(url);
httpost.setEntity(new StringEntity(outStr,"UTF-8"));
HttpResponse response = client.execute(httpost);
String result = EntityUtils.toString(response.getEntity(),"UTF-8");
// 释放连接
httpost.releaseConnection();
client.close();
return JSONObject.parseObject(result);
}
}
微信工具类————token工具类(API函数,SpringContextHolder类)
package cn.vision.weixindemo.utils.Token;
import cn.vision.weixindemo.config.WeiXinConfig;
import cn.vision.weixindemo.model.AccessToken;
import cn.vision.weixindemo.service.TokenService;
import cn.vision.weixindemo.utils.base.API.WeiXin_API;
import cn.vision.weixindemo.utils.base.BaseRequest;
import cn.vision.weixindemo.utils.spring.SpringContextHolder;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.ParseException;
import java.io.IOException;
import java.util.Date;
public class TokenUtils {
private TokenService tokenService = SpringContextHolder.getBean(TokenService.class);
/**
* 获取accessToken
*
* @return
* @throws ParseException
* @throws IOException
*/
public AccessToken getAccessToken() throws ParseException, IOException {
AccessToken accessToken = tokenService.selectByPrimaryKey(1);
System.out.println("CreateTime: "+accessToken.getCreatedate().getTime()+"NOW: "+new Date().getTime() +"\n"+ Long.parseLong(accessToken.getExpiresin()));
if(accessToken == null || accessToken.getCreatedate().getTime() + Long.parseLong(accessToken.getExpiresin()) *1000 < new Date().getTime()){
String url = WeiXin_API.API_GET_ACCESS_TOKEN.replace("APPID", WeiXinConfig.APPID).replace("APPSECRET", WeiXinConfig.APPSECRET);
JSONObject jsonObject = BaseRequest.doGet(url);
System.out.println(jsonObject.toString());
if(jsonObject.getString("errorcode") == null){
accessToken = new AccessToken();
accessToken.setId(1);
accessToken.setAccessToken(jsonObject.getString("access_token"));
accessToken.setExpiresin(jsonObject.getString("expires_in"));
accessToken.setCreatedate(new Date());
}
if(accessToken == null){
tokenService.insert(accessToken);
}
tokenService.updateByPrimaryKey(accessToken);
}
return accessToken;
}
}
Spring Boot启动类中开启缓存注解@EnableCaching
package cn.vision.weixindemo;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication(exclude={DruidDataSourceAutoConfigure.class})
@EnableCaching
@MapperScan("cn.vision.weixindemo.mapper")
public class WeixindemoApplication {
public static void main(String[] args) {
SpringApplication.run(WeixindemoApplication.class, args);
}
}
数据库表结构
数据模型
package cn.vision.weixindemo.model;
import java.util.Date;
public class AccessToken {
private Integer id;
private String accessToken;
private String expiresin;
private Date createdate;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken == null ? null : accessToken.trim();
}
public String getExpiresin() {
return expiresin;
}
public void setExpiresin(String expiresin) {
this.expiresin = expiresin == null ? null : expiresin.trim();
}
public Date getCreatedate() {
return createdate;
}
public void setCreatedate(Date createdate) {
this.createdate = createdate;
}
}
mapper
package cn.vision.weixindemo.mapper;
import cn.vision.weixindemo.model.AccessToken;
public interface AccessTokenMapper {
int deleteByPrimaryKey(Integer id);
int insert(AccessToken record);
int insertSelective(AccessToken record);
AccessToken selectByPrimaryKey(Integer id);
int updateByPrimaryKeySelective(AccessToken record);
int updateByPrimaryKey(AccessToken record);
}
mappering
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.vision.weixindemo.mapper.AccessTokenMapper" >
<resultMap id="BaseResultMap" type="cn.vision.weixindemo.model.AccessToken" >
<id column="Id" property="id" jdbcType="INTEGER" />
<result column="Access_Token" property="accessToken" jdbcType="VARCHAR" />
<result column="expiresIn" property="expiresin" jdbcType="VARCHAR" />
<result column="createDate" property="createdate" jdbcType="TIMESTAMP" />
</resultMap>
<sql id="Base_Column_List" >
Id, Access_Token, expiresIn, createDate
</sql>
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
select
<include refid="Base_Column_List" />
from weixin_token
where Id = #{id,jdbcType=INTEGER}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
delete from weixin_token
where Id = #{id,jdbcType=INTEGER}
</delete>
<insert id="insert" parameterType="cn.vision.weixindemo.model.AccessToken" >
insert into weixin_token (Id, Access_Token, expiresIn,
createDate)
values (#{id,jdbcType=INTEGER}, #{accessToken,jdbcType=VARCHAR}, #{expiresin,jdbcType=VARCHAR},
#{createdate,jdbcType=TIMESTAMP})
</insert>
<insert id="insertSelective" parameterType="cn.vision.weixindemo.model.AccessToken" >
insert into weixin_token
<trim prefix="(" suffix=")" suffixOverrides="," >
<if test="id != null" >
Id,
</if>
<if test="accessToken != null" >
Access_Token,
</if>
<if test="expiresin != null" >
expiresIn,
</if>
<if test="createdate != null" >
createDate,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides="," >
<if test="id != null" >
#{id,jdbcType=INTEGER},
</if>
<if test="accessToken != null" >
#{accessToken,jdbcType=VARCHAR},
</if>
<if test="expiresin != null" >
#{expiresin,jdbcType=VARCHAR},
</if>
<if test="createdate != null" >
#{createdate,jdbcType=TIMESTAMP},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="cn.vision.weixindemo.model.AccessToken" >
update weixin_token
<set >
<if test="accessToken != null" >
Access_Token = #{accessToken,jdbcType=VARCHAR},
</if>
<if test="expiresin != null" >
expiresIn = #{expiresin,jdbcType=VARCHAR},
</if>
<if test="createdate != null" >
createDate = #{createdate,jdbcType=TIMESTAMP},
</if>
</set>
where Id = #{id,jdbcType=INTEGER}
</update>
<update id="updateByPrimaryKey" parameterType="cn.vision.weixindemo.model.AccessToken" >
update weixin_token
set Access_Token = #{accessToken,jdbcType=VARCHAR},
expiresIn = #{expiresin,jdbcType=VARCHAR},
createDate = #{createdate,jdbcType=TIMESTAMP}
where Id = #{id,jdbcType=INTEGER}
</update>
</mapper>
service
package cn.vision.weixindemo.service;
import cn.vision.weixindemo.model.AccessToken;
import org.springframework.stereotype.Service;
public interface TokenService {
int deleteByPrimaryKey(Integer id);
int insert(AccessToken record);
int insertSelective(AccessToken record);
AccessToken selectByPrimaryKey(Integer id);
int updateByPrimaryKeySelective(AccessToken record);
int updateByPrimaryKey(AccessToken record);
}
serviceImpl
package cn.vision.weixindemo.service;
import cn.vision.weixindemo.mapper.AccessTokenMapper;
import cn.vision.weixindemo.model.AccessToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
@CacheConfig(cacheNames = "TokenCache")
public class TokenServiceImpl implements TokenService{
@Autowired
private AccessTokenMapper tokenMapper;
@Override
@CacheEvict(key = "#p0")
public int deleteByPrimaryKey(Integer id) {
return tokenMapper.deleteByPrimaryKey(id);
}
@Override
@CachePut(key = "#p0")
public int insert(AccessToken record) {
return tokenMapper.insert(record);
}
@Override
@CachePut(key = "#p0")
public int insertSelective(AccessToken record) {
return tokenMapper.insertSelective(record);
}
@Override
@Cacheable(key = "#p0")
public AccessToken selectByPrimaryKey(Integer id) {
return tokenMapper.selectByPrimaryKey(id);
}
@Override
@Cacheable(key = "#p0")
public int updateByPrimaryKeySelective(AccessToken record) {
return tokenMapper.updateByPrimaryKeySelective(record);
}
@Override
@Cacheable(key = "#p0")
public int updateByPrimaryKey(AccessToken record) {
return tokenMapper.updateByPrimaryKey(record);
}
}
测试用Controller
package cn.vision.weixindemo.controller;
import cn.vision.weixindemo.service.TokenService;
import cn.vision.weixindemo.utils.Token.TokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import java.io.IOException;
@Controller
@RequestMapping("/test")
public class TestController {
@Autowired
TokenService tokenService;
@RequestMapping("/testCache.do")
public String TestCache(Model model) throws IOException {
tokenService.selectByPrimaryKey(1);
model.addAttribute("token",new TokenUtils().getAccessToken());
return "test";
}
}
测试用html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>缓存测试</title>
</head>
<body>
<!--/*@thymesVar id="token" type="cn.vision.weixindemo.model.AccessToken"*/-->
<h1 th:text="${token.getAccessToken()}">
</h1>
</body>
</html>
运行截图
注解说明:
-
@CacheConfig
:主要用于配置该类中会用到的一些共用的缓存配置。在这里@CacheConfig(cacheNames = "users")
:配置了该数据访问对象中返回的内容将存储于名为users的缓存对象中,我们也可以不使用该注解,直接通过@Cacheable
自己配置缓存集的名字来定义。 -
@Cacheable
:配置了findByName函数的返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。该注解主要有下面几个参数:-
value
、cacheNames
:两个等同的参数(cacheNames
为Spring 4新增,作为value
的别名),用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig
,因此在Spring 3中原本必须有的value
属性,也成为非必需项了 -
key
:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = "#p0")
:使用函数第一个参数作为缓存的key值,更多关于SpEL表达式的详细内容可参考官方文档 -
condition
:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = "#p0", condition = "#p0.length() < 3")
,表示只有当第一个参数的长度小于3的时候才会被缓存,若做此配置上面的AAA用户就不会被缓存,读者可自行实验尝试。 -
unless
:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition
参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。 -
keyGenerator
:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator
接口,并使用该参数来指定。需要注意的是:该参数与key
是互斥的 -
cacheManager
:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用 -
cacheResolver
:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver
接口来实现自己的缓存解析器,并用该参数指定。
-
除了这里用到的两个注解之外,还有下面几个核心注解:
-
@CachePut
:配置于函数上,能够根据参数定义条件来进行缓存,它与@Cacheable
不同的是,它每次都会真是调用函数,所以主要用于数据新增和修改操作上。它的参数与@Cacheable
类似,具体功能可参考上面对@Cacheable
参数的解析 -
@CacheEvict
:配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。除了同@Cacheable
一样的参数之外,它还有下面两个参数:-
allEntries
:非必需,默认为false。当为true时,会移除所有数据 -
beforeInvocation
:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。
-