使用 Arthas 排查开源 Excel 组件问题
简介: 有了实际的使用之后,不免会想到,Arthas 是如何做到在程序运行时,动态监测我们的代码的呢?带着这样的问题,我们一起来看下 Java Agent 技术实现原理。

背景介绍
项目中有使用到 com.github.dreamroute excel-helper 这个工具来辅助 Excel 文件的解析,出错时的代码是这样写的:如下所示(非源代码)
try { excelDTOS = ExcelHelper.importFromFile(ExcelType.XLSX, file, ExcelDTO.class); } catch (Exception e) { log.error("ExcelHelper importFromFile exception msg {}", e.getMessage()); }
因为打印异常信息时,使用了 e.getMessage() 方法,没有将异常信息打印出来。而且本地复现也没有复现出来。所以只能考虑使用 arthas 来协助排查这个问题了。
排查过程
1、线上服务器安装 Arthas。
https://arthas.aliyun.com/doc/install-detail.html
2、使用 watch 命令监控指定方法,打印出异常的堆栈信息,命令如下:
watch com.github.dreamroute.excel.helper.ExcelHelper importFromFile '{params,throwExp}' -e -x 3
再次调用方法,捕获到异常栈信息如下:
已经捕获到异常,并打印出堆栈信息。
3、根据对应的堆栈信息,定位到具体的代码,如下:

代码很简单,从代码中可以很清晰的看到如果没有从 headerInfoMap 中没有获取到指定的 headerInfo ,就会抛这个异常。没有找到只有两种情况:
- headerInfoMap 中保存的信息不对。
- cell 中的 columnIndex 超出的正常的范围导致没有获取到对应 HeaderInfo 。
对于第二种情况,首先去校验了一下上传的 Excel 文件是否有问题,本地测试了一下 Excel 文件,没有任何问题。本地测试也是成功的,所以主观判断,第二种情况的可能性不大。
所以说主要检查第一种情况是否发生,这个时候可以再去看一下该方法的第一行代码
MapheaderInfoMap = processHeaderInfo(rows,cls);
可以看到headerInfoMap是通过processHeaderInfo中获取的。找到processHeaderInfo 的代码,如下所示。
public static MapproceeHeaderInfo(Iteratorrows, Class cls) {
if (rows.hasNext()) {
Row header = rows.next();
return CacheFactory.findHeaderInfo(cls, header);
}
return new HashMap<>(0);
}
public static MapfindHeaderInfo(Class cls, Row header) {
MapheaderInfo = HEADER_INFO.get(cls);
if (MapUtils.isEmpty(headerInfo)) {
headerInfo = ClassAssistant.getHeaderInfo(cls, header);
HEADER_INFO.put(cls, headerInfo);
}
return headerInfo;
}
public static MapgetHeaderInfo(Class cls, Row header) {
IteratorcellIterator = header.cellIterator();
Listfields = ClassAssistant.getAllFields(cls);
MapheaderInfo = new HashMap<>(fields.size());
while (cellIterator.hasNext()) {
org.apache.poi.ss.usermodel.Cell cell = cellIterator.next();
String headerName = cell.getStringCellValue();
for (Field field : fields) {
Column col = field.getAnnotation(Column.class);
String name = col.name();
if (Objects.equals(headerName, name)) {
HeaderInfo hi = new HeaderInfo(col.cellType(), field);
headerInfo.put(cell.getColumnIndex(), hi);
break;
}
}
}
return headerInfo;
}
主要通过 CacheFactory 类的 findHeaderInfo 来生成,在 findHeaderInfo 方法中,通过一个被 static final 修饰的 HEADER_INFO 变量来做缓存,被调用时先去HEADER_INFO 中查,如果有则直接返回,没有则重新创建(也就说明相同的 Excel 文件,仅初始化一次 HeaderInfo )。创建的步骤在 ClassAssistant.getHeaderInfo() 方法中。
简单的看一下 HeaderInfo 的生成过程,根据 Excel 文件的第一行中的各个 Cell 值与自定义实体类的注解比较,如果名字相同,就存为一个键值对( HeaderInfo 的数据结构为 HashMap )。
4、这个时候需要再确认一下 HEADER_INFO 中保存的 ExcelDTO.class 相关的 HeaderInfo 是怎样的。通过 ognl 命令或者 getstatic 命令来查看。这里使用 ognl 命令。
ognl '#value=new com.tom.dto.ExcelDTO(),#valueMap=@com.github.dreamroute.excel.helper.cache.CacheFactory@HEADER_INFO,#valueMap.get(#value.getClass()).entrySet().iterator.{#this.value.name}'
结果如下:正常情况下这个 Excel 文件有 6 列信息,为什么只产生了 4 个键值对呢?如果 HEADER_INFO 中保存了错的,从上面的逻辑来看,后面上传的正确的 Excel 文件在解析时都会抛错。

5、询问了当时发现这个问题的同事,得知他第一次上传的 Excel 文件是有问题的,后面想改正,再上传时便出现了问题。到这里问题也算是找到了。
Arthas 原理探究
有了实际的使用之后,不免会想到,Arthas 是如何做到在程序运行时,动态监测我们的代码的呢?带着这样的问题,我们一起来看下 Java Agent 技术实现原理。
Java Agent 技术
Agent 是一个运行在目标 JVM 的特定程序,它的职责是负责从目标 JVM 中获取数据,然后将数据传递给外部进程。加载 Agent 的时机可以是目标 JVM 启动之时,也可以是在目标 JVM 运行时进行加载,而在目标 JVM 运行时进行 Agent 加载具备动态性。
基础概念
- JVMTI(JVM Tool Interface):是 JVM 暴露出来的一些供用户扩展的接口集合,JVMTI 是基于事件驱动的,JVM 每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。
- JVMTIAgent(JVM Tool Interface):是一个动态库,利用 JVMTI 暴露出来的一些接口帮助我们在程序启动时或程序运行时 JVM Attach 机制,将 Agent 加载到目标 JVM 中。
- JPLISAgent(Java Programming Language Instrumentation Services Agent):它的作用是初始化所有通过 Java Instrumentation API 编写的 Agent,并且也承担着通过 JVMTI 实现 Java Instrumentation 中暴露 API 的责任。
- VirtualMachine :提供了Attach 动作和 Detach 动作,允许我们通过 attach 方法,远程连接到 JVM 上,然后通过 loadAgent 方法向 JVM 注册一个代理程序 agent ,在该 agent 的代理程序中会得到一个 Instrumentation 实例,该实例可以在 class 加载前改变 class 的字节码,也可以在 class 加载后重新加载。
- Instrumentation:可以在 class 加载前改变 class 的字节码(premain),也可以在 class 加载后重新加载(agentmain)。
执行过程

动手写一个 Demo
通过 javassist,在运行时更改指定方法的代码,在方法之前后添加自定义逻辑。
1、定义 Agent 类。当前 Java 提供了两种方式可以将代码代码注入到 JVM 中,这里我们的 Demo 选择使用 agentmain 方法来实现。
premain:在启动时通过 javaagent 命令,将代理注入到指定的 JVM 中。
agentmain:运行时通过 attach 工具激活指定代理。
/**
* AgentMain
*
* @author tomxin
*/
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation instrumentation) throws UnmodifiableClassException, ClassNotFoundException {
instrumentation.addTransformer(new InterceptorTransformer(agentArgs), true);
Class clazz = Class.forName(agentArgs.split(",")[1]);
instrumentation.retransformClasses(clazz);
}
}
/**
* InterceptorTransformer
*
* @author tomxin
*/
public class InterceptorTransformer implements ClassFileTransformer {
private String agentArgs;
public InterceptorTransformer(String agentArgs) {
this.agentArgs = agentArgs;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//javassist的包名是用点分割的,需要转换下
if (className != null && className.indexOf("/") != -1) {
className = className.replaceAll("/", ".");
}
try {
//通过包名获取类文件
CtClass cc = ClassPool.getDefault().get(className);
//获得指定方法名的方法
CtMethod m = cc.getDeclaredMethod(agentArgs.split(",")[2]);
//在方法执行前插入代码
m.insertBefore("{ System.out.println(\"=========开始执行=========\"); }");
m.insertAfter("{ System.out.println(\"=========结束执行=========\"); }");
return cc.toBytecode();
} catch (Exception e) {
}
return null;
}
}
2、使用 Maven 配置 MANIFEST.MF 文件,该文件能够指定 Jar 包的 main 方法。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.1</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Agent-Class>com.tom.mdc.AgentMain</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
3、定义 Attach 方法,通过 VirtualMachine.attach(#{pid}) 来指定要代理的类。
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
/**
* AttachMain
*
* @author tomxin
*/
public class AttachMain {
public static void main(String[] args) {
VirtualMachine virtualMachine = null;
try {
virtualMachine = VirtualMachine.attach(args[0]);
// 将打包好的Jar包,添加到指定的JVM进程中。
virtualMachine.loadAgent("target/agent-demo-1.0-SNAPSHOT.jar",String.join(",", args));
} catch (Exception e) {
if (virtualMachine != null) {
try {
virtualMachine.detach();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
}
4、定义测试的方法
package com.tom.mdc;
import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* PrintParamTarget
*
* @author toxmxin
*/
public class PrintParamTarget {
public static void main(String[] args) {
// 打印当前进程ID
System.out.println(ManagementFactory.getRuntimeMXBean().getName());
Random random = new Random();
while (true) {
int sleepTime = 5 + random.nextInt(5);
running(sleepTime);
}
}
private static void running(int sleepTime) {
try {
TimeUnit.SECONDS.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("running sleep time " + sleepTime);
}
}
原文链接
本文为阿里云原创内容,未经允许不得转载。
使用 Arthas 排查开源 Excel 组件问题的更多相关文章
- 【目录】C#操作Excel组件Spire.XLS系列文章目录
本博客所有文章分类的总目录链接:http://www.cnblogs.com/asxinyu/p/4288836.html 1.C#操作Excel组件Spire.XLS文章目录 1.[原创].NET读 ...
- Net Core开源通讯组件 SmartRoute
Net Core开源通讯组件 SmartRoute(服务即集群) SmartRoute是基于Dotnet Core设计的可运行在linux和windows下的服务通讯组件,其设计理念是去中心化和零配置 ...
- 开源通讯组件ec
跨平台开源通讯组件elastic communication elastic communication是基于c#开发支持.net和mono的通讯组件(简称EC),EC的主要目的简化mono和.net ...
- 解决在IIS中调用Microsoft Office Excel组件后进程无法正常退出的问题
来源:http://www.cnblogs.com/ahui/archive/2013/03/05/2944441.html 有一个项目用到Excel组件产生报表,本以为这个通用功能是个很简单的cas ...
- .NET开源Protobuf-net组件修炼手册
一.前言 Protocol Buffer(简称Protobuf或PB) 是一个跨平台的消息交互协议,类似xml.json等 :别只会用Json和XML了,快来看看Google出品的Protocol B ...
- .NET开源Protobuf-net组件葵花手册
一.前言 我们都知道 protobuf是由Google开发的一款与平台无关,语言无关,可扩展的序列化结构数据格式,可用做数据存储格式, 通信协议 ! 在前面<.NET开源Protobuf-net ...
- NPOI读写Excel组件封装Excel导入导出组件
后台管理系统多数情况会与Excel打交道,常见的就是Excel的导入导出,对于Excel的操作往往是繁琐且容易出错的,对于后台系统的导入导出交互过程往往是固定的,对于这部分操作,我们可以抽离出公共组件 ...
- 鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件
目录: 1.前言 2.背景 3.效果展示 4.Sample解析 5.Library解析 6.<鸿蒙开源第三方组件>文章合集 前言 基于安卓平台的SlidingMenu侧滑菜单组件(http ...
- 【全网首发】鸿蒙开源三方组件--强大的弹窗库XPopup组件
目录: 1.介绍 2.效果一览 3.依赖 4.如何使用 5.下载链接 6.<鸿蒙开源三方组件>文章合集 1. 介绍 XPopup是一个弹窗库,可能是Harmony平台最好的弹窗库.它从 ...
- 开源MyBatisGenerator组件源码分析
开源MyBatisGenerator组件源码分析 看源码前,先了解Generator能做什么? MyBatisGenerator是用来生成mybatis的Mapper接口和xml文件的工具,提供多种启 ...
随机推荐
- 公开的Webservice集锦
备注:以下所有的来自 互联网,版权归原作者所有 股票行情数据 WEB 服务(支持香港.深圳.上海基金.债券和股票:支持多股票同时查询) Endpoint: http://webservice.webx ...
- MediaCodec 低延时解码
介绍 我们在使用Android的硬解进行解码时,如果是Android11以上则可以使用其特性低延迟,谷歌官方文档 以下是Android 11支持的低时延特性: ANGLE支持:Android 11引入 ...
- java 手动生成jni头文件(JNI静态注册)
PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明 本文作为本人csdn blog的主站的备份.(Bl ...
- LocalDate获取指定月的上个月的第一天和最后一天
LocalDate date = LocalDate.of(2022, 8, 30); LocalDate lastMonth = date.minusMonths(1); // 当前月份减1 Loc ...
- Mac M芯片使用PD安装centos7无页面安装
1.选择Centos镜像 点击继续 设置虚拟机名称: 点击创建 : 选择第一个回车开始下载系统,下载完成进入设置页面,首先输入 1 设置语言: 进入语言设置,选择77普通话: 选择c继续,又回到系统配 ...
- KingbaesES数据库查看阻止vacuum的常用语句
KingbaesES数据库中遇到表膨胀,有时碰到vacuum失效的场景.最常见的原因如下. 以下测试基于有关analyze和autovacuum的默认阈值触发条件. 1.autovacuum是否运行, ...
- 分享本人依照NOI大纲整理的CSPJ复习资料
2023 CSP-J 复习文件 考纲 复习好 : 基础知识与编程环境 难度 计算机的基本构成 计算机的组成及功能 - 知乎 (zhihu.com) 操作系统的基本概念与常见操作 操作系统基础知识大汇总 ...
- WPF实现树形表格控件(TreeListView)
前言 本文将探讨如何利用WPF框架实现树形表格控件,该控件不仅能够有效地展示复杂的层级数据,还能够提供丰富的个性化定制选项.我们将介绍如何使用WPF提供的控件.模板.布局.数据绑定等技术来构建这样一个 ...
- 动图演示步骤 Vmware安装Centos-7 最小安装/图形化界面及常见错误参考,基础配置推荐
程序软件工具安装篇 --[Linux](Vmware/Centos-7) 目录 程序软件工具安装篇 --[Linux](Vmware/Centos-7) ①:文件准备工作 虚拟机工具安装文件 系统镜像 ...
- #网络流,分层图#洛谷 4400 [JSOI2008] Blue Mary的旅行
题目 分析 考虑答案一定最大不超过\(n\),那么可以建分层图, 若当前最大流等于\(n\),直接输出枚举的天数 \((x,x')\)容量为\(inf\),\((x,y')\)容量为一个航班最多的票数 ...