当前位置:主页 > 查看内容

使用 Arthas 排查开源 Excel 组件问题

发布时间:2021-06-29 00:00| 位朋友查看

简介:? 背景介绍 ? 项目中有使用到 com.github.dreamroute excel-helper 这个工具来辅助 Excel 文件的解析,出错时的代码是这样写的:如下所示(非源代码) try { excelDTOS = ExcelHelper.importFromFile(ExcelType.XLSX, file, ExcelDTO.class); } catch (Excepti……

image.png
?

背景介绍

?

项目中有使用到 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

再次调用方法,捕获到异常栈信息如下:
image.png

已经捕获到异常,并打印出堆栈信息。
?

3、根据对应的堆栈信息,定位到具体的代码,如下:

image.png

代码很简单,从代码中可以很清晰的看到如果没有从 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 文件在解析时都会抛错。

image.png

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)。

?

执行过程

?

image.png

动手写一个 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);
}

本文转自网络,原文链接:https://developer.aliyun.com/article/784923
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文

  • 周排行
  • 月排行
  • 总排行

随机推荐