使用 Arthas 排查開(kāi)源 Excel 組件問(wèn)題
背景介紹
項(xiàng)目中有使用到 com.github.dreamroute excel-helper 這個(gè)工具來(lái)輔助 Excel 文件的解析,出錯(cuò)時(shí)的代碼是這樣寫的:如下所示(非源代碼)
- try { excelDTOS = ExcelHelper.importFromFile(ExcelType.XLSX, file, ExcelDTO.class); } catch (Exception e) { log.error("ExcelHelper importFromFile exception msg {}", e.getMessage()); }
因?yàn)榇蛴‘惓P畔r(shí),使用了 e.getMessage() 方法,沒(méi)有將異常信息打印出來(lái)。而且本地復(fù)現(xiàn)也沒(méi)有復(fù)現(xiàn)出來(lái)。所以只能考慮使用 arthas 來(lái)協(xié)助排查這個(gè)問(wèn)題了。
排查過(guò)程
1、線上服務(wù)器安裝 Arthas。
https://arthas.aliyun.com/doc/install-detail.html
2、使用 watch 命令監(jiān)控指定方法,打印出異常的堆棧信息,命令如下:
watch com.github.dreamroute.excel.helper.ExcelHelper importFromFile '{params,throwExp}' -e -x 3
再次調(diào)用方法,捕獲到異常棧信息如下:
已經(jīng)捕獲到異常,并打印出堆棧信息。
3、根據(jù)對(duì)應(yīng)的堆棧信息,定位到具體的代碼,如下:
代碼很簡(jiǎn)單,從代碼中可以很清晰的看到如果沒(méi)有從 headerInfoMap 中沒(méi)有獲取到指定的 headerInfo ,就會(huì)拋這個(gè)異常。沒(méi)有找到只有兩種情況:
headerInfoMap 中保存的信息不對(duì)。
cell 中的 columnIndex 超出的正常的范圍導(dǎo)致沒(méi)有獲取到對(duì)應(yīng) HeaderInfo 。
對(duì)于第二種情況,首先去校驗(yàn)了一下上傳的 Excel 文件是否有問(wèn)題,本地測(cè)試了一下 Excel 文件,沒(méi)有任何問(wèn)題。本地測(cè)試也是成功的,所以主觀判斷,第二種情況的可能性不大。
所以說(shuō)主要檢查第一種情況是否發(fā)生,這個(gè)時(shí)候可以再去看一下該方法的第一行代碼
- MapheaderInfoMap = processHeaderInfo(rows,cls);
可以看到headerInfoMap是通過(guò)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;}
主要通過(guò) CacheFactory 類的 findHeaderInfo 來(lái)生成,在 findHeaderInfo 方法中,通過(guò)一個(gè)被 static final 修飾的 HEADER_INFO 變量來(lái)做緩存,被調(diào)用時(shí)先去HEADER_INFO 中查,如果有則直接返回,沒(méi)有則重新創(chuàng)建(也就說(shuō)明相同的 Excel 文件,僅初始化一次 HeaderInfo )。創(chuàng)建的步驟在 ClassAssistant.getHeaderInfo() 方法中。
簡(jiǎn)單的看一下 HeaderInfo 的生成過(guò)程,根據(jù) Excel 文件的第一行中的各個(gè) Cell 值與自定義實(shí)體類的注解比較,如果名字相同,就存為一個(gè)鍵值對(duì)( HeaderInfo 的數(shù)據(jù)結(jié)構(gòu)為 HashMap )。
4、這個(gè)時(shí)候需要再確認(rèn)一下 HEADER_INFO 中保存的 ExcelDTO.class 相關(guān)的 HeaderInfo 是怎樣的。通過(guò) ognl 命令或者 getstatic 命令來(lái)查看。這里使用 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}'
結(jié)果如下:正常情況下這個(gè) Excel 文件有 6 列信息,為什么只產(chǎn)生了 4 個(gè)鍵值對(duì)呢?如果 HEADER_INFO 中保存了錯(cuò)的,從上面的邏輯來(lái)看,后面上傳的正確的 Excel 文件在解析時(shí)都會(huì)拋錯(cuò)。
5、詢問(wèn)了當(dāng)時(shí)發(fā)現(xiàn)這個(gè)問(wèn)題的同事,得知他第一次上傳的 Excel 文件是有問(wèn)題的,后面想改正,再上傳時(shí)便出現(xiàn)了問(wèn)題。到這里問(wèn)題也算是找到了。
Arthas 原理探究
有了實(shí)際的使用之后,不免會(huì)想到,Arthas 是如何做到在程序運(yùn)行時(shí),動(dòng)態(tài)監(jiān)測(cè)我們的代碼的呢?帶著這樣的問(wèn)題,我們一起來(lái)看下 Java Agent 技術(shù)實(shí)現(xiàn)原理。
Java Agent 技術(shù)
Agent 是一個(gè)運(yùn)行在目標(biāo) JVM 的特定程序,它的職責(zé)是負(fù)責(zé)從目標(biāo) JVM 中獲取數(shù)據(jù),然后將數(shù)據(jù)傳遞給外部進(jìn)程。加載 Agent 的時(shí)機(jī)可以是目標(biāo) JVM 啟動(dòng)之時(shí),也可以是在目標(biāo) JVM 運(yùn)行時(shí)進(jìn)行加載,而在目標(biāo) JVM 運(yùn)行時(shí)進(jìn)行 Agent 加載具備動(dòng)態(tài)性。
基礎(chǔ)概念
JVMTI(JVM Tool Interface):是 JVM 暴露出來(lái)的一些供用戶擴(kuò)展的接口集合,JVMTI 是基于事件驅(qū)動(dòng)的,JVM 每執(zhí)行到一定的邏輯就會(huì)調(diào)用一些事件的回調(diào)接口(如果有的話),這些接口可以供開(kāi)發(fā)者去擴(kuò)展自己的邏輯。
JVMTIAgent(JVM Tool Interface):是一個(gè)動(dòng)態(tài)庫(kù),利用 JVMTI 暴露出來(lái)的一些接口幫助我們?cè)诔绦騿?dòng)時(shí)或程序運(yùn)行時(shí) JVM Attach 機(jī)制,將 Agent 加載到目標(biāo) JVM 中。
JPLISAgent(Java Programming Language Instrumentation Services Agent):它的作用是初始化所有通過(guò) Java Instrumentation API 編寫的 Agent,并且也承擔(dān)著通過(guò) JVMTI 實(shí)現(xiàn) Java Instrumentation 中暴露 API 的責(zé)任。
VirtualMachine :提供了Attach 動(dòng)作和 Detach 動(dòng)作,允許我們通過(guò) attach 方法,遠(yuǎn)程連接到 JVM 上,然后通過(guò) loadAgent 方法向 JVM 注冊(cè)一個(gè)代理程序 agent ,在該 agent 的代理程序中會(huì)得到一個(gè) Instrumentation 實(shí)例,該實(shí)例可以在 class 加載前改變 class 的字節(jié)碼,也可以在 class 加載后重新加載。
Instrumentation:可以在 class 加載前改變 class 的字節(jié)碼(premain),也可以在 class 加載后重新加載(agentmain)。
執(zhí)行過(guò)程
動(dòng)手寫一個(gè) Demo
通過(guò) javassist,在運(yùn)行時(shí)更改指定方法的代碼,在方法之前后添加自定義邏輯。
1、定義 Agent 類。當(dāng)前 Java 提供了兩種方式可以將代碼代碼注入到 JVM 中,這里我們的 Demo 選擇使用 agentmain 方法來(lái)實(shí)現(xiàn)。
premain:在啟動(dòng)時(shí)通過(guò) javaagent 命令,將代理注入到指定的 JVM 中。
agentmain:運(yùn)行時(shí)通過(guò) 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的包名是用點(diǎn)分割的,需要轉(zhuǎn)換下 if (className != null && className.indexOf("/") != -1) { className = className.replaceAll("/", "."); } try { //通過(guò)包名獲取類文件 CtClass cc = ClassPool.getDefault().get(className); //獲得指定方法名的方法 CtMethod m = cc.getDeclaredMethod(agentArgs.split(",")[2]); //在方法執(zhí)行前插入代碼 m.insertBefore("{ System.out.println(\"=========開(kāi)始執(zhí)行=========\"); }"); m.insertAfter("{ System.out.println(\"=========結(jié)束執(zhí)行=========\"); }"); 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 方法,通過(guò) VirtualMachine.attach(#{pid}) 來(lái)指定要代理的類。
- 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進(jìn)程中。 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、定義測(cè)試的方法
- 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) { // 打印當(dāng)前進(jìn)程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); }}