Browse Source

:new: #1562企业微信自研应用增加Redisson的存储实现,支持分布式锁

* 企业微信自研应用,增加Redisson的存储实现,支持分布式锁

* 增加redisson的依赖。

Co-authored-by: 袁启勋 <xun.yuan@infoship.cn>
yuanqixun 5 years ago
parent
commit
cb3f105121

+ 5 - 1
weixin-java-cp/pom.xml

@@ -38,7 +38,11 @@
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
     </dependency>
-
+    <!--分布式锁支持-->
+    <dependency>
+      <groupId>org.redisson</groupId>
+      <artifactId>redisson</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.testng</groupId>
       <artifactId>testng</artifactId>

+ 113 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java

@@ -1,12 +1,125 @@
 package me.chanjar.weixin.cp.api.impl;
 
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import me.chanjar.weixin.common.WxType;
+import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.BasicResponseHandler;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.io.IOException;
+import java.util.concurrent.locks.Lock;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_AGENT_CONFIG_TICKET;
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_JSAPI_TICKET;
+
 /**
  * <pre>
  *  默认接口实现类,使用apache httpclient实现
  * Created by Binary Wang on 2017-5-27.
  * </pre>
+ * <pre>
+ * 增加分布式锁(基于WxCpConfigStorage实现)的支持
+ * Updated by yuanqixun on 2020-05-13
+ * </pre>
+ *
  *
  * @author <a href="https://github.com/binarywang">Binary Wang</a>
  */
 public class WxCpServiceImpl extends WxCpServiceApacheHttpClientImpl {
+  @Override
+  public String getAccessToken(boolean forceRefresh) throws WxErrorException {
+    if (!getWxCpConfigStorage().isAccessTokenExpired() && !forceRefresh) {
+      return getWxCpConfigStorage().getAccessToken();
+    }
+    Lock lock = getWxCpConfigStorage().getAccessTokenLock();
+    lock.lock();
+    try {
+      // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+      if (!getWxCpConfigStorage().isAccessTokenExpired() && !forceRefresh) {
+        return getWxCpConfigStorage().getAccessToken();
+      }
+      String url = String.format(getWxCpConfigStorage().getApiUrl(WxCpApiPathConsts.GET_TOKEN), this.configStorage.getCorpId(), this.configStorage.getCorpSecret());
+      try {
+        HttpGet httpGet = new HttpGet(url);
+        if (getRequestHttpProxy() != null) {
+          RequestConfig config = RequestConfig.custom()
+            .setProxy(getRequestHttpProxy()).build();
+          httpGet.setConfig(config);
+        }
+        String resultContent;
+        try (CloseableHttpClient httpClient = getRequestHttpClient();
+             CloseableHttpResponse response = httpClient.execute(httpGet)) {
+          resultContent = new BasicResponseHandler().handleResponse(response);
+        } finally {
+          httpGet.releaseConnection();
+        }
+        WxError error = WxError.fromJson(resultContent, WxType.CP);
+        if (error.getErrorCode() != 0) {
+          throw new WxErrorException(error);
+        }
+
+        WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+        getWxCpConfigStorage().updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    } finally {
+      lock.unlock();
+    }
+    return getWxCpConfigStorage().getAccessToken();
+  }
+
+  @Override
+  public String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException {
+    if (forceRefresh) {
+      getWxCpConfigStorage().expireAgentJsapiTicket();
+    }
+    if (getWxCpConfigStorage().isAgentJsapiTicketExpired()) {
+      Lock lock = getWxCpConfigStorage().getAgentJsapiTicketLock();
+      lock.lock();
+      try {
+        // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+        if (getWxCpConfigStorage().isAgentJsapiTicketExpired()) {
+          String responseContent = this.get(getWxCpConfigStorage().getApiUrl(GET_AGENT_CONFIG_TICKET), null);
+          JsonObject jsonObject = new JsonParser().parse(responseContent).getAsJsonObject();
+          getWxCpConfigStorage().updateAgentJsapiTicket(jsonObject.get("ticket").getAsString(),
+            jsonObject.get("expires_in").getAsInt());
+        }
+      } finally {
+        lock.unlock();
+      }
+    }
+    return getWxCpConfigStorage().getAgentJsapiTicket();
+  }
+
+  @Override
+  public String getJsapiTicket(boolean forceRefresh) throws WxErrorException {
+    if (forceRefresh) {
+      getWxCpConfigStorage().expireJsapiTicket();
+    }
+
+    if (getWxCpConfigStorage().isJsapiTicketExpired()) {
+      Lock lock = getWxCpConfigStorage().getJsapiTicketLock();
+      lock.lock();
+      try {
+        // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+        if (getWxCpConfigStorage().isJsapiTicketExpired()) {
+          String responseContent = this.get(getWxCpConfigStorage().getApiUrl(GET_JSAPI_TICKET), null);
+          JsonObject tmpJsonObject = new JsonParser().parse(responseContent).getAsJsonObject();
+          getWxCpConfigStorage().updateJsapiTicket(tmpJsonObject.get("ticket").getAsString(),
+            tmpJsonObject.get("expires_in").getAsInt());
+        }
+      } finally {
+        lock.unlock();
+      }
+    }
+    return getWxCpConfigStorage().getJsapiTicket();
+  }
 }

+ 7 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java

@@ -4,6 +4,7 @@ import me.chanjar.weixin.common.bean.WxAccessToken;
 import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
 
 import java.io.File;
+import java.util.concurrent.locks.Lock;
 
 /**
  * 微信客户端配置存储.
@@ -28,6 +29,8 @@ public interface WxCpConfigStorage {
 
   String getAccessToken();
 
+  Lock getAccessTokenLock();
+
   boolean isAccessTokenExpired();
 
   /**
@@ -41,6 +44,8 @@ public interface WxCpConfigStorage {
 
   String getJsapiTicket();
 
+  Lock getJsapiTicketLock();
+
   boolean isJsapiTicketExpired();
 
   /**
@@ -55,6 +60,8 @@ public interface WxCpConfigStorage {
 
   String getAgentJsapiTicket();
 
+  Lock getAgentJsapiTicketLock();
+
   boolean isAgentJsapiTicketExpired();
 
   /**

+ 20 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java

@@ -8,6 +8,8 @@ import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
 
 import java.io.File;
 import java.io.Serializable;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化.
@@ -22,6 +24,7 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
 
   private volatile String token;
   protected volatile String accessToken;
+  protected Lock accessTokenLock = new ReentrantLock();
   private volatile String aesKey;
   protected volatile Integer agentId;
   private volatile long expiresTime;
@@ -34,9 +37,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
   private volatile String httpProxyPassword;
 
   private volatile String jsapiTicket;
+  protected Lock jsapiTicketLock = new ReentrantLock();
   private volatile long jsapiTicketExpiresTime;
 
   private volatile String agentJsapiTicket;
+  protected Lock agentJsapiTicketLock = new ReentrantLock();
   private volatile long agentJsapiTicketExpiresTime;
 
   private volatile File tmpDirFile;
@@ -63,6 +68,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
     return this.accessToken;
   }
 
+  @Override
+  public Lock getAccessTokenLock() {
+    return this.accessTokenLock;
+  }
+
   public void setAccessToken(String accessToken) {
     this.accessToken = accessToken;
   }
@@ -93,6 +103,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
     return this.jsapiTicket;
   }
 
+  @Override
+  public Lock getJsapiTicketLock() {
+    return this.jsapiTicketLock;
+  }
+
   public void setJsapiTicket(String jsapiTicket) {
     this.jsapiTicket = jsapiTicket;
   }
@@ -123,6 +138,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
   }
 
   @Override
+  public Lock getAgentJsapiTicketLock() {
+    return this.agentJsapiTicketLock;
+  }
+
+  @Override
   public boolean isAgentJsapiTicketExpired() {
     return System.currentTimeMillis() > this.agentJsapiTicketExpiresTime;
   }

+ 17 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java

@@ -9,6 +9,8 @@ import redis.clients.jedis.JedisPool;
 import redis.clients.jedis.JedisPoolConfig;
 
 import java.io.File;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * <pre>
@@ -90,6 +92,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
   }
 
   @Override
+  public Lock getAccessTokenLock() {
+    return new ReentrantLock();
+  }
+
+  @Override
   public boolean isAccessTokenExpired() {
     try (Jedis jedis = this.jedisPool.getResource()) {
       String expiresTimeStr = jedis.get(ACCESS_TOKEN_EXPIRES_TIME_KEY);
@@ -133,6 +140,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
   }
 
   @Override
+  public Lock getJsapiTicketLock() {
+    return new ReentrantLock();
+  }
+
+  @Override
   public boolean isJsapiTicketExpired() {
     try (Jedis jedis = this.jedisPool.getResource()) {
       String expiresTimeStr = jedis.get(JS_API_TICKET_EXPIRES_TIME_KEY);
@@ -171,6 +183,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
   }
 
   @Override
+  public Lock getAgentJsapiTicketLock() {
+    return new ReentrantLock();
+  }
+
+  @Override
   public boolean isAgentJsapiTicketExpired() {
     try (Jedis jedis = this.jedisPool.getResource()) {
       String expiresTimeStr = jedis.get(String.format(AGENT_JSAPI_TICKET_EXPIRES_TIME_KEY, agentId));

+ 154 - 0
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedissonConfigImpl.java

@@ -0,0 +1,154 @@
+package me.chanjar.weixin.cp.config.impl;
+
+import lombok.NonNull;
+import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.redis.RedissonWxRedisOps;
+import me.chanjar.weixin.common.redis.WxRedisOps;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.api.RedissonClient;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * 基于Redisson的实现
+ *
+ * @author yuanqixun
+ * @date 2020/5/13
+ */
+public class WxCpRedissonConfigImpl extends WxCpDefaultConfigImpl {
+  protected final static String LOCK_KEY = "wechat_cp_lock:";
+  protected final static String CP_ACCESS_TOKEN_KEY = "wechat_cp_access_token_key:";
+  protected final static String CP_JSAPI_TICKET_KEY = "wechat_cp_jsapi_ticket_key:";
+  protected final static String CP_AGENT_JSAPI_TICKET_KEY = "wechat_cp_agent_jsapi_ticket_key:";
+
+  /**
+   * redis 存储的 key 的前缀,可为空
+   */
+  protected String keyPrefix;
+  protected String accessTokenKey;
+  protected String jsapiTicketKey;
+  protected String agentJsapiTicketKey;
+  protected String lockKey;
+
+  private final WxRedisOps redisOps;
+
+  public WxCpRedissonConfigImpl(@NonNull RedissonClient redissonClient, String keyPrefix) {
+    this(new RedissonWxRedisOps(redissonClient), keyPrefix);
+  }
+
+  public WxCpRedissonConfigImpl(@NonNull RedissonClient redissonClient) {
+    this(redissonClient, null);
+  }
+
+  private WxCpRedissonConfigImpl(@NonNull WxRedisOps redisOps, String keyPrefix) {
+    this.redisOps = redisOps;
+    this.keyPrefix = keyPrefix;
+  }
+
+  /**
+   * 设置企业微信自研应用ID(整数),同时初始化相关的redis key,注意要先调用setCorpId,再调用setAgentId
+   *
+   * @param agentId
+   */
+  @Override
+  public void setAgentId(Integer agentId) {
+    super.setAgentId(agentId);
+    String ukey = getCorpId().concat(":").concat(String.valueOf(agentId));
+    String prefix = StringUtils.isBlank(keyPrefix) ? "" :
+      (StringUtils.endsWith(keyPrefix, ":") ? keyPrefix : (keyPrefix + ":"));
+    lockKey = prefix + LOCK_KEY.concat(ukey);
+    accessTokenKey = prefix + CP_ACCESS_TOKEN_KEY.concat(ukey);
+    jsapiTicketKey = prefix + CP_JSAPI_TICKET_KEY.concat(ukey);
+    agentJsapiTicketKey = prefix + CP_AGENT_JSAPI_TICKET_KEY.concat(ukey);
+  }
+
+  protected Lock getLockByKey(String key) {
+    return redisOps.getLock(key);
+  }
+
+  @Override
+  public Lock getAccessTokenLock() {
+    return getLockByKey(this.lockKey.concat(":").concat("accessToken"));
+  }
+
+  @Override
+  public Lock getAgentJsapiTicketLock() {
+    return getLockByKey(this.lockKey.concat(":").concat("agentJsapiTicket"));
+
+  }
+
+  @Override
+  public Lock getJsapiTicketLock() {
+    return getLockByKey(this.lockKey.concat(":").concat("jsapiTicket"));
+  }
+
+  @Override
+  public String getAccessToken() {
+    return redisOps.getValue(this.accessTokenKey);
+  }
+
+  @Override
+  public boolean isAccessTokenExpired() {
+    Long expire = redisOps.getExpire(this.accessTokenKey);
+    return expire == null || expire < 2;
+  }
+
+  @Override
+  public void updateAccessToken(WxAccessToken accessToken) {
+    redisOps.setValue(this.accessTokenKey, accessToken.getAccessToken(), accessToken.getExpiresIn(), TimeUnit.SECONDS);
+  }
+
+  @Override
+  public void updateAccessToken(String accessToken, int expiresInSeconds) {
+    redisOps.setValue(this.accessTokenKey, accessToken, expiresInSeconds, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public void expireAccessToken() {
+    redisOps.expire(this.accessTokenKey, 0, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public String getJsapiTicket() {
+    return redisOps.getValue(this.jsapiTicketKey);
+  }
+
+  @Override
+  public boolean isJsapiTicketExpired() {
+    Long expire = redisOps.getExpire(this.jsapiTicketKey);
+    return expire == null || expire < 2;
+  }
+
+  @Override
+  public void expireJsapiTicket() {
+    redisOps.expire(this.jsapiTicketKey, 0, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) {
+    redisOps.setValue(this.jsapiTicketKey, jsapiTicket, expiresInSeconds, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public void expireAgentJsapiTicket() {
+    redisOps.expire(this.agentJsapiTicketKey, 0, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public void updateAgentJsapiTicket(String agentJsapiTicket, int expiresInSeconds) {
+    redisOps.setValue(this.agentJsapiTicketKey, agentJsapiTicket, expiresInSeconds, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public String getAgentJsapiTicket() {
+    return redisOps.getValue(this.agentJsapiTicketKey);
+  }
+
+  @Override
+  public boolean isAgentJsapiTicketExpired() {
+    Long expire = redisOps.getExpire(this.agentJsapiTicketKey);
+    return expire == null || expire < 2;
+  }
+
+}

+ 0 - 3
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/config/impl/WxMaRedissonConfigImpl.java

@@ -26,9 +26,6 @@ public class WxMaRedissonConfigImpl extends WxMaDefaultConfigImpl {
   /**
    * redis 存储的 key 的前缀,可为空
    */
-  /**
-   * redis 存储的 key 的前缀,可为空
-   */
   protected String keyPrefix;
   protected String accessTokenKey;
   protected String jsapiTicketKey;