最全偷天换日,用JavaAgent欺骗你的JVM(1),十年Java编程开发生涯

最后

看完美团、字节、腾讯这三家的面试问题,是不是感觉问的特别多,可能咱们又得开启面试造火箭、工作拧螺丝的模式去准备下一次的面试了。

开篇有提及我可是足足背下了1000道题目,多少还是有点用的呢,我看了下,上面这些问题大部分都能从我背的题里找到的,所以今天给大家分享一下互联网工程师必备的面试1000题

注意不论是我说的互联网面试1000题,还是后面提及的算法与数据结构、设计模式以及更多的Java学习笔记等,皆可分享给各位朋友

最新“美团+字节+腾讯”一二三面问题,挑战一下你能走到哪一面?

互联网工程师必备的面试1000题

而且从上面三家来看,算法与数据结构是必备不可少的呀,因此我建议大家可以去刷刷这本左程云大佬著作的《程序员代码面试指南 IT名企算法与数据结构题目最优解》,里面近200道真实出现过的经典代码面试题

最新“美团+字节+腾讯”一二三面问题,挑战一下你能走到哪一面?

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

主程序

在主程序的工程中,只需要一个能够执行的main方法的入口就可以了。

public class AgentTest {

public static void main(String[] args) {

System.out.println(“main project start”);

}

}

在主程序完成后,要考虑的就是应该如何将主程序与agent工程连接起来。这里可以通过-javaagent参数来指定运行的代理,命令格式如下:

java -javaagent:myAgent.jar -jar AgentTest.jar

并且,可以指定的代理的数量是没有限制的,会根据指定的顺序先后依次执行各个代理,如果要同时运行两个代理,就可以按照下面的命令执行:

java -javaagent:myAgent1.jar -javaagent:myAgent2.jar  -jar AgentTest.jar

以我们在idea中执行程序为例,在VM options中加入添加启动参数:

-javaagent:F:\Workspace\MyAgent\target\myAgent-1.0.jar=Hydra

-javaagent:F:\Workspace\MyAgent\target\myAgent-1.0.jar=Trunks

执行main方法,查看输出结果:

图片

根据执行结果的打印语句可以看出,在执行主程序前,依次执行了两次我们的agent代理。可以通过下面的图来表示执行代理与主程序的执行顺序。

图片

缺陷

在提供便利的同时,premain模式也有一些缺陷,例如如果agent在运行过程中出现异常,那么也会导致主程序的启动失败。我们对上面例子中agent的代码进行一下改造,手动抛出一个异常。

public static void premain(String agentArgs, Instrumentation inst) {

System.out.println(“premain start”);

System.out.println(“args:”+agentArgs);

throw new RuntimeException(“error”);

}

再次运行主程序:

图片

可以看到,在agent抛出异常后主程序也没有启动。针对premain模式的一些缺陷,在jdk1.6之后引入了agentmain模式。

2、Agentmain模式


agentmain模式可以说是premain的升级版本,它允许代理的目标主程序的jvm先行启动,再通过attach机制连接两个jvm,下面我们分3个部分实现。

agent

agent部分和上面一样,实现简单的打印功能:

public class MyAgentMain {

public static void agentmain(String agentArgs, Instrumentation instrumentation) {

System.out.println(“agent main start”);

System.out.println(“args:”+agentArgs);

}

}

修改maven插件配置,指定Agent-Class:

org.apache.maven.plugins

maven-jar-plugin

3.1.0

true

com.cn.agent.MyAgentMain

true

true

主程序

这里我们直接启动主程序等待代理被载入,在主程序中使用了System.in进行阻塞,防止主进程提前结束。

public class AgentmainTest {

public static void main(String[] args) throws IOException {

System.in.read();

}

}

attach机制

和premain模式不同,我们不能再通过添加启动参数的方式来连接agent和主程序了,这里需要借助com.sun.tools.attach包下的VirtualMachine工具类,需要注意该类不是jvm标准规范,是由Sun公司自己实现的,使用前需要引入依赖:

com.sun

tools

1.8

system

${JAVA_HOME}\lib\tools.jar

VirtualMachine代表了一个要被附着的java虚拟机,也就是程序中需要监控的目标虚拟机,外部进程可以使用VirtualMachine的实例将agent加载到目标虚拟机中。先看一下它的静态方法attach:

public static VirtualMachine attach(String var0);

通过attach方法可以获取一个jvm的对象实例,这里传入的参数是目标虚拟机运行时的进程号pid。也就是说,我们在使用attach前,需要先获取刚才启动的主程序的pid,使用jps命令查看线程pid:

11140

16372 RemoteMavenServer36

16392 AgentmainTest

20204 Jps

2460 Launcher

获取到主程序AgentmainTest运行时pid是16392,将它应用于虚拟机的连接。

public class AttachTest {

public static void main(String[] args) {

try {

VirtualMachine vm= VirtualMachine.attach(“16392”);

vm.loadAgent(“F:\Workspace\MyAgent\target\myAgent-1.0.jar”,“param”);

} catch (Exception e) {

e.printStackTrace();

}

}

}

在获取到VirtualMachine实例后,就可以通过loadAgent方法可以实现注入agent代理类的操作,方法的第一个参数是代理的本地路径,第二个参数是传给代理的参数。执行AttachTest,再回到主程序AgentmainTest的控制台,可以看到执行了了agent中的代码:

图片

这样,一个简单的agentMain模式代理就实现完成了,可以通过下面这张图再梳理一下三个模块之间的关系。

图片

3、应用


到这里,我们就已经简单地了解了两种模式的实现方法,但是作为高质量程序员,我们肯定不能满足于只用代理单纯地打印语句,下面我们再来看看能怎么利用Java Agent搞点实用的东西。

在上面的两种模式中,agent部分的逻辑分别是在premain方法和agentmain方法中实现的,并且,这两个方法在签名上对参数有严格的要求,premain方法允许以下面两种方式定义:

public static void premain(String agentArgs)

public static void premain(String agentArgs, Instrumentation inst)

agentmain方法允许以下面两种方式定义:

public static void agentmain(String agentArgs)

public static void agentmain(String agentArgs, Instrumentation inst)

如果在agent中同时存在两种签名的方法,带有Instrumentation参数的方法优先级更高,会被jvm优先加载,它的实例inst会由jvm自动注入,下面我们就看看能通过Instrumentation实现什么功能。

Instrumentation

先大体介绍一下Instrumentation接口,其中的方法允许在运行时操作java程序,提供了诸如改变字节码,新增jar包,替换class等功能,而通过这些功能使Java具有了更强的动态控制和解释能力。在我们编写agent代理的过程中,Instrumentation中下面3个方法比较重要和常用,我们来着重看一下。

addTransformer

addTransformer方法允许我们在类加载之前,重新定义Class,先看一下方法的定义:

void addTransformer(ClassFileTransformer transformer);

ClassFileTransformer是一个接口,只有一个transform方法,它在主程序的main方法执行前,装载的每个类都要经过transform执行一次,可以将它称为转换器。我们可以实现这个方法来重新定义Class,下面就通过一个例子看看具体如何使用。

首先,在主程序工程创建一个Fruit类:

public class Fruit {

public void getFruit(){

System.out.println(“banana”);

}

}

编译完成后复制一份class文件,并将其重命名为Fruit2.class,再修改Fruit中的方法为:

public void getFruit(){

System.out.println(“apple”);

}

创建主程序,在主程序中创建了一个Fruit对象并调用了其getFruit方法:

public class TransformMain {

public static void main(String[] args) {

new Fruit().getFruit();

}

}

这时执行结果会打印apple,接下来开始实现premain代理部分。

在代理的premain方法中,使用Instrumentation的addTransformer方法拦截类的加载:

public class TransformAgent {

public static void premain(String agentArgs, Instrumentation inst) {

inst.addTransformer(new FruitTransformer());

}

}

FruitTransformer类实现了ClassFileTransformer接口,转换class部分的逻辑都在transform方法中:

public class FruitTransformer implements ClassFileTransformer {

@Override

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,

ProtectionDomain protectionDomain, byte[] classfileBuffer){

if (!className.equals(“com/cn/hydra/test/Fruit”))

return classfileBuffer;

String fileName=“F:\Workspace\agent-test\target\classes\com\cn\hydra\test\Fruit2.class”;

return getClassBytes(fileName);

}

public static byte[] getClassBytes(String fileName){

File file = new File(fileName);

try(InputStream is = new FileInputStream(file);

ByteArrayOutputStream bs = new ByteArrayOutputStream()){

long length = file.length();

byte[] bytes = new byte[(int) length];

int n;

while ((n = is.read(bytes)) != -1) {

bs.write(bytes, 0, n);

}

return bytes;

}catch (Exception e) {

e.printStackTrace();

return null;

}

}

}

在transform方法中,主要做了两件事:

因为addTransformer方法不能指明需要转换的类,所以需要通过className判断当前加载的class是否我们要拦截的目标class,对于非目标class直接返回原字节数组,注意className的格式,需要将类全限定名中的.替换为/

  • 读取我们之前复制出来的class文件,读入二进制字符流,替换原有classfileBuffer字节数组并返回,完成class定义的替换

将agent部分打包完成后,在主程序添加启动参数:

-javaagent:F:\Workspace\MyAgent\target\transformAgent-1.0.jar

再次执行主程序,结果打印:

banana

这样,就实现了在main方法执行前class的替换。

redefineClasses

我们可以直观地从方法的名字上来理解它的作用,重定义class,通俗点来讲的话就是实现指定类的替换。方法定义如下:

void redefineClasses(ClassDefinition… definitions) throws  ClassNotFoundException, UnmodifiableClassException;

它的参数是可变长的ClassDefinition数组,再看一下ClassDefinition的构造方法:

public ClassDefinition(Class<?> theClass,byte[] theClassFile) {…}

ClassDefinition中指定了的Class对象和修改后的字节码数组,简单来说,就是使用提供的类文件字节,替换了原有的类。并且,在redefineClasses方法重定义的过程中,传入的是ClassDefinition的数组,它会按照这个数组顺序进行加载,以便满足在类之间相互依赖的情况下进行更改。

下面通过一个例子来看一下它的生效过程,premain代理部分:

public class RedefineAgent {

public static void premain(String agentArgs, Instrumentation inst)

throws UnmodifiableClassException, ClassNotFoundException {

String fileName=“F:\Workspace\agent-test\target\classes\com\cn\hydra\test\Fruit2.class”;

ClassDefinition def=new ClassDefinition(Fruit.class,

FruitTransformer.getClassBytes(fileName));

inst.redefineClasses(new ClassDefinition[]{def});

}

}

主程序可以直接复用上面的,执行后打印:

banana

可以看到,用我们指定的class文件的字节替换了原有类,即实现了指定类的替换。

retransformClasses

retransformClasses应用于agentmain模式,可以在类加载之后重新定义Class,即触发类的重新加载。首先看一下该方法的定义:

void retransformClasses(Class<?>… classes) throws UnmodifiableClassException;

它的参数classes是需要转换的类数组,可变长参数也说明了它和redefineClasses方法一样,也可以批量转换类的定义。

下面,我们通过例子来看看如何使用retransformClasses方法,agent代理部分代码如下:

public class RetransformAgent {

public static void agentmain(String agentArgs, Instrumentation inst)

throws UnmodifiableClassException {

inst.addTransformer(new FruitTransformer(),true);

inst.retransformClasses(Fruit.class);

System.out.println(“retransform success”);

}

}

看一下这里调用的addTransformer方法的定义,与上面略有不同:

void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

写在最后

还有一份JAVA核心知识点整理(PDF):JVM,JAVA集合,JAVA多线程并发,JAVA基础,Spring原理,微服务,Netty与RPC,网络,日志,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算…

image

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

rmer(),true);

inst.retransformClasses(Fruit.class);

System.out.println(“retransform success”);

}

}

看一下这里调用的addTransformer方法的定义,与上面略有不同:

void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

写在最后

还有一份JAVA核心知识点整理(PDF):JVM,JAVA集合,JAVA多线程并发,JAVA基础,Spring原理,微服务,Netty与RPC,网络,日志,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算…

[外链图片转存中…(img-NiA1pqsl-1715594693685)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值