使用 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文件的工具,提供多种启 ...
随机推荐
- 【转】git-flow 的工作流程
git-flow 的工作流程 当在团队开发中使用版本控制系统时,商定一个统一的工作流程是至关重要的.Git 的确可以在各个方面做很多事情,然而,如果在你的团队中还没有能形成一个特定有效的工作流程,那么 ...
- c基础-指针、函数与预处理器
指针.函数.预处理器 目录 指针.函数.预处理器 1.指针 解引用 指针运算 数组和指针 const char *, char const *, char * const,char const * c ...
- 记录--让URL地址都变成了"ooooooooo"
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 发现一个很有创意的小工具网站,如封面图所示功能很简单,就是将一个URL地址转换为都是 ooooooooo 的样子,通过转换后的地址访问可以 ...
- 一文搞懂JavaEE的接口
在Java EE(Java Enterprise Edition)开发环境中,编程意义上的"接口"(interface)和API接口虽然都涉及接口的概念,但它们属于不同层面的术语. ...
- C++设计模式 - 模板方法(Template Method)
组件协作模式: 现代软件专业分工之后的第一个结果是"框架与应用程序的划分","组件协作"模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用 ...
- #轮廓线dp,模型转换#洛谷 3226 [HNOI2012]集合选数
题目 问有多少个集合 \(S\) 是 \([1,n]\) 的子集, 并且 \(\forall a,b\in S,a|b\),满足 \(\frac{b}{a}\neq \{2,3\}\) 分析 可以发现 ...
- 开发人员使用HANA交付业务的学习路径
本文于2019年7月22日完成,发布在个人博客网站上. 考虑个人博客因某种原因无法修复,于是在博客园安家,之前发布的文章逐步搬迁过来. 入门 编程规范. 开发环境使用方法. 基本语法,与其它同类软件的 ...
- 可能有人听过ThreadLocal,但一定没人听过ThreadLocal对象池
目录 简介 ThreadLocal ThreadLocalMap Recycler 总结 简介 JDK中的Thread大家肯定用过,只要是用过异步编程的同学肯定都熟悉.为了保存Thread中特有的变量 ...
- 深入理解 Spring IoC 和 DI:掌握控制反转和依赖注入的精髓
在本文中,我们将介绍 IoC(控制反转)和 DI(依赖注入)的概念,以及如何在 Spring 框架中实现它们. 什么是控制反转? 控制反转是软件工程中的一个原则,它将对象或程序的某些部分的控制权转移给 ...
- Python 布尔类型
布尔值表示两个值之一:True(真)或False(假). 布尔值 在编程中,您经常需要知道一个表达式是否为True或False. 您可以在Python中评估任何表达式,并获得两个答案之一:True或F ...