通过WxJava获取微信AccessToken出现acquire timeouted问题

图片来自pixabay.com的Alexas_Fotos会员

WxJava是一个目前应用广泛的微信SDK工具开发包,本文记录一个WxJava获取微信AccessToken失败出现acquire timeouted的问题。

1. 故障现象

最近产线出现了一个严重的技术故障问题,用户无法登录长达1个多小时,通过查看日志发现和WxJava相关,在故障期间内,WxJava一直无法正常获取微信Access Token,报acquire timeouted的错误,见下图。

期间尝试重启两次,在19:20第一次重启应用后问题有所减弱,但是过了5分钟又重新大量发生,直到第二次重启应用后问题消失。

2. 问题定位

通过查看错误日志的堆栈信息,

java.lang.RuntimeException: acquire timeouted
    at cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl$DistributedLock.lock(WxMaRedisConfigImpl.java:332)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.getAccessToken(WxMaServiceImpl.java:110)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.executeInternal(WxMaServiceImpl.java:249)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.execute(WxMaServiceImpl.java:215)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.get(WxMaServiceImpl.java:199)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.jsCode2SessionInfo(WxMaServiceImpl.java:178)
    at cn.binarywang.wx.miniapp.api.impl.WxMaUserServiceImpl.getSessionInfo(WxMaUserServiceImpl.java:28)

可以看到和WxJava通过分布式锁获取token有关,产线用的WxJava代码版本是3.6.0,之前已经稳定运行了一年多。

下载相应版本WxJava代码,找到报错的代码行,

// 类文件 - cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl

public class WxMaServiceImpl implements WxMaService {

  public String getAccessToken(boolean forceRefresh) throws WxErrorException {
    if (!this.getWxMaConfig().isAccessTokenExpired() && !forceRefresh) {
      return this.getWxMaConfig().getAccessToken();
    }

    Lock lock = this.getWxMaConfig().getAccessTokenLock();
    lock.lock(); // <-- 问题发生代码行 WxMaServiceImpl.java:110
    try {
      String url = String.format(WxMaService.GET_ACCESS_TOKEN_URL, this.getWxMaConfig().getAppid(),
        this.getWxMaConfig().getSecret());
      try {
        HttpGet httpGet = new HttpGet(url);
        if (this.getRequestHttpProxy() != null) {
          RequestConfig config = RequestConfig.custom().setProxy(this.getRequestHttpProxy()).build();
          httpGet.setConfig(config);
        }
        try (CloseableHttpResponse response = getRequestHttpClient().execute(httpGet)) {
          String resultContent = new BasicResponseHandler().handleResponse(response);
          WxError error = WxError.fromJson(resultContent, WxType.MiniApp);
          if (error.getErrorCode() != 0) {
            throw new WxErrorException(error);
          }
          WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
          this.getWxMaConfig().updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());

          return this.getWxMaConfig().getAccessToken();
        } finally {
          httpGet.releaseConnection();
        }
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    } finally {
      lock.unlock();
    }

  }
}

// 类文件 - cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl$DistributedLock

public class WxMaRedisConfigImpl implements WxMaConfig {

  private class DistributedLock implements Lock {

    private JedisLock lock;

    private DistributedLock(String key) {
      this.lock = new JedisLock(getRedisKey(key));
    }

    @Override
    public void lock() {
      try (Jedis jedis = jedisPool.getResource()) {
        if (!lock.acquire(jedis)) {
          throw new RuntimeException("acquire timeouted"); // <-- 问题发生代码行 WxMaRedisConfigImpl.java:332
        }
      } catch (InterruptedException e) {
        throw new RuntimeException("lock failed", e);
      }
    }

    // ...
  }
}

上面的代码主要是通过redis分布式锁,使得只有一个进程中的一个线程执行token获取并存储到redis,梳理getAccessToken()方法的执行路径,可以发现其大概经历的步骤如下,

若仔细研读这个流程图,可以发现其中有很多相互抢占竞争资源的问题,其中有竞争的资源有3处,

  1. DistributedLock.lock():分布式锁,多个进程/线程抢占这个锁,只有获取到此锁的线程才能执行锁更新操作。
  2. jedisPool.getResource():单进程中多个线程竞争从连接池获取redis连接。
  3. JedisLock.acquire()和JedisLock.release():两个方法被synchronized修饰,这个是一个比较隐蔽的同步琐,被单进程中的多个线程所竞争抢占。

Java字节码增强技术Bytebuddy探路篇二(NoClassDefFoundError问题)

图片来自pixabay.com的NickRivers会员

最近为了实现Java应用RPC调用的录制和Mock回放,需要以无侵入方式获取到RPC方法的出入参数和返回响应消息等数据,于是踏上了Java字节码增强技术的道路摸索。技术路径上选择了Java Agent + Bytebuddy框架,但是在应用实践过程中,对tomcat/dubbo/rocketmq进行类切面增强时出现了NoClassDefFoundError的问题,本文针对这个关键问题的出现原因进行分析讨论,提供相应的解决思路。

1. 演示代码工程

为了方便讨论,请先下载演示代码工程,整个工程结构如下,

+ phantom-core 演示基础类。
+ phantom-demo 演示web应用,这是一个简单的sprint boot应用,运行在端口18080。
+ phantom-agent 一个Java agent,里面通过ByteBuddy对指定类以无侵入方式切面增强,在演示工程中主要对Tomcat web的请求进行切面处理。
+ phantom-agent-plugin 一个增强类插件,这个是为了解决NoClassDefFoundError问题而提供的一个插件解决方案,应用于phantom-agent的TransformerV3.tranform()中,项目构建后需要复制构建包到路径/tmp/phantom-agent-plugin.jar上。

下载后,分别对项目进行构建、启动和测试,命令如下,

# 使用mvn工具编译构建
mvn clean package

1. 启动演示web应用
java -jar ./phantom-demo/target/phantom-demo.jar

1. 测试web请求命令
curl -X POST http://127.0.0.1:18080/api/hello

若上面的web应用启动成功,则测试web请求将会收到“hello,world”的消息响应,这个简单的测试请求已经走过web服务的完整路径。工程中的Transformer将通过java agent方式对tomcat web请求进行切面处理,获取所有http请求的执行前、执行后、执行异常相关情况并打印到日志中,tomcat切点定义为,

# 切点:tomcat核心类StandardHostValve的invoke方法
org.apache.catalina.core.StandardHostValve.invoke(Request request, Response response);

演示工程中Transformer类有三个,

  1. com.phantom.agent.trace.TransformerV1 :用于演示问题的复现和定位
  2. com.phantom.agent.trace.TransformerV2 :用于演示问题的解决思路1
  3. com.phantom.agent.trace.TransformerV3 :用于演示问题的解决思路2

前端页面在运行过程中出现ElementPlusError错误,报[ElOnlyChild] no valid child node found错误消息

本文记录一个前端页面开发出现的问题,涉及的前端框架版本为,

  • “vue”: “^3.2.22”
  • “element-plus”: “^2.0.4”

1. 问题

近期在使用vue 3 + element plus进行前端页面开发,在前端页面运行过程浏览器后台中出现如下错误消息,

error.ts:12 ElementPlusError: [ElOnlyChild] no valid child node found
    at debugWarn (error.ts:12:18)
    at Proxy.<anonymous> (only-child.ts:35:9)
    at renderComponentRoot (runtime-core.esm-bundler.js:893:44)
    at ReactiveEffect.componentUpdateFn [as fn] (runtime-core.esm-bundler.js:5098:34)
    at ReactiveEffect.run (reactivity.esm-bundler.js:167:25)
    at updateComponent (runtime-core.esm-bundler.js:4968:26)
    at processComponent (runtime-core.esm-bundler.js:4901:13)
    at patch (runtime-core.esm-bundler.js:4489:21)
    at ReactiveEffect.componentUpdateFn [as fn] (runtime-core.esm-bundler.js:5107:17)
    at ReactiveEffect.run (reactivity.esm-bundler.js:167:25)

出现这个错误消息概率比较高,但不是每次都必现。页面上各个组件按钮都运行正常,出现这个问题感觉有些纳闷,上面的错误消息也没有直接给出哪里不对,一时不知道问题在哪里。

2. 定位

从错误消息[ElOnlyChild] no valid child node found来看,报的是子组件无效,估计是什么组件使用的不当,没有映射到相应的模型数据,是不是哪里变量命名有拼写错误,使用Ctrl+F检查了一番代码,没有发现任何变量拼写错误。

为了发现问题,开始对页面组件进行二分法排查,把页面组件逐一过滤,定位问题出现在如下一个气泡确认框组件上,

<el-popconfirm title="确定要删除吗?" @confirm="deleteConfig(row)">
   <template #reference>
       <el-button type="text" v-if="row.version != 'new'">删除</el-button>
   </template>
</el-popconfirm>

这个组件没有什么特别,之前已经在很多地方用的很好,唯一不同的是添加了v-if控制,稍微判断了一下,应该就是这个v-if导致。

若把上面的el-button去掉,则直接可以复现这个问题,

<el-popconfirm title="确定要删除吗?" @confirm="deleteConfig(row)">
   <template #reference>
   </template>
</el-popconfirm>

气泡确认框组件的用途,主要是在点击某个元素弹出一个简单的气泡确认框,其需要绑定在一个子组件上,若子组件为空,则会报出如下的错误信息,

[ElOnlyChild] no valid child node found...

在了解到如上的问题后,将v-if移到el-popconfirm组件,正确的书写方法为,

<el-popconfirm title="确定要删除吗?" v-if="row.version != 'new'" @confirm="deleteConfig(row)">
   <template #reference>
       <el-button type="text">删除</el-button>
   </template>
</el-popconfirm>

至此问题解决。