利用Java Agent进行代码植入

Java Agent 又叫做 Java 探针,是在 JDK1.5 引入的一种可以动态修改 Java 字节码的技术。可以把javaagent理解成一种代码注入的方式。但是这种注入比起spring的aop更加的优美。

Java agent的使用方式有两种:

  • 实现premain方法,在JVM启动前加载。
  • 实现agentmain方法,在JVM启动后加载。

premain和agentmain函数声明如下,方法名相同情况下,拥有Instrumentation inst参数的方法优先级更高:

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

JVM 会优先加载带 Instrumentation 签名的方法,加载成功忽略第二种;如果第一种没有,则加载第二种方法。

  • 第一个参数String agentArgs就是Java agent的参数。

  • Inst 是一个 java.lang.instrument.Instrumentation 的实例,可以用来类定义的转换和操作等等。

premain方式

JVM启动时 会先执行 premain 方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,就可以结合第三方的字节码编译工具,比如ASM,javassist,cglib等等来改写实现类。

使用实例:

1)创建应用程序Task.jar

先创建一个Task.jar用于模拟在实际场景中的应用程序,Task.java:

public class Task {

    public static void main (String[] args) {
System.out.println("task mian run");
}
}

把Task打成jar包:

此jar包可以单独执行:java -jar Task.jar

2)创建premain方式的Agent

新建一个Agent01.jar,用于在task之前执行:

import java.lang.instrument.Instrumentation;

public class Agent01 {
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("premain run----");
}
}

此时项目如果打成jar包,缺少入口main文件,所以需要自己定义一个MANIFEST.MF文件,用于指明premain的入口在哪里:

src/main/resources/目录下创建META-INF/MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: com.test.Agent01

注意:最后一行是空行,不能省略。以下是MANIFEST.MF的其他选项

Premain-Class: 包含 premain 方法的类(类的全路径名)
Agent-Class: 包含 agentmain 方法的类(类的全路径名)
Boot-Class-Path: 设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)
Can-Redefine-Classes: true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes: true 表示能重转换此代理所需的类,默认值为 false (可选)
Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)

同样的打成jar包:

回顾下我们之前单独运行task.jar时候,控制台前后并没有打印其他信息

现在我们来使用premain进行注入: java -javaagent:Agent01.jar -jar Task.jar

可以看到premain比task先运行,通过启动时候指定参数javaagent来达到注入的效果

以下是先知社区师傅的流程图:

这种方法存在一定的局限性——只能在启动时使用-javaagent参数指定。在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain。相比之下,agentmain更加实用。

agentmain方式

同样使用一个案例来说明使用方式

使用实例:

1)创建应用程序Task.jar

和之前的premain方式一样,创建一个Task.jar作为应用程序:

import java.util.Scanner;

public class Task {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
scanner.hasNext();
}
}

把创建的Task.jar运行起来:java -jar Task.jar

2)创建一个agentmain方式的Agent

创建一个agentmain方式的Agent02.jar,Agent02.java:

import java.lang.instrument.Instrumentation;

public class Agent02 {
public static void agentmain(String agentArgs, Instrumentation inst){
System.out.println("打印全部加载的类:");
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class allLoadedClass : allLoadedClasses) {
System.out.println(allLoadedClass.getName());
}
}
}

同样生成jar包的话,需要手动定义一个MANIFEST.MF文件

Manifest-Version: 1.0
Agent-Class: com.test.Agent02

3)利用VirtualMachine注入

使用VirtualMachine类来利用前面创建的Agent进行代理类注入,VirtualMachine类在jdk目录下的lib/tools.jar包,需要手动导入

package com.test;import com.sun.tools.attach.AgentInitializationException;import com.sun.tools.attach.AgentLoadException;import com.sun.tools.attach.AttachNotSupportedException;import com.sun.tools.attach.VirtualMachine;import java.io.IOException;public class App {    public static void main( String[] args )    {        try {            //VirtualMachine 来自tools.jar            // VirtualMachine.attach("9444") 9444为线程PID,使用jps查看            VirtualMachine vm = VirtualMachine.attach("9444");            //指定要使用的Agent路径            vm.loadAgent("C:\\Users\\xxx\\Desktop\\Agent02.jar");        } catch (AttachNotSupportedException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        } catch (AgentLoadException e) {            e.printStackTrace();        } catch (AgentInitializationException e) {            e.printStackTrace();        }    }}

运行这个名为App的类之后,正在运行的Task程序会执行代码:

以下是先知社区的图:

Java Agent 代码植入

利用agentmain配合Javassist,在方法执行前,修改任意类的方法。在演示之前,先来看几个知识点。

Instrumentation类

在agentmain的构造函数中,第二个参数就是Instrumentation

public static void agentmain(String agentArgs, Instrumentation inst)

这个类就是用来进行aop操作的类,能够替换和修改某些类的定义

public interface Instrumentation {    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。    void addTransformer(ClassFileTransformer transformer);    // 删除一个类转换器    boolean removeTransformer(ClassFileTransformer transformer);    // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;    // 判断目标类是否能够修改。    boolean isModifiableClass(Class<?> theClass);    // 获取目标已经加载的类。    @SuppressWarnings("rawtypes")    Class[] getAllLoadedClasses();    ......}

其中addTransformer()retransformClasses()用来篡改Class的字节码。

从源码中看到addTransformer方法参数中,第一个参数传递的为ClassFileTransformer类型

ClassFileTransformer接口

这是一个接口,它提供了一个transform方法:

public interface ClassFileTransformer {    default byte[]    transform(  ClassLoader         loader,                String              className,                Class<?>            classBeingRedefined,                ProtectionDomain    protectionDomain,                byte[]              classfileBuffer) {        ....    }}

接下来就用一个示例来演示利用agentmain配合Javassist进行代码植入的操作

示例:

1)新建一个hello.jar模拟启动的应用程序

//HelloWorld.javapublic class HelloWorld {    public static void main(String[] args) {        System.out.println("start...");        hello h1 = new hello();        h1.hello();        // 产生中断,等待注入        Scanner sc = new Scanner(System.in);        sc.nextInt();        hello h2 = new hello();        h2.hello();        System.out.println("ends...");    }}//hello.javapublic class hello {    public void hello(){        System.out.println("hello world");    }}

2)创建javaAgent.jar

//AgentDemo.javapackage com.test;import java.io.IOException;import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException;public class AgentDemo {    public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {        Class[] classes = inst.getAllLoadedClasses();        // 判断类是否已经加载        for (Class aClass : classes) {            if (aClass.getName().equals(TransformerDemo.editClassName)) {                // 添加 Transformer                inst.addTransformer(new TransformerDemo(), true);                // 触发 Transformer                inst.retransformClasses(aClass);            }        }    }}//TransformerDemo.javapackage com.test;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;public class TransformerDemo implements ClassFileTransformer {    // 只需要修改这里就能修改别的函数    public static final String editClassName = "com.test.hello";    public static final String editClassName2 = editClassName.replace('.', '/');    public static final String editMethodName = "hello";    @Override    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {        try {            ClassPool cp = ClassPool.getDefault();            if (classBeingRedefined != null) {                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);                cp.insertClassPath(ccp);            }            CtClass ctc = cp.get(editClassName);            CtMethod method = ctc.getDeclaredMethod(editMethodName);            String source = "{System.out.println(\"hello transformer\");}";            method.insertBefore(source);            byte[] bytes = ctc.toBytecode();            ctc.detach();            return bytes;        } catch (Exception e){            e.printStackTrace();        }        return null;    }}

MANIFEST.MF文件中加入

Manifest-Version: 1.0Agent-Class: com.test.AgentDemoCan-Redefine-Classes: trueCan-Retransform-Classes: true

3)利用VirtualMachine注入

package com.test;import com.sun.tools.attach.AgentInitializationException;import com.sun.tools.attach.AgentLoadException;import com.sun.tools.attach.AttachNotSupportedException;import com.sun.tools.attach.VirtualMachine;import java.io.IOException;public class App {    public static void main( String[] args )    {        try {            //VirtualMachine 来自tools.jar            // VirtualMachine.attach("9444") 9444为线程PID            VirtualMachine vm = VirtualMachine.attach("9444");            //指定要使用的Agent路径            vm.loadAgent("C:\\Users\\xxx\\Desktop\\javaAgent.jar");        } catch (AttachNotSupportedException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        } catch (AgentLoadException e) {            e.printStackTrace();        } catch (AgentInitializationException e) {            e.printStackTrace();        }    }}

测试:

运行hello.jar

使用VirtualMachine连接VM,进行注入后,第二次调用hello方法已经成功增加了一行hello transformer

利用Java Agent进行代码植入的更多相关文章

  1. JAVA热部署,通过agent进行代码增量热替换!!!

    在前说明:好久没有更新博客了,这一年在公司做了好多事情,包括代码分析和热部署替换等黑科技,一直没有时间来进行落地写出一些一文章来,甚是可惜,趁着中午睡觉的时间补一篇介绍性的文章吧. 首先热部署的场景是 ...

  2. 利用Java代码在某些时刻创建Spring上下文

    上一篇中,描述了如何使用Spring隐式的创建bean,但当我们需要引进第三方类库添加到我们的逻辑上时,@Conponent与@Autowired是无法添加到类上的,这时,自动装配便不适用了,我们需要 ...

  3. Java Agent初探——动态修改代码

    用了一下午总算把java agent给跑通了,本篇文章记录一下具体的操作步骤,以免遗忘... 通过java agent可以动态修改代码(替换.修改类的定义),进行AOP. 目标: ? 1 为所有添加@ ...

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

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

  5. 探秘 Java 热部署三(Java agent agentmain)

    前言 让我们继续探秘 Java 热部署.在前文 探秘 Java 热部署二(Java agent premain)中,我们介绍了 Java agent premain.通过在main方法之前通过类似 A ...

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

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

  7. JVM插庄之二:Java agent基础原理

    javaagent 简介 Javaagent 只要作用在class被加载之前对其加载,插入我们需要添加的字节码. Javaagent面向的是我们java程序员,而且agent都是用java编写的,不需 ...

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

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

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

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

随机推荐

  1. MZY项目笔记:session歧路

    from my typora MZY项目笔记:session歧路 文章目录 MZY项目笔记:session歧路 那该怎么办? 1. 手动加上cookie的header. 2.自己模拟一个Session ...

  2. Supervisor服务开机自启动

    要解决的问题 在机器上部署自己编写的服务时候,我们可以使用Supervisor作为进程检活工具,用来自动重启服务. 但是当机器重启后,Supervisor却不能自动重启,那么谁来解决这个问题呢? 答案 ...

  3. 战胜了所有对手,却输给了时代。MVVM--jQuery永远的痛。

    前言 第二次浏览器战争中,随着以 Firefox 和 Opera 为首的 W3C 阵营与 IE 对抗程度的加剧,浏览器碎片化问题越来越严重,不同的浏览器执行不同的标准,对于开发人员来说这是一个恶梦.为 ...

  4. SSD算法原理

    Paper: https://arxiv.org/pdf/1512.02325.pdf SSD用神经网络(VGG)提取多层feature map ,来实现对不同大小物体的检测.如下图所示: We us ...

  5. 786. 第k个数

    题目传送门 一.理解感悟 1.这是快速排序模板的练习题. 2.不一样的地方在于它可以利用快排模板,但却不需要真的把所有数据排序完成,每次一分为二后,只关心自己所有的那一半,就是可以节约一半的递归. 3 ...

  6. python 动图gif合成与分解

    合成 #!/usr/bin/env python3 # -*- coding: utf-8 -*- import os import sys import imageio def main(imgs_ ...

  7. vue 封装 axios 和 各类的请求,以及引入 .vue 文件中使用

    //src 底下建立 api 文件夹 // api 文件夹下建立 request,js 文件,文件内容复制下面这段代码即可   /**  * ajax请求配置  */ import axios fro ...

  8. HashMap 为什么线程不安全?

    作者:developer http://cnblogs.com/developer_chan/p/10450908.html 我们都知道HashMap是线程不安全的,在多线程环境中不建议使用,但是其线 ...

  9. Mybatis-Plus常用的查询方法--看这一篇就够了!!!

    前言: Mybatis-Plus作为Mybatis的增强,自己封装了很多简单还用的方法,来解脱自己写sql! 对于项目的搭建小编就不在说了,可以参考: SpringBoot+Mybatis-Plus的 ...

  10. 博主有偿带徒 《编程语言设计和实现》《MUD游戏开发》《软件破解和加密》《游戏辅助外挂》《JAVA开发》

    <考研专题>操作系统原理 理论解答:8K 实战 1.5W CPU设计 理论解答:1W 实战 2.5W <编程语言设计和实现>初窥门径<5K>:编译原理.编译设计小试 ...