使用|使用 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 。
所以说主要检查第一种情况是否发生,这个时候可以再去看一下该方法的第一行代码
?
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=https://www.it610.com/article/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 方法。
org.apache.maven.plugins
maven-jar-plugin
2.3.1
true
com.tom.mdc.AgentMain
true
true
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();
}
}
}
}
}
?
【使用|使用 Arthas 排查开源 Excel 组件问题】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);
}
}
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。
推荐阅读
- 由浅入深理解AOP
- 【译】20个更有效地使用谷歌搜索的技巧
- mybatisplus如何在xml的连表查询中使用queryWrapper
- MybatisPlus|MybatisPlus LambdaQueryWrapper使用int默认值的坑及解决
- MybatisPlus使用queryWrapper如何实现复杂查询
- iOS中的Block
- Linux下面如何查看tomcat已经使用多少线程
- 使用composer自动加载类文件
- android|android studio中ndk的使用
- 使用协程爬取网页,计算网页数据大小