使用|使用 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=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); } }

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

    推荐阅读