当前位置: 首页 > 科技观察

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

时间:2023-03-13 01:45:57 科技观察

背景介绍项目中使用com.github.dreamrouteexcel-helper来辅助解析Excel文件。出错时的代码如下(非源代码)try{excelDTOS=ExcelHelper.importFromFile(ExcelType.XLSX,file,ExcelDTO.class);}catch(Exceptione){log.error("ExcelHelperimportFromFileexceptionmsg{}",e.getMessage());}因为在打印异常信息的时候,使用了e.getMessage()方法,但是没有打印出异常信息。并且本地复制还没有复制。所以只能考虑使用arthas来帮助排查这个问题。排错过程1.在线上服务器上安装Arthas。https://arthas.aliyun.com/doc/install-detail.html2。使用watch命令监视指定的方法,并打印出异常的堆栈信息。命令如下:watchcom.github.dreamroute.excel.helper.ExcelHelperimportFromFile'{params,throwExp}'-e-x3再次调用该方法,捕获异常的堆栈信息如下:已经被捕获并打印出堆栈信息。3、根据对应的栈信息定位到具体的代码,如下:代码很简单,从代码中可以清楚的看出,如果不是从headerInfoMap中获取到指定的headerInfo,就会抛出这个异常。如果没有找到,只有两种情况:headerInfoMap中保存的信息不正确。单元格中的columnIndex超出正常范围,所以没有获取到对应的HeaderInfo。对于第二种情况,首先检查上传的Excel文件是否有问题,在本地测试Excel文件,没有问题。本地测试也成功了,所以主观判断,第二种情况的可能性不大。所以主要是检查是否出现第一种情况。这时候可以看第一行代码MapheaderInfoMap=processHeaderInfo(rows,cls);可以看到headerInfoMap是通过processHeaderInfo得到的。找到processHeaderInfo的代码,如下所示。publicstaticMapproceeHeaderInfo(Iteratorrows,Classcls){if(rows.hasNext()){Rowheader=rows.next();returnCacheFactory.findHeaderInfo(cls,header);}returnnewHashMap<>(0);}publicstaticMapfindHeaderInfo(Classcls,Rowheader){MapheaderInfo=HEADER_INFO.get(cls);if(MapUtils.isEmpty(headerInfo)){headerInfo=ClassAssistant.getHeaderInfo(cls,header);HEADER_INFO.put(cls,headerInfo);}returnheaderInfo;}publicstaticMapgetHeaderInfo(Classcls,Rowheader){IteratorcellIterator=header.cellIterator();Listfields=ClassAssistant.getAllFields(cls);MapheaderInfo=newHashMap<>(fields.size());while(cellIterator.hasNext()){org.apache.poi.ss.usermodel.Cellcell=cellIterator.next();StringheaderName=cell.getStringCellValue();for(Fieldfield:fields){Columncol=field.getAnnotation(Column.class);Stringname=col.name();if(Objects.equals(headerName,name)){HeaderInfohi=newHeaderInfo(col.cellType(),field);headerInfo.put(cell.getColumnIndex(),hi);break;}}}returnheaderInfo;}主要通过CacheF生成actor类的findHeaderInfo。在findHeaderInfo方法中,使用了一个staticfinal修饰的HEADER_INFO变量作为缓存。调用时,首先检查HEADER_INFO。Excel文件,只初始化一次HeaderInfo)创建步骤在ClassAssistant.getHeaderInfo()方法中。简单看一下HeaderInfo的生成过程。根据Excel文件第一行每个Cell的值与自定义实体类的注解进行比较,如果名称相同,则存储为键值对(HeaderInfo的数据结构为HashMap).4、此时需要确认HEADER_INFO中保存的ExcelDTO.class相关的HeaderInfo。使用ognl命令或getstatic命令查看。这里使用ognl命令。ognl'#value=newcom.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是如何在程序运行时动态监控我们的代码的?带着这样的疑问,我们来看看JavaAgent技术的实现原理。JavaAgent技术Agent是运行在目标JVM上的具体程序,它的职责是从目标JVM中获取数据,然后将数据传递给外部进程。加载Agent的时机可以是在目标JVM启动时加载,也可以是在目标JVM运行时加载,在目标JVM运行时加载Agent是动态的。基本概念JVMTI(JVMToolInterface):是JVM暴露给用户扩展的一组接口。JVMTI是事件驱动的。JVM每次执行某个逻辑时,都会调用一些事件回调接口(如果有的话),开发者可以使用这些接口来扩展自己的逻辑。JVMTIAgent(JVMToolInterface):是一个动态库,它使用JVMTI暴露的一些接口,帮助我们在程序启动或程序运行时,通过JVMAttach机制将Agent加载到目标JVM中。JPLISAgent(JavaProgrammingLanguageInstrumentationServicesAgent):它的作用是初始化所有通过JavaInstrumentationAPI编写的Agent,同时也承担着通过JVMTI实现JavaInstrumentation中暴露的API的责任。VirtualMachine:提供Attach动作和Detach动作,让我们通过attach方法远程连接到JVM,然后通过loadAgent方法向JVM注册一个agent代理,在agent代理中会得到一个Instrumentation实例代理,可以在类加载前改变类的字节码,也可以在类加载后重新加载。Instrumentation:类的字节码可以在类加载前改变(premain),也可以在类加载后重新加载(agentmain)。在执行过程中,通过javassist手写一个Demo,在运行时更改指定方法的代码,在方法前后添加自定义逻辑。1.定义代理类。目前Java提供了两种向JVM注入代码的方式,这里我们的Demo选择使用agentmain方法来实现。premain:在启动时通过javaagent命令将agent注入到指定的JVM中。agentmain:运行时通过attach工具激活指定的agent。/***AgentMain**@authortomxin*/publicclassAgentMain{publicstaticvoidagentmain(StringagentArgs,Instrumentationinstrumentation)throwsUnmodifiableClassException,ClassNotFoundException{instrumentation.addTransformer(newInterceptorTransformer(agentArgs),true);Classclazz=Class.forName("itArgs,"spl)1]);instrumentation.retransformClasses(clazz);}}/***InterceptorTransformer**@authortomxin*/publicclassInterceptorTransformerimplementsClassFileTransformer{privateStringagentArgs;publicInterceptorTransformer(StringagentArgs){this.agentArgs=agentArgs;}@Overridepublicbyte[]transform,ClassLoader,ClassclassBeingRedefined,ProtectionDomainprotectionDomain,byte[]classfileBuffer)throwsIllegalClassFormatException{//javassist包名用点分隔,需要转换if(className!=null&&className.indexOf("/")!=-1){className=className.replaceAll("/",".");}try{//通过包名获取class文件CtClasscc=ClassPool.getDefault().get(className);//获取指令判断方法名的方法CtMethodm=cc.getDeclaredMethod(agentArgs.split(",")[2]);//在方法执行前插入代码m.insertBefore("{System.out.println(\"=========开始执行==========\");}");m.insertAfter("{System.out.println(\"=========EndExecution==========\");}");returncc.toBytecode();}catch(Exceptione){}returnnull;}}2.使用Maven配置MANIFEST.MF文件,可以指定Jar包的main方法org.apache.maven.pluginsmaven-jar-plugin2.3.1<配置>truecom.tom.mdc.AgentMaintruetrue3.定义Attach方法,通过VirtualMachine.attach(#{pid})指定要代理的类。importcom.sun.tools.attach.VirtualMachine;importjava.io.IOException;/***AttachMain**@authortomxin*/publicclassAttachMain{publicstaticvoidmain(String[]args){VirtualMachinevirtualMachine=null;try{virtualMachine=VirtualMachine.attach(args[0]);//将打包好的Jar包添加到指定的JVM进程中。virtualMachine.loadAgent("target/agent-demo-1.0-SNAPSHOT.jar",String.join(",",args));}catch(Exceptione){if(virtualMachine!=null){try{virtualMachine.detach();}catch(IOExceptionex){ex.printStackTrace();}}}}}4、确定测试的方法packagecom.tom.mdc;importjava.lang.management.ManagementFactory;importjava.util.Random;importjava.util.concurrent.TimeUnit;/***PrintParamTarget**@authortoxmxin*/publicclassPrintParamTarget{publicstaticvoidmain(String[]args){//打印当前进程IDSystem.out.println(ManagementFactory.getRuntimeMXBean().getName());Randomrandom=newRandom();while(true){intsleepTime=5+random.nextInt(5);running(sleepTime);}}privatestaticvoidrunning(intsleepTime){try{TimeUnit.SECONDS.sleep(sleepTime);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("runningsleeptime"+sleepTime);}}