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

-各个 Java IDE 的调试功能,例如 eclipse、IntelliJ ;

-热部署功能,例如 JRebel、XRebel、 spring-loaded;

-各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas;

-各种性能分析工具,例如 Visual VM、JConsole 等;

Java Agent 直译过来叫做 Java 代理,还有另一种称呼叫做 Java 探针。首先说 Java Agent 是一个 jar 包,只不过这个 jar 包不能独立运行,它需要依附到我们的目标 JVM 进程中。我们来理解一下这两种叫法。

代理:比方说我们需要了解目标 JVM 的一些运行指标,我们可以通过 Java Agent 来实现,这样看来它就是一个代理的效果,我们最后拿到的指标是目标 JVM ,但是我们是通过 Java Agent 来获取的,对于目标 JVM 来说,它就像是一个代理;

探针:这个说法我感觉非常形象,JVM 一旦跑起来,对于外界来说,它就是一个黑盒。而 Java Agent 可以像一支针一样插到 JVM 内部,探到我们想要的东西,并且可以注入东西进去。

拿上面的几个我们平时会用到的技术举例子。拿 IDEA 调试器来说吧,当开启调试功能后,在 debugger 面板中可以看到当前上下文变量的结构和内容,还可以在 watches 面板中运行一些简单的代码,比如取值赋值等操作。还有 Btrace、Arthas 这些线上排查问题的工具,比方说有接口没有按预期的返回结果,但日志又没有错误,这时,我们只要清楚方法的所在包名、类名、方法名等,不用修改部署服务,就能查到调用的参数、返回值、异常等信息。

上面只是说到了探测的功能,而热部署功能那就不仅仅是探测这么简单了。热部署的意思就是说再不重启服务的情况下,保证最新的代码逻辑在服务生效。当我们修改某个类后,通过 Java Agent 的 instrument 机制,把之前的字节码替换为新代码所对应的字节码。

Java Agent 结构



Java Agent 最终以 jar 包的形式存在。主要包含两个部分,一部分是实现代码,一部分是配置文件。

配置文件放在 META-INF 目录下,文件名为 MANIFEST.MF 。包括以下配置项:

Manifest-Version: 版本号

Created-By: 创作者

Agent-Class: agentmain 方法所在类

Can-Redefine-Classes: 是否可以实现类的重定义

Can-Retransform-Classes: 是否可以实现字节码替换

Premain-Class: premain 方法所在类

入口类实现 agentmainpremain 两个方法即可,方法要实现什么功能就由你的需求决定了。

Java Agent 实现和使用

接下来就来实现一个简单的 Java Agent,基于 Java 1.8,主要实现两点简单的功能:

1、打印当前加载的所有类的名称;

2、监控一个特定的方法,在方法中动态插入简单的代码并获取方法返回值;

在方法中插入代码主要是用到了字节码修改技术,字节码修改技术主要有 javassist、ASM,已经 ASM 的高级封装可扩展 cglib,这个例子中用的是 javassist。所以需要引入相关的 maven 包。

<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>

实现入口类和功能逻辑

入口类上面也说了,要实现 agentmainpremain 两个方法。这两个方法的运行时机不一样。这要从 Java Agent 的使用方式来说了,Java Agent 有两种启动方式,一种是以 JVM 启动参数 -javaagent:xxx.jar 的形式随着 JVM 一起启动,这种情况下,会调用 premain方法,并且是在主进程的 main方法之前执行。另外一种是以 loadAgent 方法动态 attach 到目标 JVM 上,这种情况下,会执行 agentmain方法。

加载方式 执行方法
-javaagent:xxx.jar 参数形式 premain
动态 attach agentmain

代码实现如下:

package kite.lab.custom.agent;

import java.lang.instrument.Instrumentation;

public class MyCustomAgent {
/**
* jvm 参数形式启动,运行此方法
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("premain");
customLogic(inst);
} /**
* 动态 attach 方式启动,运行此方法
* @param agentArgs
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst){
System.out.println("agentmain");
customLogic(inst);
} /**
* 打印所有已加载的类名称
* 修改字节码
* @param inst
*/
private static void customLogic(Instrumentation inst){
inst.addTransformer(new MyTransformer(), true);
Class[] classes = inst.getAllLoadedClasses();
for(Class cls :classes){
System.out.println(cls.getName());
}
}
}

我们看到这两个方法都有参数 agentArgs 和 inst,其中 agentArgs 是我们启动 Java Agent 时带进来的参数,比如-javaagent:xxx.jar agentArgs。Instrumentation Java 开放出来的专门用于字节码修改和程序监控的实现。我们要实现的打印已加载类和修改字节码也就是基于它来实现的。其中 inst.getAllLoadedClasses()一个方法就实现了获取所以已加载类的功能。

inst.addTransformer方法则是实现字节码修改的关键,后面的参数就是实现字节码修改的实现类,代码如下:

public class MyTransformer implements ClassFileTransformer {

    @Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("正在加载类:"+ className);
if (!"kite/attachapi/Person".equals(className)){
return classfileBuffer;
} CtClass cl = null;
try {
ClassPool classPool = ClassPool.getDefault();
cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod ctMethod = cl.getDeclaredMethod("test");
System.out.println("获取方法名称:"+ ctMethod.getName()); ctMethod.insertBefore("System.out.println(\" 动态插入的打印语句 \");");
ctMethod.insertAfter("System.out.println($_);"); byte[] transformed = cl.toBytecode();
return transformed;
}catch (Exception e){
e.printStackTrace(); }
return classfileBuffer;
}
}

以上代码的逻辑就是当碰到加载的类是 kite.attachapi.Person的时候,在其中的 test 方法开始时插入一条打印语句,打印内容是"动态插入的打印语句",在test方法结尾处,打印返回值,其中$_就是返回值,这是 javassist 里特定的标示符。

MANIFEST.MF 配置文件

在目录 resources/META-INF/ 下创建文件名为 MANIFEST.MF 的文件,在其中加入如下的配置内容:

Manifest-Version: 1.0
Created-By: fengzheng
Agent-Class: kite.lab.custom.agent.MyCustomAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: kite.lab.custom.agent.MyCustomAgent

配置打包所需的 pom 设置

最后 Java Agent 是以 jar 包的形式存在,所以最后一步就是将上面的内容打到一个 jar 包里。

在 pom 文件中加入以下配置

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>

用的是 maven 的 maven-assembly-plugin 插件,注意其中要用 manifestFile 指定 MANIFEST.MF 所在路径,然后指定 jar-with-dependencies ,将依赖包打进去。

上面这是一种打包方式,需要单独的 MANIFEST.MF 配合,还有一种方式,不需要在项目中单独的添加 MANIFEST.MF 配置文件,完全在 pom 文件中配置上即可。

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<phase>package</phase>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>kite.agent.vmargsmethod.MyAgent</Premain-Class>
<Agent-Class>kite.agent.vmargsmethod.MyAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

这种方式是将 MANIFEST.MF 的内容全部写作 pom 配置中,打包的时候就会自动将配置信息生成 MANIFEST.MF 配置文件打进包里。

运行打包命令

接下来就简单了,执行一条 maven 命令即可。

mvn assembly:assembly

最后打出来的 jar 包默认是以「项目名称-版本号-jar-with-dependencies.jar」这样的格式生成到 target 目录下。

运行打包好的 Java Agent

首先写一个简单的测试项目,用来作为目标 JVM,稍后会以两种方式将 Java Agent 挂到这个测试项目上。

package kite.attachapi;

import java.util.Scanner;

public class RunJvm {

    public static void main(String[] args){
System.out.println("按数字键 1 调用测试方法");
while (true) {
Scanner reader = new Scanner(System.in);
int number = reader.nextInt();
if(number==1){
Person person = new Person();
person.test();
}
}
}
}

以上只有一个简单的 main 方法,用 while 的方式保证线程不退出,并且在输入数字 1 的时候,调用 person.test()方法。

以下是 Person 类

package kite.attachapi;

public class Person {

    public String test(){
System.out.println("执行测试方法");
return "I'm ok";
}
}

以命令行的方式运行

因为项目是在 IDEA 里创建的,为了省事儿,我就直接在 IDEA 的 「Run/Debug Configurations」里加参数了。

-javaagent:/java-agent路径/lab-custom-agent-1.0-SNAPSHOT-jar-with-dependencies.jar

然后直接运行就可以看到效果了,会看到加载的类名称。然后输入数字键 "1",会看到字节码修改后的内容。

以动态 attach 的方式运行

测试之前先要把这个测试项目跑起来,并把之前的参数去掉。运行后,找到这个它的进程id,一般利用jps -l即可。

动态 attach 的方式是需要代码实现的,实现代码如下:

public class AttachAgent {

    public static void main(String[] args) throws Exception{
VirtualMachine vm = VirtualMachine.attach("pid(进程号)");
vm.loadAgent("java-agent路径/lab-custom-agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
}
}

运行上面的 main 方法 并在测试程序中输入“1”,会得到上图同样的结果。

发现了没,我们到这里实现的简单的功能是不是和 BTrace 和 Arthas 有点像呢。我们拦截了指定的一个方法,并在这个方法里插入了代码而且拿到了返回结果。如果把方法名称变成可配置项,并且把返回结果保存到一个公共位置,例如一个内存数据库,是不是我们就可以像 Arthas 那样轻松的检测线上问题了呢。当然了,Arthas 要复杂的多,但原理是一样的。

sun.management.Agent 的实现

不知道你平时有没有用过 visualVM 或者 JConsole 之类的工具,其实,它们就是用了 management-agent.jar 这个Java Agent 来实现的。如果我们希望 Java 服务允许远程查看 JVM 信息,往往会配置上一下这些参数:

-Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=192.168.1.1
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.rmi.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

这些参数都是 management-agent.jar 定义的。

我们进到 management-agent.jar 包下,看到只有一个 MANIFEST.MF 配置文件,配置内容为:

Manifest-Version: 1.0
Created-By: 1.7.0_07 (Oracle Corporation)
Agent-Class: sun.management.Agent
Premain-Class: sun.management.Agent

可以看到入口 class 为 sun.management.Agent,进到这个类里面可以找到 agentmain 和 premain,并可以看到它们的逻辑。在这个类的开始,能看到我们前面对服务开启远程 JVM 监控需要开启的那些参数定义。

不要吝惜你的「推荐」呦

欢迎关注,不定期更新本系列和其他文章

古时的风筝 ,进入公众号可以加入交流群

Java 调式、热部署、JVM 背后的支持者 Java Agent的更多相关文章

  1. paip.提升用户体验--提升java的热部署热更新能力

    paip.提升用户体验--提升java的热部署热更新能力 想让java做到php那么好的热部署能力  "fix online"/在线修复吗??直接在服务器上修改源码生效,无需重启应 ...

  2. java的热部署和热加载

    ps:热部署和热加载其实是两个类似但不同的概念,之前理解不深,so,这篇文章重构了下. 一.热部署与热加载 在应用运行的时升级软件,无需重新启动的方式有两种,热部署和热加载. 对于Java应用程序来说 ...

  3. JAVA代码热部署,在线不停服动态更新

    本地debug的时候,可以实时编译并更新代码,线上也可以不停服来动态更新类,即所说的java热部署.   JDK代理的两种方式: 1.premain方式是Java SE5开始就提供的代理方式,但其必须 ...

  4. Java服务器热部署的实现原理

    转自:http://blog.csdn.net/chenjie19891104/article/details/42807959 在web应用开发或者游戏服务器开发的过程中,我们时时刻刻都在使用热部署 ...

  5. java 中 热部署与卸载关系

    今天发现早年在大象笔记中写的一篇笔记,之前放在ijavaboy上的,现在它已经访问不了了.前几天又有同事在讨论这个问题.这里拿来分享一下. 在web应用开发或者游戏服务器开发的过程中,我们时时刻刻都在 ...

  6. Java 项目热部署,节省构建时间的正确姿势

    上周末,帮杨小邪(我的大学室友)远程调试项目.SpringBoot 构建,没有热部署,改一下就得重启相关模块.小小的 bug ,搞了我一个多小时,大部分时间都还在构建上(特么,下次得收钱才行).我跟他 ...

  7. Java Web 热部署

    热部署有多种方案,下面的方案是其中的一种. 暂时还没找到一种令人满意的方案. 1,配置WEB Server 去这里 (https://tomcat.apache.org/download-90.cgi ...

  8. Java内存模型、JVM内存结构和Java对象模型

    JVM内存结构 我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途.其中有些区域随着虚拟机进程的启动而存 ...

  9. 2017.7.12 IDEA热部署(更新jsp或java代码不用重启tomcat即可即时生效)

    选择war explored. 主要在于 On frame deactivation选项配置选择为 Update classes and resourses(当且仅当在Deployment配置页,对应 ...

随机推荐

  1. 富文本编辑器TinyMCE的使用(React Vue)

    富文本编辑器TinyMCE的使用(React Vue) 一,需求与介绍 1.1,需求 编辑新闻等富有个性化的文本 1.2,介绍 TinyMCE是一款易用.且功能强大的所见即所得的富文本编辑器. Tin ...

  2. Python中input()的使用方法

    input()以字符串的方式获取用户输入: >>> x = input() 4.5 >>> type(x) <class 'str'> >> ...

  3. NAS

    NAS, Network Attached Storage, 网络附属存储, 简单来说就是连接在网络上, 可以存储资料的装置.可以用来做私有网盘,同步各种设备的照片.视频.音频和文件. 常见的 NAS ...

  4. 集成方法 Ensemble

    一.bagging 用于基础模型复杂.容易过拟合的情况,用来减小 variance(比如决策树).基础模型之间没有太多联系(相对于boosting),训练可以并行.但用 bagging 并不能有助于把 ...

  5. Eclipse高级操作 远程调试

    Eclipse高级操作 远程调试 JPDA是SUN JDK自带的远程调试机制.它提供了一套标准的调试接口,可以从虚拟机一级允许外界用特定协议探测虚拟机内部的运作细节.只要你装了JDK1.2以上的SUN ...

  6. APP手机商城系统选择,混合开发与原生开发哪个好?

    关于手机APP开发用混合还是原生现在说法不一,有说混合开发好:时间短.费用低.效果也不错,有说原生开发好,原生APP在性能方面比较好.而商城系统中的手机APP用混合开发还是原生开发比较好呢? 最近我参 ...

  7. fiddler抓HTTPS及APP请求的配置教程

    一.fiddler安装 Fiddler官网下载Fiddler进行安装:https://www.telerik.com/fiddler 二.fiddler设置抓取HTTPS的请求 fiddler默认设置 ...

  8. cython 第一次接触

    https://www.cnblogs.com/kaituorensheng/p/4452881.html

  9. unity shader之预备知识

    1.渲染流水线 任务:从一个三维场景出发,生成(或者渲染)一张二维图像.即:计算机需要从一系列的定点出数据,纹理等信息出发,把这些信息最终转换程一张人眼可以看到的图像.而这个工作通常是由CPU和GPU ...

  10. MSIL实用指南-逻辑运算

    逻辑运算有三种:与.或.非.实现它们可以用位运算指令And.Or.Not等三个指令.它们的使用方法和位运算一致. 完整的程序如下: using System; using System.Reflect ...