簡介:有了實際的使用以後,難免會想到,Arthas 是如何作到在程序運行時,動態監測咱們的代碼的呢?帶着這樣的問題,咱們一塊兒來看下 Java Agent 技術實現原理。
項目中有使用到 com.github.dreamroute excel-helper 這個工具來輔助 Excel 文件的解析,出錯時的代碼是這樣寫的:以下所示(非源代碼)html
try { excelDTOS = ExcelHelper.importFromFile(ExcelType.XLSX, file, ExcelDTO.class); } catch (Exception e) { log.error("ExcelHelper importFromFile exception msg {}", e.getMessage()); }
由於打印異常信息時,使用了 e.getMessage() 方法,沒有將異常信息打印出來。並且本地復現也沒有復現出來。因此只能考慮使用 arthas 來協助排查這個問題了。
java
一、線上服務器安裝 Arthas。
https://arthas.aliyun.com/doc/install-detail.htmlgit
二、使用 watch 命令監控指定方法,打印出異常的堆棧信息,命令以下:github
watch com.github.dreamroute.excel.helper.ExcelHelper importFromFile '{params,throwExp}' -e -x 3
再次調用方法,捕獲到異常棧信息以下: apache
已經捕獲到異常,並打印出堆棧信息。
緩存
三、根據對應的堆棧信息,定位到具體的代碼,以下:服務器
代碼很簡單,從代碼中能夠很清晰的看到若是沒有從 headerInfoMap 中沒有獲取到指定的 headerInfo ,就會拋這個異常。沒有找到只有兩種狀況:
數據結構
對於第二種狀況,首先去校驗了一下上傳的 Excel 文件是否有問題,本地測試了一下 Excel 文件,沒有任何問題。本地測試也是成功的,因此主觀判斷,第二種狀況的可能性不大。app
因此說主要檢查第一種狀況是否發生,這個時候能夠再去看一下該方法的第一行代碼
dom
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 )。
四、這個時候須要再確認一下 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 文件在解析時都會拋錯。
五、詢問了當時發現這個問題的同事,得知他第一次上傳的 Excel 文件是有問題的,後面想改正,再上傳時便出現了問題。到這裏問題也算是找到了。
有了實際的使用以後,難免會想到,Arthas 是如何作到在程序運行時,動態監測咱們的代碼的呢?帶着這樣的問題,咱們一塊兒來看下 Java Agent 技術實現原理。
Agent 是一個運行在目標 JVM 的特定程序,它的職責是負責從目標 JVM 中獲取數據,而後將數據傳遞給外部進程。加載 Agent 的時機能夠是目標 JVM 啓動之時,也能夠是在目標 JVM 運行時進行加載,而在目標 JVM 運行時進行 Agent 加載具有動態性。
經過 javassist,在運行時更改指定方法的代碼,在方法以前後添加自定義邏輯。
一、定義 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; } }
二、使用 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>
三、定義 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(); } } } } }
四、定義測試的方法
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); } }
本文內容由阿里雲實名註冊用戶自發貢獻,版權歸原做者全部,阿里雲開發者社區不擁有其著做權,亦不承擔相應法律責任。具體規則請查看《阿里雲開發者社區用戶服務協議》和《阿里雲開發者社區知識產權保護指引》。若是您發現本社區中有涉嫌抄襲的內容,填寫侵權投訴表單進行舉報,一經查實,本社區將馬上刪除涉嫌侵權內容。