在定位公司问题的时候,需要了解一下skywalking的相关知识,而agent就提上了日程。

官网文档

Agent技术是Jdk在1.5版本之后,所提供的一个在jvm启动前后对部分java类代理加强的机制。由于是直接修改字节码,并不会对业务代码有注入,所以可以很好的应用于监控或者热部署等场景。

正常所提到的Agent一般都是部署成jar包的样子,比如agent-1.0-SNAPSHOT.jar。

在这个jar包中,要添加一个MANIFEST.MF文件,在文件中指定jar包的代理类,比如下面代码中的Premain-Class。

在对应的代理类,要实现一个permain方法或者agentmain方法,这样jvm可以通过MANIFEST找到类,通过类再找到对应的方法,从而进行加强,所以加强逻辑是在permain方法或者agentmain方法内部实现的。

Manifest-Version: 1.0
Built-By: qisi
Premain-Class: com.qisi.agent.InterviewAgent
Agent-Class: com.qisi.agent.InterviewAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Class-Path: byte-buddy-1.10.22.jar
Created-By: Apache Maven 3.8.1
Build-Jdk: 1.8.0_332
public class InterviewAgent {
public static void premain(String agentArgs, Instrumentation instrumentation) {
}
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
}
}

而如果在permain或者agentmain方法打上debug可以发现,执行时是通过sun.instrument.InstrumentationImpl#loadClassAndCallPremain和sun.instrument.InstrumentationImpl#loadClassAndCallAgentmain两个方法通过反射来执行到我们指定的类的。

Agent技术有两种场景,一种是在jvm启动之前,通过-javaagent:path来指定jar包,像是skywalking就是采用的这种方式;另一种则是在jvm启动之后,通过attach指定的进程,对jvm中的类进行加强,arthas就是采用的这种方式。

在具体介绍这两种方式之前,需要先讲一下Instrumentation相关类和接口

java.lang.Instrumentation

Instrumentation

Instrumentation相关的类都在java.lang.Instrumentation包下,两个异常,两个接口,一个类。



两个异常在这里不做介绍,功能就像类名一样。核心的其实是Instrumentation接口,本文仅关注红框内的几个方法。这几个方法都是通过permain和agentmain获取到的instrumentation实例进行的操作。



从时间发展来看,其中jdk1.5开始支持的是下面几个方法,也就是说在jdk5的时候,仅支持添加和移除类转换器,且添加的类转换器只能在加载和重定义的时候使用。就是说如果类没有加载,那么通过addTransformer方法注册的ClassFileTransformer就可以对这个类进行增强,否则一旦类已经加载完毕,则只能通过redefineClasses,完全替换类定义再次触发loadClass来增强

addTransformer(ClassFileTransformer transformer)
removeTransformer(ClassFileTransformer transformer)
isRedefineClassesSupported();//依赖于MANIFEST中的Can-Redefine-Classes值
redefineClasses(ClassDefinition... definitions)

而从jdk1.6开始,增加了一个retransformClasses的概念。retransform和redefine的区别,前者是在原有类的基础上进行修改,后者则是完全重定义,不使用原有类做任何参考。

需要注意的事,只有在首次调用addTransformer时,将canRetransform设置为true的类,才可以被重新转换。

addTransformer(ClassFileTransformer transformer, boolean canRetransform);
isRetransformClassesSupported();
retransformClasses(Class<?>... classes)//依赖于MANIFEST中的Can-Retransform-Classes值
isModifiableClass(Class<?> theClass);

ClassFileTransformer、ClassDefinition

这两个类其实都是Instrumentation接口方法的入参,其中用的比较多的应该是ClassFileTransformer。这个类只有一个transform,jvm类加载的时候都会调用一遍这个方法。如果需要加强,那么就利用给定的参数,进行字节码的改动,将改动后的字节码作为返回值返回;如果无需增强,则直接返回null即可。

byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)

ClassDefinition也类似,不过是在对象里重新绑定class和byte的关系

public final class ClassDefinition {
/**
* The class to redefine
*/
private final Class<?> mClass; /**
* The replacement class file bytes
*/
private final byte[] mClassFile;

实践

MANIFEST.MF配置

在pom文件中添加下面的代码,根据需要修改参数值

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>
com.qisi.agent.InterviewByteButtyAgent
</Premain-Class>
<Agent-Class>
com.qisi.agent.InterviewByteButtyAgent
</Agent-Class>
<Can-Redefine-Classes>
true
</Can-Redefine-Classes>
<Can-Retransform-Classes>
true
</Can-Retransform-Classes>
<Built-By>
qisi
</Built-By>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>

-javaagent:

在这种方式下,起作用的是permain,也就是说-javaagent和permain方法是配套使用的。

核心就是添加一个自定义的ClassFileTransformer,可以另起一个类,也可以这样匿名类。

如果只是熟悉流程可以像下面一样,直接打印一些日志,不去修改类;

    public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("enhance by premain,params:"+agentArgs);
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println("premain load Class :" + className);
return classfileBuffer;
}
}, true);
}

如果要真实修改,需要引入asm javassist bytebuddy等修改字节码的框架。下面这部分就是使用了bytebuddy,作用是让任何类的testAgent方法,都返回固定值transformed

public static void premain(String agentArgs, Instrumentation instrumentation) throws ClassNotFoundException {
System.out.println("enhance by permain InterviewByteButtyAgent,params:"+agentArgs);
new AgentBuilder.Default().type(any()).transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
return builder.method(named("testAgent"))
.intercept(FixedValue.value("transformed"));
}
}).installOn(instrumentation);
}

编写完之后,就可以在任意项目添加一个存在testAgent方法的进行尝试了,比如

java -javaagent:/xxxx/path/agent-1.0-SNAPSHOT.jar=key1:value1,key2:value2 -jar AppDemo.jar

attach

agentmain

这种方式需要实现agentmain方法,和permian不太一样的地方是需要在addTransformer之后触发需要retransformClasses想要加强的类。

public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("enhance by agentmain,params:"+agentArgs);
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println("agentmain load Class :" + className);
return classfileBuffer;
}
}, true);
try {
instrumentation.retransformClasses(Class.forName("com.qisi.mybatis.app.controller.FirstRequestController"));
} catch (UnmodifiableClassException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

同样,提供一个bytebuddy的例子,下面这个则是指定修改FirstRequestController的testAgent方法的返回值为transformed

public static void agentmain(String agentArgs, Instrumentation instrumentation) throws ClassNotFoundException {
System.out.println("enhance by agentmain InterviewByteButtyAgent,params:"+agentArgs);
//这里RedefinitionStrategy必须注意,默认的DISABLED是不支持retransform
new AgentBuilder.Default().with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION).type(new AgentBuilder.RawMatcher() {
@Override
public boolean matches(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, Class<?> classBeingRedefined, ProtectionDomain protectionDomain) {
return typeDescription.getName().contains("FirstRequestController");
}
}).transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
System.out.println("enhance"+typeDescription.getName());
return builder.method(named("testAgent"))
.intercept(FixedValue.value("transformed"));
}
//这里采用disableClassFormatChanges的方案,好像还可以使用advice
}).disableClassFormatChanges().installOn(instrumentation);
try {
instrumentation.retransformClasses(Class.forName("com.qisi.mybatis.app.controller.FirstRequestController"));
} catch (UnmodifiableClassException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

VirtualMachine

不同于-javaagent命令,这里需要使用自jdk6开始提供的VirtualMachine类,在tool.jar包里

下面的方法是我参考arthas写的一个attach的流程,选择我们想要attach的进程,然后加载我们上面写好的jar包就好了。

public class AgentTest {
public static void main(String[] args) throws IOException, AttachNotSupportedException {
String pid = null;
try {
Process jps = Runtime.getRuntime().exec("jps");
InputStream inputStream = jps.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
System.out.println("选择要attach的进程");
pid= new Scanner(System.in).nextLine();
System.out.println("选择的pid是"+pid);
} catch (IOException e) {
e.printStackTrace();
}
for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) {
if (virtualMachineDescriptor.id().equals(pid)){
VirtualMachine attach = VirtualMachine.attach(virtualMachineDescriptor);
try {
attach.loadAgent("/xxxxx/agent/target/agent-1.0-SNAPSHOT.jar","参数1,参数2");
} catch (AgentLoadException e) {
e.printStackTrace();
} catch (AgentInitializationException e) {
e.printStackTrace();
} finally {
attach.detach();
}
break;
}
}
}
}

参考文档:

探秘 Java 热部署二(Java agent premain)

JAVA热更新1:Agent方式热更 | 花隐间-JAVA游戏技术解决方案

ByteBuddy入门教程

Java Agent技术的更多相关文章

  1. java agent技术原理及简单实现

    注:本文定义-在函数执行前后增加对应的逻辑的操作统称为MOCK 1.引子 在某天与QA同学进行沟通时,发现QA同学有针对某个方法调用时,有让该方法停止一段时间的需求,我对这部分的功能实现非常好奇,因此 ...

  2. Java探针-Java Agent技术-阿里面试题

    Java探针参考:Java探针技术在应用安全领域的新突破 最近面试阿里,面试官先是问我类加载的流程,然后问了个问题,能否在加载类的时候,对字节码进行修改 我懵逼了,答曰不知道,面试官说可以的,使用Ja ...

  3. 深入浅出Java探针技术1--基于java agent的字节码增强案例

    Java agent又叫做Java 探针,本文将从以下四个问题出发来深入浅出了解下Java agent 一.什么是java agent? Java agent是在JDK1.5引入的,是一种可以动态修改 ...

  4. Java 调式、热部署、JVM 背后的支持者 Java Agent

    我们平时写 Java Agent 的机会确实不多,也可以说几乎用不着.但其实我们一直在用它,而且接触的机会非常多.下面这些技术都使用了 Java Agent 技术,看一下你就知道为什么了. -各个 J ...

  5. Java动态追踪技术探究(动态修改)

    Java动态追踪技术探究 Java探针-Java Agent技术-阿里面试题 秒懂Java动态编程(Javassist研究) 可以用于在类加载的时候,修改字节码. Java agent(Java探针) ...

  6. [转] Java Agent使用详解

    以下文章来源于古时的风筝 ,作者古时的风筝 我们平时写 Java Agent 的机会确实不多,也可以说几乎用不着.但其实我们一直在用它,而且接触的机会非常多.下面这些技术都使用了 Java Agent ...

  7. Java 安全之Java Agent

    Java 安全之Java Agent 0x00 前言 在前面发现很多技术都会去采用Java Agent该技术去做实现,比分说RASP和内存马(其中一种方式).包括IDEA的这些破解都是基于Java A ...

  8. 大厂偏爱的Agent技术究竟是个啥

    搜索关注微信公众号"捉虫大师",后端技术分享,架构设计.性能优化.源码阅读.问题排查.踩坑实践. hello大家好,我是小楼,今天给大家分享一个关于Agent技术的话题,也是后端启 ...

  9. 记一次多个Java Agent同时使用的类增强冲突问题及分析

    摘要:Java Agent技术常被用于加载class文件之前进行拦截并修改字节码,以实现对Java应用的无侵入式增强. 本文分享自华为云社区<记一次多个JavaAgent同时使用的类增强冲突问题 ...

  10. Java Agent场景性能测试分析优化经验分享

    摘要:本文将以Sermant的SpringBoot 注册插件的性能测试及优化过程为例,分享在Java Agent场景如何进行更好的性能测试优化及在Java Agent下需要着重注意的性能陷阱. 作者: ...

随机推荐

  1. ES6学习 第六章 数值的扩展

    前言 本章介绍数值的扩展.新增了很多方法,有些不常用的方法了解即可. 本章原文链接:数值的扩展 进制表示法 ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示. ...

  2. test-02-java 单元测试框架 junit5 入门介绍

    拓展阅读 junit5 系列 基于 junit5 实现 junitperf 源码分析 Auto generate mock data for java test.(便于 Java 测试自动生成对象信息 ...

  3. 从零开始手写 mybatis (三)jdbc pool 从零实现数据库连接池

    前景回顾 第一节 从零开始手写 mybatis(一)MVP 版本 中我们实现了一个最基本的可以运行的 mybatis. 第二节 从零开始手写 mybatis(二)mybatis interceptor ...

  4. 【Unity3D】动态路障导航

    1 NavMeshObstacle组件 ​ 导航系统.分离路面导航中路障都是静态的,程序运行过程中烘焙的导航网格一直不变,本文将进一步讲解动态路障场景下导航的实现. ​ 对于动态路障游戏对象,除了要设 ...

  5. 如何在Windows上使用Git创建一个可执行脚本?

    长话短说,今天介绍如何在windows上使用Git上创建一个可执行的shell脚本. 首先我们要知道windows上Git新添加的文件权限是:-rw-r--r--(对应权限值是644),而通常创建的s ...

  6. [Revit二次开发] 使用过滤器时,可能存在的坑:FilteredElementCollector.MoveNext()报错

    1.问题描述 在使用FilteredElementCollector时,如果涉及到需要对collector进行多次过滤处理,可能会出现MoveNext的报错. 问题代码如下: 1 var collec ...

  7. CSS之浮动Float

    前言 提到浮动,前端的小伙伴肯定都不陌生,但是随着弹性布局等等一些更好用的标准出来后,用在布局方面少了很多,当初我刚开始接触前端的时候,很习惯用浮动来给元素改变定位,当时还并不是很流行flexbox布 ...

  8. 学习go语言编程之数据类型

    数据类型概述 Golang语言内置了如下基础数据类型: 布尔类型:bool 整型:int8,unit8,int16,uint16,int32,uint32,int64,uint64,int,uint, ...

  9. Docker实践之06-访问仓库

    目录 什么是仓库 一.Docker Hub 注册 登录 拉取镜像 推送镜像 自动创建 二.私有仓库 Docker Registry 安装Docker Registry 在私有仓库上传/搜索/下载镜像 ...

  10. Ubuntu防火墙相关

    查看防火墙当前状态 sudo ufw status 开启防火墙 sudo ufw enable 关闭防火墙 sudo ufw disable 查看防火墙版本 sudo ufw version 默认允许 ...