简单来说,Java Agent就是JVM为了应用程序提供的具有检测功能的软件组件。在Java Agent的上下文中,通过JDK1.5出现的java.lang.instrument.Instrumentation来提供重新定义在运行时加载的类的内容的能力。那么这有什么用?其实对我们实现一些需要通过字节码的形式隐式注入到业务代码中的中间件非常有用(注意,这和Java的远程debug使用的JDWP(Java Debug Wire Protocol)原理不同),比较典型的有韩国Naver开源的应用性能管理工具Pinpoint(https://github.com/naver/pinpoint),当然,Java Agent还能实现动态对运行的Java应用进行字节码注入,做到“窥探”运行时的信息,典型的代表有Java追踪工具BTrace(https://github.com/btraceio/btrace)、阿里开源的JVM-SANDBOX(https://github.com/alibaba/jvm-sandbox)、Java在线问题诊断工具Greys(https://github.com/oldmanpushcart/greys-anatomy)等。
Java Agent的最常用方式是premain方式,它属于静态注入,即在Java应用程序启动时,在类加载器对类的字节码进行加载之前对类字节码进行“再改造”来做功能增强(例如实现AOP),另一种方式是HotSpot独有的attach方式(JDK1.6才出现),它能实现动态注入,即对已经运行的Java应用的类进行字节码增强。
说得这么神乎其神,我们直接举例来看!!!
我们先看看premain方式,前面说了,它属于静态注入,通过引用一个本地的Jar来在Java应用启动时去做类的增强:
java -javaagent:/root/application-premain.jar MyApplication
如上面这句java命令,我们假定/root目录下已经有一个符合Java Agent规范的Jar了(这里指application-premain.jar),而MyApplication指的是我们Java应用的启动类(main方法的类),于是我们就成功的对这个Java应用进行了静态注入,那我们接下来看看这个application-premain.jar需要遵循什么规范才能让JVM识别。
我们先看看MyApplication,假定这个Java应用相当简单,里面就一个打印语句:
package com.mzz.study.javaagent; import java.util.concurrent.TimeUnit; /** * 某个jvm进程 */ public class MyApplication { public static void main(String[] args) { while (true) { testPrint(); } } private static void testPrint() { System.out.println("这是我第 " + (System.currentTimeMillis() / 1000) + " 次想你"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } } }
假如我们希望通过代码植入在testPrint()方法的开始和结束各打印一个语句,那么我们该如何做?这个就是前面application-premain.jar要做的事情了,我们新建一个Maven项目,项目结构如下:
这里我们先重点关注premian包下面的MyTransformer和PremainMain类,既然是修改字节码,我们当然得告诉JVM该如何修改,这个是由MyTransformer类来完成的,具体代码如下:
package com.mzz.study.javaagent.premain; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import java.util.Objects; public class MyTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { //java自带的方法不进行处理 if(className.startsWith("java") || className.startsWith("sun")){ return classfileBuffer; } /** * 好像使用premain这个className是没问题的,但使用attach时className的.变成了/,所以如果是attach,那么这里需要替换 */ className = className.replace('/', '.'); // 只处理MyApplication类 if(!className.endsWith("MyApplication")) { return classfileBuffer; } try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get(className); CtMethod[] declaredMethods = ctClass.getDeclaredMethods(); for (CtMethod declaredMethod : declaredMethods) { // 只处理testPrint方法 if(Objects.equals("testPrint", declaredMethod.getName())) { /** * 在方法执行之前加入打印语句 */ declaredMethod.insertBefore("System.out.println(\"欧,亲爱的,\");"); /** * 在方法执行之后加入打印语句 */ declaredMethod.insertAfter("System.out.println(\"祝你一切安好!\");"); } } return ctClass.toBytecode(); } catch (Exception e) { e.printStackTrace(); } return classfileBuffer; } }
可以看出,我们可以看到MyTransformer实现了ClassFileTransformer接口,ClassFileTransformer是专门为Java Agent提供类转换功能的接口,其中有transform方法,在transform方法中,我们可以大显身手了,从上面代码片段可以看出,我们只是对MyApplication#testPrint方法的之前和末尾各加入了一条打印语句,你可能会奇怪,不是字节码吗?为啥可以直接像表达式引擎一样直接输入Java表达式?是因为这里使用了javassist这一轻量级的字节码工具,它帮我们屏蔽了字节码的细节,使我们可以只关注Java代码。
有了MyTransformer,在哪用?答案就在PremainMain类中,PremainMain要做的事情很简单,就是把我们自定义的类转换器MyTransformer加到前面提到的Instrumentation实例中:
package com.mzz.study.javaagent.premain; import java.lang.instrument.Instrumentation; public class PremainMain { /** * 注意,这个premain方法签名是Java Agent约定的,不要随意修改 * @param agentArgs * @param instrumentation */ public static void premain(String agentArgs, Instrumentation instrumentation) { instrumentation.addTransformer(new MyTransformer()); } }
需要注意的是,PremainMain#premain的方法签名是Java Agent内部约定的,不能随意修改。既然说是内部约定的,那么Java Agent是怎么知道premain方法在哪个类中呢?这是个好问题,答案就是在项目中resources/META-INF/MANIFEST.MF文件中,MANIFEST.MF文件内容如下:
Manifest-Version: 1.0 Created-By: manzhizhen Premain-Class: com.mzz.study.javaagent.premain.PremainMain
注意最后一行需要留一个空行,到目前为止,所有文件已经就绪,我们需要把它打成一个jar包(需要包含javassist),于是我们用到了maven-assembly-plugin插件,项目的pom.xml文件内容如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.mzz</groupId> <artifactId>mzz-study-javaagent</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>mzz-study-javaagent</name> <dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.21.0-GA</version> </dependency> <!-- 注意:如果需要进行attach,那么需要引入tools.jar --> <dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.8</version> <scope>system</scope> <systemPath>${java.home}/../lib/tools.jar</systemPath> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <source>1.7</source> <target>1.7</target> <encoding>UTF-8</encoding> </configuration> <dependencies> <dependency> <groupId>org.codehaus.plexus</groupId> <artifactId>plexus-compiler-eclipse</artifactId> <version>1.9.1</version> </dependency> </dependencies> </plugin> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>3.1.1</version> <configuration> <archive> <!--避免MANIFEST.MF被覆盖--> <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile> </archive> <descriptorRefs> <!--打包时加入依赖--> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
我们直接执行Maven的打包命令:
mvn clean package
这里假定打包得到的jar是:application-premain.jar
一切就绪了,本以为直接可以在idea运行MyApplication时带上-javaagent参数,但实际上会报错,所以我们把application-premain.jar和MyApplication.java都拷贝到/root目录下(注意,为了方便javac,需要去掉MyApplication.java中package com.mzz.study.javaagent;)。
cd到/root目录下,执行:
javac MyApplication.java
于是我们看到/root下多了一个MyApplication.class字节码文件。后面直接执行我们上面提到的命令:
java -javaagent:/root/application-premain.jar MyApplication
于是我们在控制台看到了如下效果:
我们可以看到,原本只有一个打印语句的MyApplication类,在前后加了两条打印语句,目标达成!!!!!
前面我们也说了,Naver开源的应用性能管理工具Pinpoint用的就是静态注入这一招。接下来,我们看看如果应用程序已经运行,我们如何动态注入字节码,还是使用刚才这个项目,我们新增AttachAgent类:
package com.mzz.study.javaagent.attach; import com.mzz.study.javaagent.premain.MyTransformer; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; public class AttachAgent { /** * 注意:agentmain的方法签名也是约定好的,不能随意修改 * * 其实如果要支持premain和attach两种方式的话,可以把premain和agentmain两个方法写在一个类里,这里为了方便演示,写成了两个 * * @param agentArgs * @param instrumentation */ public static void agentmain(String agentArgs, Instrumentation instrumentation) { String targetClassPath = "com.mzz.study.javaagent.MyApplication"; for (Class<?> clazz : instrumentation.getAllLoadedClasses()) { // 过滤掉不能修改的类 if(!instrumentation.isModifiableClass(clazz)) { continue; } // 这里只修改我们关心的类 if (clazz.getName().equals(targetClassPath)) { // 最根本的目的还是把MyTransformer添加到instrumentation中 instrumentation.addTransformer(new MyTransformer(), true); try { instrumentation.retransformClasses(clazz); } catch (UnmodifiableClassException e) { e.printStackTrace(); } return; } } } }
这里约定好的方法是agentmain,不在是premain,但agentmain方法的本质也是把MyTransformer添加到instrumentation中,AttachAgent类准备好后,同样的问题来了,Java Agent如何知道AttachAgent中有它想要的agentmain方法?同理秘密还是在MANIFEST.MF文件中,在MANIFEST.MF中添加如下三行:
Manifest-Version: 1.0 Created-By: manzhizhen Premain-Class: com.mzz.study.javaagent.premain.PremainMain Agent-Class: com.mzz.study.javaagent.attach.AttachAgent Can-Redefine-Classes: true Can-Retransform-Classes: true
最关键的是Agent-Class这个key,指明了使用AttachAgent类,注意,同样最后需要多留一个空行。
准备好后,我们同样需要打一个包(注意,pom.xml文件需要依赖tools,参见上面给的pom.xml全文),执行如下命令打包:
mvn clean package
这里假定打包得到的jar还是(别忘了拷贝到/root目录下):application-premain.jar
注意,这个attach的case可以直接用idea演示,我们先在idea中直接运行MyApplication,于是我们看到如下源源不断的输出:
我们使用jps命令来看下MyApplication的进程ID,假如是:1234,于是我们创建一个AttachMain类,来准备通过它来对MyApplication动态注入字节码:
package com.mzz.study.javaagent.attach; import com.sun.tools.attach.VirtualMachine; import java.io.File; import java.util.concurrent.TimeUnit; /** * 先运行需要植入代码的MyApplication类 */ public class AttachMain { public static void main(String[] args) { // MyApplication的jvm进程ID String jvmPid = "1234"; File agentFile = new File("/root/application-premain.jar"); if (!agentFile.isFile()) { System.out.println("jar 不存在"); return; } try { VirtualMachine jvm = VirtualMachine.attach(jvmPid); jvm.loadAgent(agentFile.getAbsolutePath()); jvm.detach(); System.out.println("attach 成功"); } catch (Exception e) { e.printStackTrace(); } try { TimeUnit.SECONDS.sleep(10000); } catch (InterruptedException e) { } } }
这里需要关注的就是VirtualMachine类,它进行attach时需要知道目标Java进程的ID,因为不需要输入IP,所以它的设定是只支持本地attach,准备好AttachMain后,直接运行AttachMain,我们可以看到MyApplication的控制台输出变了:
说明我们确实动态对MyApplication注入了字节码。attach的这种动态字节码注入方式衍生出了很多工具,像本文开头提到的JVM-SANDBOX、BTrace和Greys等。
好了,本文就到这里了,希望对各位能有所帮助。
相关推荐
使用`-javaagent`参数指定你的Agent jar路径,以及`premain`方法的参数,如果有的话。 7. **收集和展示数据**:在Agent中,你需要实现数据的收集和展示。这可能包括将统计信息记录到日志文件,或者通过Socket发送到...
Java爬虫项目实战源码是学习和掌握网络爬虫技术的一种实用方式,它涵盖了从数据抓取、数据处理到数据存储等一系列步骤。本项目实战源码提供了完整的代码实现,可以帮助开发者深入理解Java爬虫的工作原理,并能快速...
在Java爬虫实战训练中,我们可能还会涉及到反爬策略的应对,如设置User-Agent、Cookies,模拟登录,以及处理验证码和动态加载的内容。对于动态加载内容,Selenium库能帮助我们模拟浏览器行为,执行JavaScript,获取...
Java Nebula 客户端集成详解 Java Nebula 客户端集成是指将 Nebula 图数据库与 Java 应用程序集成,以便在 Java 应用程序中使用 Nebula 图数据库的功能。下面是 Java Nebula 客户端集成的详细过程。 一、建立 ...
Java爬虫实战项目源码是针对有一定编程基础的开发者设计的学习资源,主要涵盖了Java语言在爬虫领域的应用。这个项目提供了完整的源代码,允许用户自行导入到编译工具中进行运行,以加深对爬虫技术的理解和实践。下面...
Java爬虫项目实战源码是针对想要学习或深入理解Java爬虫技术的开发者提供的一份实践性极强的学习资源。这个压缩包包含了完整的项目代码,可以让学习者通过实际操作来了解和掌握网络爬虫的开发过程。以下是这个项目中...
Java爬虫项目实战源码是针对计算机网络技术、毕业设计以及Java编程语言的一份实践教程。这个项目旨在帮助开发者深入理解和应用Java爬虫技术,从而能够有效地从互联网上抓取和处理数据。以下是对该项目中可能包含的...
Java爬虫项目实战是IT行业中一个非常实用且热门的话题,尤其对于数据挖掘、数据分析和自动化测试等领域至关重要。在这个项目中,我们将深入探讨Java语言在构建网络爬虫时的应用,通过源码分享和实战案例,来提升对...
本书作为“Wiley Series in Agent Technology”系列的一部分,不仅提供了理论基础,还深入探讨了如何利用Java Agent DEvelopment (JADE)框架来构建实用的多Agent系统。 #### JADE概述 JADE(Java Agent ...
在这个名为"Java爬虫项目实战源码.zip"的压缩包中,包含了一个完整的Java爬虫项目的源代码。这个项目旨在帮助开发者深入理解如何利用Java语言进行网络数据抓取,为数据分析、信息自动化或其他相关用途提供基础。以下...
Java爬虫项目实战源码是Java开发者学习网络爬虫技术的重要参考资料,尤其对于那些希望提升自己在Java领域技能的人来说,这是一个宝贵的资源。本资源包含了完整的Java爬虫项目的源代码,可以帮助学习者深入理解如何...
Java爬虫项目实战源码是学习和开发网络爬虫的重要资源,它可以帮助开发者深入理解爬虫的工作原理,提升编程技能,特别是对于Java编程语言的使用者。在这个压缩包中,我们很可能会找到一系列的Java源代码文件,它们...
根据提供的文件信息,本文将重点解析“Java软件开发实战”中的第19章关于URL和URLConnection类的内容。这部分内容深入探讨了如何利用这两个类来进行网络通信,并提供了实用的示例来帮助理解这些概念。 ### 19.4 URL...
8. **实战应用**:Java爬虫在数据挖掘、市场分析、舆情监控等领域有广泛应用。通过爬虫获取的数据可以用于产品推荐、用户行为分析、竞争情报收集等多种业务场景。 在学习和实践Java爬虫的过程中,需要不断学习新的...
我们将探讨如何使用Java实现模拟浏览器行为,更换User-Agent,以及使用代理IP等策略来应对这些挑战。 9. **实战项目**:通过实际的网络机器人项目,比如微博爬虫或电商商品信息抓取,将理论知识转化为实践,巩固...
然而,它需要通过Maven或Gradle运行,与IDEA的集成并不理想,使用javaagent参数时可能增加复杂性,且缺乏IDE的即时提示功能。 2. 技术选型: 综合考虑,选择Mockito结合PowerMock的方案。Mockito用于处理大部分...