需求背景
agent是什么大家应该很熟悉了,今天我们来实战下,效果就是为项目所有elasticsearch请求方法增加耗时告警!
学会Java Agent你能做什么?
- 自动添加getter/setter方法的工具lombok就使用了这一技术
- btrace、Arthas和housemd等动态诊断工具也是用了instrument技术
- Intellij idea 的 HotSwap、Jrebel 等也是该技术的实现之一
- pinpoint、skywalking、newrelic、听云的 APM 产品等都基于 Instrumentation 实现
使用方法
依赖maven
<dependency>
<groupId>com.uc.agent</groupId>
<artifactId>neighbour-agent-elasticsearch-starter</artifactId>
<version>0.0.56</version>
</dependency>
到此我们的agent就已经集成了,不需要加任何启动参数,完全是无侵入式!!!!
解决了jar -jar方式启动的问题:
- springboot自定义类加载器LaunchedURLClassLoader ,与agent的类加载器不同的冲突问题。
- VirtualMachine绑定agent时,loadAgent方法找不到agentjar问题。
- AgentLoader 加载之前 (agent动态绑定之前) 被JVM加载过的class是不会回调addTransformer方法的。 springboot扩展点和import方式导入的组件class优先AgentLoader 加载了,所以会造成agent拦截不到。
- springboot本地可以,打包到线上jar启动方式agent无效等问题。
当es执行 search方法时,会自动打印方法耗时:
neighbour-agent-elasticsearch-starter 的下载地址 在github上面
https://github.com/HadLuo/neighbour-agent-elasticsearch-starter.git
下面我们看下简单的agent使用,但是没有解决上面 jar启动的问题,要了解实现请下载源码!!!
开场实例:
比如我们业务代码的网络请求框架代码(模拟):
public class HttpClient {
public void post() {
System.out.println("HttpClient pos 请求");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我们要实现的就是监听当网络请求超过1秒就钉钉告警出来。比如:
实现过程
AgentLoader
我们先实现一个AgentLoader 用来加载agent:
@Configuration
public class AgentLoader implements InitializingBean{
@Override
public void afterPropertiesSet() throws Exception {
// 动态获取SpringBoot启动类名称
StartAppClassName = getMainClassName();
// 加载agent jar包 得到路径
File file = FileLoads.loadFile("agent-client-0.0.1-SNAPSHOT-jar-with-dependencies.jar");
String jar = file.getAbsolutePath();
try {
for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) {
// 针对指定名称的JVM实例
if (virtualMachineDescriptor.displayName().equals(StartAppClassName)) {
System.out.println(
"将对该进程的vm进行增强:org.example.agent.AgentTest的vm进程, pid=" + virtualMachineDescriptor.id());
// attach到新JVM
VirtualMachine vm = VirtualMachine.attach(virtualMachineDescriptor);
// 加载agentmain所在的jar包
vm.loadAgent(jar);
// detach
vm.detach();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
当前放到SpringBoot初始化 Import 这个类 加载就行。
agent-client-0.0.1-SNAPSHOT-jar-with-dependencies.jar 为下面要制作的 agent jar名称,需要放到项目的resource目录下。
这里我们其实就是用到了agent的动态绑定方式去绑定。
agent jar制作:
agentmain方法:
JDK 1.6 引入了新的 agentmain 用于支持在类加载后再次加载该类,也就是重定义类,在重定义的时候可以修改类。但是这种方式对类的修改有较大的限制,修改后的类要兼容原来的旧类,具体的要求在 Java 官方文档 Instrumentation#retransformClasses()方法介绍 中可以找到: 转换类时禁止添加、删除、重命名成员变量和方法,禁止修改方法的签名,禁止改变类的继承关系。
public static void agentmain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader l, String className, Class<?> c, ProtectionDomain pd, byte[] b) {
try {
if (className == null) {
return null;
}
// System.err.println(className);
className = className.replace("/", ".");
if (className.equals("com.uc.riskcontroller.trace.HttpClient")) {
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get("com.uc.riskcontroller.trace.HttpClient");
for (CtMethod method : clazz.getMethods()) {
if (Modifier.isNative(method.getModifiers())) {
continue;
}
method.addLocalVariable("s", classPool.get("long"));
method.insertBefore("s = System.currentTimeMillis();");
method.insertAfter("System.out.println(System.currentTimeMillis() - s);", false);
method.insertAfter("com.uc.framework.alert.AlertContext.robot(com.uc.framework.env.EnvironmentServer.UnkownExceptionWebwork).alert(\"http客户端请求耗时:\" + (System.currentTimeMillis() - s ));", false);
}
return clazz.toBytecode();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}, true);
Class<?>[] classes = instrumentation.getAllLoadedClasses();
if (classes != null) {
for (Class<?> c : classes) {
if (c.isInterface() || c.isAnnotation() || c.isArray() || c.isEnum()) {
continue;
}
if (c.getName().equals("com.uc.riskcontroller.trace.HttpClient")) {
try {
System.out.println("retransformClasses start, class: " + c.getName());
instrumentation.retransformClasses(c);
System.out.println("retransformClasses end, class: " + c.getName());
} catch (UnmodifiableClassException e) {
System.out.println("retransformClasses error, class: " + c.getName() + ", ex:" + e);
e.printStackTrace();
}
}
}
}
}
核心就在于Instrumentation的两个方法:
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
- addTransformer()用来注册类的修改器;JVM每装载一个类,
transform
都会被回调执行。 - retransformClasses()会让类重新加载,从而使得注册的类修改器能够重新修改类的字节码。
在利用javaassit进行字节码修改,达到了增加耗时告警目的。
到此我们实例已经制作完毕。
但是上面会有一个问题,在线上 我们用jar -jar 启动时,会有各种问题, 但是在文章的前面实现的案例都已经解决了,需要读者自行下载。
下载地址在github上面
https://github.com/HadLuo/neighbour-agent-elasticsearch-starter.git