一个简单JavaAgent的实现
一、什么是javaagent
javaagent是一个JVM“插件”,一种专门精心制作的.jar文件,它能够利用JVM提供的Instrumentation API。
1.1、概要
Java Agent由三部分组成:代理类、代理类元信息和JVM加载.jar和代理的机制,整体内容如下图所示:
1.2、javaagent的基石
java.lang.instrument
为javaagent 通过修改方法字节码的方式操作运行在JVM上的程序提供服务。javaagent以JAR包的形式部署,JAR文件清单中的属性指定要加载的代理类,以启动代理。
javaagent的启动方式有以下几种:
通过在命令行指定参数启动。
JVM启动后启动。例如,提供一种工具,该工具可以依附到已运行的应用,并允许在已运行的应用内加载代理。
与应用一起打包为可执行文件。
1.3、启动 javaagent
1.3.1、命令行启动
命令行启动参数如下:
-javaagent:<jarpath>[=<options>]
<jarpath>
:javaagent的路径,比如 /opt/var/Agent-1.0.0.jar
。
<options>
: javaagent参数,参数的解析由javaagent负责。
javaagent JAR文件清单必须包含 Premain-Class
属性,属性的值为agent class的全路径名(包名+类名)。代理类必须实现 premain
方法,premain
方法和 main
方法一样分别是代理和应用的入口点。JVM初始化完成后首先调用代理的premain函数,然后调用应用的main函数,premain方法必须返回后进程才能启动。
premain
方法签名如下:
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
JVM首先尝试在代理中调用签名为1的方法,如果代理类没有实现签名为1的方法,JVM尝试调用签名为2的方法:
代理类可以有一个 agentmain
函数,函数会在JVM启动完成之后调用。如果,使用命令行启动代理,agentmain
方式不会被调用。
代理的所有参数被当作一个字符串通过 agentArgs
变量传递,代理负责解析参数字符串。
如果代理因为代理类无法被加载、代理类未实现 premain
方法或抛出了未被捕获的异常,JVM将会退出。
javaagent的启动不要求实现一定提供命令行的方式,如果,实现支持通过命令行启动,实现必须支持在命令行中通过指定 -javaagent
参数启动。 -javaagent
可以在命令行中使用多次,启动多个代理。premain
函数的调用顺序和命令行中指定的顺序一致,多个代理可以使用相同 <jarpath>
。
没有一个严格模型来定义 premain
函数的工作范围,任何 main
函数可以做的工作,比如创建线程,在 premain
函数中都是合法的。
1.3.2、JVM启动后启动
实现可以提供在JVM启动之后再启动代理的机制。代理如何启动的细节特定于实现,通常应用程序已经启动,并且它的 main
方法已经被调用。如果实现支持在JVM启动后启动代理,代理必须满足以下条件:
清单文件包含
Agent-Class
属性,属性的值为代理类全名。代理类必须实现
public static agentmain
方法。
agentmain方法有以下两个函数签名:
public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)
JVM首先尝试调用具有签名1的方法,如果,代理类没有实现该方法,JVM尝试调用签名为2的方法。
代理类可以同时实现 premain
和 agentmain
两个方法,当代理以命令行方式启动时,JVM调用 premain
函数,当代理在JVM启动之后启动时,JVM调用 agentmain
函数,而且JVM不会调用 premain
函数。
agentmain
函数参数的传递也是通过 agentArgs
,所有参数组合为一个字符串,参数的解析由代理负责。
agentmain
函数必须完成启动代理所有必须的初始化动作,当启动完成后,agentmain
函数必须返回。如果,代理不能启动或抛出未捕获的异常,JVM都会退出。
1.3.3、打包为可执行文件
如果代理打包到可执行JAR文件中,可执行JAR文件的清单中必须包含 Launcher-Agent-Class
属性,指定一个在应用main函数调用之前代理启动的类。JVM尝试在代理上调用以下方法:
public static void agentmain(String agentArgs, Instrumentation inst)
如果,代理类没有实现上述方法,JVM则调用下面的方法。
public static void agentmain(String agentArgs)
agentArgs
参数的值必须为空字符串。
agentmain
函数必须完成代理启动必须的所有初始化动作并在启动后返回。如果,代理无法启动或抛出未捕获的异常,JVM会退出。
1.3.4、加载代理类以及代理类可用的模块/类
系统类加载器负责加载代理JAR文件中的所有类,并且成为系统类加载器的未命名模块的成员。 系统类加载器通常也定义包含应用程序 main
方法的类。对代理类可见的所有类都对系统类加载器可见,必须满足下面的最低要求:
启动层中的模块导出的包中的类。 启动层是否包含所有平台模块取决于初始模块或应用程序的启动方式。
类可被系统类加载器定义。
启动类加载器定义的所有代理的类为其未命名模块的成员。
如果代理类需要链接到不在启动层中的平台(或其他)模块中的类,则需要以确保这些模块位于启动层中的方式启动应用程序。 例如,在JDK实现中,--add-modules
命令行选项可用于将模块添加到要在启动时解析的根模块集中。
启动类加载器可以加载代理支持的类(通过 appendToBootstrapClassLoaderSearch
或指定Boot-Class-Path属性)必须仅链接到定义启动类加载器的类。 无法保证启动类加载器可以在所有平台工作。
如果配置了自定义系统类加载器(通过 getSystemClassLoader
方法中指定的系统属性 java.system.class.loader
),则必须定义 appendToSystemClassLoaderSearch
中指定的 appendToClassPathForInstrumentation
方法。 换句话说,自定义系统类加载器必须支持将代理JAR文件添加到系统类加载器搜索范围内的机制。
1.4、javaagent清单属性
属性 | 说明 | 是否必选 | 默认值 |
---|---|---|---|
Premain-Class | 包含premain方法的类 | 依赖启动方式 | 无 |
Agent-Class | 包含agentmain方法的类 | 依赖启动方式 | 无 |
Boot-Class-Path | 启动类加载器搜索路径 | 否 | 无 |
Can-Redefine-Classis | 是否可以重定义代理所需的类 | 否 | false |
Can-Retransform-Classis | 是否能够重新转换此代理所需的类 | 否 | false |
Can-Set-Native-Method-Prefix | 是否能够设置此代理所需的本机方法前缀 | 否 | false |
二、写一个Java Agent
基于上面的介绍,我们实现一个下载JVM中所有非系统类的javaagent。
整个开发过程包括以下三步:
1)定义代理类,实现类下载功能;
2)配置、打包;
3)命令行启动测试。
2.1、代理类实现
实现 premain
函数
package io.ct.java.agent;import java.lang.instrument.Instrumentation;public class AgentApplication {public static void premain(String arg, Instrumentation instrumentation) {System.err.println("agent startup , args is " + arg);// 注册我们的文件下载函数instrumentation.addTransformer(new DumpClassesService());}
}
文件下载类实现 ClassFileTransformer
接口,在类被加载时下载类的字节码:
package io.ct.java.agent;import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.List;/*** Copyright (C), 2018-2018, open source* FileName: DumpClassesService** @author : 大哥* Date: 2018/12/8 21:01*/
public class DumpClassesService implements ClassFileTransformer {private static final List<String> SYSTEM_CLASS_PREFIX = Arrays.asList("java", "sum", "jdk");@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {if (!isSystemClass(className)) {System.out.println("load class " + className);FileOutputStream fos = null;try {// 将类名统一命名为classNamedump.class格式fos = new FileOutputStream(className + "dump.class");fos.write(classfileBuffer);fos.flush();} catch (IOException ioe) {ioe.printStackTrace();} finally {// 关闭文件输出流if (null != fos) {try {fos.close();} catch (IOException e) {e.printStackTrace();}}}}return classfileBuffer;}/*** 判断一个类是否为系统类** @param className 类名* @return System Class then return true,else return false*/private boolean isSystemClass(String className) {// 假设系统类的类名不为NULL而且不为空if (null == className || className.isEmpty()) {return false;}for (String prefix : SYSTEM_CLASS_PREFIX) {if (className.startsWith(prefix)) {return true;}}return false;}
}
2.2、配置MANIFEST.MF
MANIFEST.MF
文件两种方式生成:手动配置和自动生成,手动配置只需要在 resources
文件下创建 META-INF/MENIFEST.MF
文件即可。除去手动配置外,可以使用maven插件在打包阶段自动生成,maven的插件配置如下:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><configuration><archive><manifestEntries><Premain-Class>io.ct.java.agent.AgentApplication</Premain-Class><Agent-Class>io.ct.java.agent.AgentApplication</Agent-Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes></manifestEntries></archive></configuration></plugin>
生成的jar包格式如下:
其中MANIFEST.MF的文件内容如下(不同的配置生成的文件内容不完全一致):
Manifest-Version: 1.0
Implementation-Title: agent
Premain-Class: io.ct.java.agent.AgentApplication
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: chentong
Agent-Class: io.ct.java.agent.AgentApplication
Can-Redefine-Classes: true
Implementation-Vendor-Id: io.ct.java
Can-Retransform-Classes: true
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_171
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/agent
2.3、命令行启动Java Agent
执行下面的命令,运行已经编译好的类Hello,可以在同级目录下生成一个名为Hellodump.class的文件。
java -javaagent:agent-0.0.1-SNAPSHOT.jar Hello
一个简单JavaAgent的实现相关推荐
- 聊聊高并发(十六)实现一个简单的可重入锁
可重入锁指的是假设一个线程已经获得了一个锁,那么它能够多次进入这个锁,当然前提是线程须要先获得这个锁. 可重入锁是最常使用的锁.Java的内置锁就是可重入锁,使用synchronizedkeyword ...
- 用php做一个简单的汇率,vue实现简单实时汇率计算功能
最近在自己摸索vue的使用,因为相对于只是去看教程和实例,感觉不如自己动手写一个demo入门来的快.刚好看到小程序中有一个简单但是很精致的应用极简汇率,而且它的表现形式和vue的表现形式很像,于是想着 ...
- python编写赛车游戏单机版_使用Python中OrderedDict模拟一个简单的竞速游戏排名
上一篇,我们梳理了Python中关于字典排序的一些常用方法(杂乱无章的数据结构如何进行排序,简明讲述Python字典排序那些事).其中,我们讲到了Python的collections模块中的Order ...
- 编写一个最简单的.php,学习猿地- 说明 如果我们要编写一个简单的PHP脚本,需要学习哪些...
说明 如果我们要编写一个简单的 PHP 脚本,需要学习哪些基础知识呢? PHP 基础 PHP 脚本可放置于文档中的任何位置. 标准 的 PHP 脚本以 <?php 开头,以 ?> 结尾: ...
- 【javamatlab】以一个简单的例子实现java和matlab混编
目录 使用环境: MATLAB: matlab代码: 将matlab代码打包: eclipse: jar包配置: 使用jar包: 使用环境: jdk8(ide使用eclipse2019-6).matl ...
- 一个简单的slider滑块组件
2019独角兽企业重金招聘Python工程师标准>>> 我们先来看一张图片: 要实现这样的效果我们有很多种方法,比如直接使用<input type="range&qu ...
- 用 cooking 搭建一个简单又优雅的 Vue 项目开发环境 (入门篇)
本文适合 Vue 的初学者,以及对 webpack 不熟悉的同学阅读.前提是你要会用基本的命令行. Node 和 NPM,以及掌握 ES2015 的基础知识.本文都是在 macOS 环境下运行,要求使 ...
- Directx11教程(6) 画一个简单的三角形(2)
在上篇教程中,我们实现了在D3D11中画一个简单的三角形,但是,当我们改变窗口大小时候,三角形形状却随着窗口高宽比例改变而改变,如下图所示: 这是因为我们改变了窗口大小,但后缓冲大小在程序初始化时候, ...
- python如何编写数据库_如何在几分钟内用Python编写一个简单的玩具数据库
python如何编写数据库 MySQL, PostgreSQL, Oracle, Redis, and many more, you just name it - databases are a re ...
最新文章
- SSE4.1和SSE4.2 Intrinsics各函数介绍
- Linq To Sql 练习
- iphone怎么分屏_问答 | Mac 应用商店中无法”获取“软件怎么办?
- QT的QGraphicsLinearLayout类的使用
- leetcode 347. Top K Frequent Elements | 347. 前 K 个高频元素(大根堆)
- 使用双重循环,输出数字金字塔
- json字符串导入oracle,如何在Oracle中将JSON字符串转换为JSON
- python实现图片找不同游戏_用Python实现谷歌的小恐龙游戏
- java 修改最大nio连接数_携程基于Quasar协程的NIO实践
- 2014.10.1 Form中显示pdf文件
- [渝粤教育] 武汉理工大学 模拟电子技术基础 参考 资料
- 从何润东代言团宝,看团购行业逐渐成熟
- CentOS 7 忘记root密码的解决之道
- mysql dump语句_mysql/mariadb知识点总结(28):mysql备份工具之mysqldump
- 网络编程在线英英词典之登录模块实现(四)
- html怎么设置目录中间的虚线,Word目录里面的虚线怎么打方法
- android 日记 app推荐,有什么写日记的软件?这4个app推荐给大家!
- printJs 打印HTML 去掉页眉页脚
- python获取交互式ssh shell
- android sim卡槽,包教包会,十分钟让你搞定与或卡槽
热门文章
- mysql存储引擎的区别_Mysql的两种存储引擎以及区别
- 一元三次方程重根判别式_许兴华——关于复数集中解一元二次方程的问题
- linux 字符串截取_linux下可执行文件分析
- Python中的魔法方法
- C#多线程编程系列(五)- 浅析C# Dictionary实现原理
- 第十节:利用async和await简化异步编程模式的几种写法
- 第十一节:WebApi的版本管理的几种方式
- r语言中1c0怎么表示什么,r语言表示或者用什么符号?
- 【POJ - 3177】Redundant Paths(边双连通分量,去重边)
- (1).数据结构概述