简介:有了理论的应用之后,未免会想到,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=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 办法。
<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);
}
原文链接
本文为阿里云原创内容,未经容许不得转载。