当前位置: 首页 > news >正文

Java Agent通灵之术

一、概述

1、通灵之术

通灵之术:在《火影忍者》中,通灵之术,属于时空间忍术的一种。

如下图,可以召唤通灵兽。

那么,"通灵之术",在Java领域,代表什么意思呢? 

就是将正在运行的JVM当中的class进行导出。

借助Java Agent将class文件从JVM当中导出。

  HelloWorld.class在JVM中运行,通过Java Agent把 HelloWorld.class从JVM中下载出来。

2、Java Agent

        曾经有一篇文章《Retrieving .class files from a running app》,最初是发表在Sun公司的网站,后来转移到了Oracle的网站,再后来就从Oracle网站消失了。

        Sometimes it is better to dump .class files of generated/modified classes for off-line debugging - for example, we may want to view such classes using tools like jclasslib.

===============================================================

ClassDumpAgent.java

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.ArrayList;
import java.util.List;

/**
 * This is a java.lang.instrument agent to dump .class files
 * from a running Java application.
 */
public class ClassDumpAgent {
	
    public static void premain(String agentArgs, Instrumentation inst) {
        agentmain(agentArgs, inst);
    }

    /**
     * 
     * @param agentArgs:输出路径/正则表达式
     * @param inst:JVM传参
     */
    @SuppressWarnings("rawtypes")
	public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentArgs: " + agentArgs);
        ClassDumpUtils.parseArgs(agentArgs);
        inst.addTransformer(new ClassDumpTransformer(), true);
        // by the time we are attached, the classes to be
        // dumped may have been loaded already.
        // So, check for candidates in the loaded classes.
        Class[] classes = inst.getAllLoadedClasses();
        List<Class> candidates = new ArrayList<>();
        for (Class c : classes) {
            String className = c.getName();

            // 第一步,排除法:不考虑JDK自带的类
            if (className.startsWith("java")) continue;
            if (className.startsWith("javax")) continue;
            if (className.startsWith("jdk")) continue;
            if (className.startsWith("sun")) continue;
            if (className.startsWith("com.sun")) continue;

            // 第二步,筛选法:只留下感兴趣的类(正则表达式匹配)
            boolean isModifiable = inst.isModifiableClass(c);
            boolean isCandidate = ClassDumpUtils.isCandidate(className);
            if (isModifiable && isCandidate) {
                candidates.add(c);
            }

            // 不重要:打印调试信息
            String message = String.format("[DEBUG] Loaded Class: %s ---> Modifiable: %s, Candidate: %s", className, isModifiable, isCandidate);
            System.out.println(message);
        }
        try {
            // 第三步,将具体的class进行dump操作
            // if we have matching candidates, then retransform those classes
            // so that we will get callback to transform.
            if (!candidates.isEmpty()) {
                inst.retransformClasses(candidates.toArray(new Class[0]));

                // 不重要:打印调试信息
                String message = String.format("[DEBUG] candidates size: %d", candidates.size());
                System.out.println(message);
            }
        }
        catch (UnmodifiableClassException ignored) {
        }
    }
}

ClassDumpTransformer.java

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class ClassDumpTransformer implements ClassFileTransformer {

    @SuppressWarnings("rawtypes")
	public byte[] transform(ClassLoader loader,
                            String className,
                            Class redefinedClass,
                            ProtectionDomain protDomain,
                            byte[] classBytes) {
        // check and dump .class file
        if (ClassDumpUtils.isCandidate(className)) {
            ClassDumpUtils.dumpClass(className, classBytes);
        }

        // we don't mess with .class file, just return null
        return null;
    }

}

ClassDumpUtils.java

import java.io.File;
import java.io.FileOutputStream;
import java.util.regex.Pattern;

public class ClassDumpUtils {
	
    // directory where we would write .class files
	// .class 输出路径
    private static String dumpDir;
    // classes with name matching this pattern will be dumped
    // 正则表达式
    private static Pattern classes;

    // parse agent args of the form arg1=value1,arg2=value2
    public static void parseArgs(String agentArgs) {
        if (agentArgs != null) {
            String[] args = agentArgs.split(",");
            for (String arg : args) {
                String[] tmp = arg.split("=");
                if (tmp.length == 2) {
                    String name = tmp[0];
                    String value = tmp[1];
                    if (name.equals("dumpDir")) {
                        dumpDir = value;
                    }
                    else if (name.equals("classes")) {
                        classes = Pattern.compile(value);
                    }
                }
            }
        }
        if (dumpDir == null) {
            dumpDir = ".";
        }
        if (classes == null) {
            classes = Pattern.compile(".*");
        }
        System.out.println("[DEBUG] dumpDir: " + dumpDir);
        System.out.println("[DEBUG] classes: " + classes);
    }

    public static boolean isCandidate(String className) {
        // ignore array classes
        if (className.charAt(0) == '[') {
            return false;
        }
        // convert the class name to external name
        className = className.replace('/', '.');
        // check for name pattern match
        return classes.matcher(className).matches();
    }

    public static void dumpClass(String className, byte[] classBuf) {
        try {
            // create package directories if needed
            className = className.replace("/", File.separator);
            StringBuilder buf = new StringBuilder();
            buf.append(dumpDir);
            buf.append(File.separatorChar);
            int index = className.lastIndexOf(File.separatorChar);
            if (index != -1) {
                String pkgPath = className.substring(0, index);
                buf.append(pkgPath);
            }
            String dir = buf.toString();
            new File(dir).mkdirs();
            // write .class file
            String fileName = dumpDir + File.separator + className + ".class";
            FileOutputStream fos = new FileOutputStream(fileName);
            fos.write(classBuf);
            fos.close();
            System.out.println("[DEBUG] FileName: " + fileName);
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }

}

manifest.txt

Premain-Class: ClassDumpAgent
Agent-Class: ClassDumpAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

注意:在结尾处添加一个空行。

===>编译打包

把上面文件放到同目录下,进行编译,打包生成classdumper.jar文件。

1、进行编译
javac src/ClassDump*.java
2、在Windows操作系统,如果遇到错误: 编码GBK的不可映射字符
3、可以添加-encoding选项:
javac -encoding UTF-8 src/ClassDump*.java
4、生成jar文件
jar -cvfm classdumper.jar manifest.txt ClassDump*.class

3、测试代码

HelloWorld.java

public class HelloWorld {
	
	public static int add(int a,int b) {
		return a + b;
	}
	
	public static int sub(int a,int b) {
		return a - b;
	}

}

Program.java---->测试启动程序

import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class Program {

	public static void main(String[] args) throws InterruptedException {
		
		//打印进程ID
		String runningMxName = ManagementFactory.getRuntimeMXBean().getName();
		System.out.println(runningMxName);
		
		int count = 600;
		for (int i = 0; i < count; i++) {
			String info = String.format("|%03d| %s remains %03d seconds", i,runningMxName,(count - i));
			System.out.println(info);
			
			Random rand = new Random(System.currentTimeMillis());
			int a = rand.nextInt(10);
			int b = rand.nextInt(10);
			
			boolean flag = rand.nextBoolean();
			String message;
			if (flag) {
				message = String.format("a + b = %d", HelloWorld.add(a, b));
			}else {
				message = String.format("a - b = %d", HelloWorld.sub(a, b));
			}
			
			System.out.println(message);
			TimeUnit.SECONDS.sleep(1);
		}
	}

}

此测试代码,可以编译运行,可以在idea/eclipse中运行。

4、Tools Attach

将一个Agent Jar与一个正在运行的java程序建立联系,需要用到Attach机制:

Agent Jar ---> Tools Attach ---> JAVA运行程序(JVM)

与Attach机制相关的类,定义在tools.jar文件:
JDK_HOME/lib/tools.jar

Attach.java-->启动需要传3个参数:

        <pid> <agent-jar-full-path> [<agent-args>]

        进程ID agent打包jar全路径 参数:.class出力路径,正则表达式限制.class条件

import com.sun.tools.attach.VirtualMachine;

/**
 * Simple attach-on-demand client tool
 * that loads the given agent into the given Java process.
 */
public class Attach {
	
    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            System.out.println("usage: java Attach <pid> <agent-jar-full-path> [<agent-args>]");
            System.exit(1);
        }
        // JVM is identified by process id (pid).
        VirtualMachine vm = VirtualMachine.attach(args[0]);
        String agentArgs = (args.length > 2) ? args[2] : null;
        // load a specified agent onto the JVM
        vm.loadAgent(args[1], agentArgs);
        vm.detach();
        System.out.println("complete");
    }
}

Attach代码,可以编译运行,也可以在idea/eclipse中运行。

需要引入jar包JDK_HOME/lib/tools.jar

编译

# 编译(Linux)
$ javac -cp "${JAVA_HOME}/lib/tools.jar":. Attach.java 

# 编译(MINGW64)
$ javac -cp "${JAVA_HOME}/lib/tools.jar"\;. Attach.java 

# 编译(Windows)
$ javac -cp "%JAVA_HOME%/lib/tools.jar";. Attach.java 

运行

# 运行(Linux)
java -cp "${JAVA_HOME}/lib/tools.jar":. Attach <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern>

# 运行(MINGW64)
java -cp "${JAVA_HOME}/lib/tools.jar"\;. Attach <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern>

# 运行(Windows)
java -cp "%JAVA_HOME%/lib/tools.jar";. Attach <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern>

示例:

# 运行(Windows)
java -cp "%JAVA_HOME%/lib/tools.jar";. Attach <pid> D:/dev/classdumper.jar dumpDir=D:/dev/out,classes=lwz\.HelloWorld

eclipse中 右键--->Run As--->Run Configurations...--->Arguments--->Program arguments:

输入:<pid> D:/dev/classdumper.jar dumpDir=D:/dev/out,classes=lwz\.HelloWorld

-----------------------------------------------------------------------------

<pid>为:Program.java---->测试启动程序,日志中进程ID,如下则是:3696

3696@MS-SKYFSYTUHFPM
|000| 3696@MS-SKYFSYTUHFPM remains 600 seconds
a + b = 9
|001| 3696@MS-SKYFSYTUHFPM remains 599 seconds
a - b = 0
|002| 3696@MS-SKYFSYTUHFPM remains 598 seconds
a - b = 2

-----------------------------------------------------------------------------

测试过程中遇到的问题:

D:\dev\javaAgent\out>java -cp "%JAVA_HOME%/lib/tools.jar";. Attach 16716 D:/dev/classdumper.jar dumpDir=D:/dev/out,classes=lwz\.HelloWorld
java.util.ServiceConfigurationError: com.sun.tools.attach.spi.AttachProvider: Provider sun.tools.attach.WindowsAttachProvider could not be instantiated
Exception in thread "main" com.sun.tools.attach.AttachNotSupportedException: no providers installed
        at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:203)
        at Attach.main(Attach.java:15)

解决方式:${JAVA_HOME}/jre/bin/attach.dll 文件没有找到,将这个文件复制到${JAVA_HOME}/bin/目录下即可;前提是${JAVA_HOME}/bin/目录已经加入到操作系统的path环境变量下;

5、总结

第一点,主要功能。从功能的角度来讲,是如何从一个正在运行的JVM当中将某一个class文件导出到磁盘上。
第二点,实现方式。从实现方式上来说,是借助于Java Agent和正则表达式(区配类名)来实现功能。
第三点,注意事项。在Java 8的环境下,想要将Agent Jar加载到一个正在运行的JVM当中,需要用到tools.jar。
 

天下事有难易乎?为之,则难者亦易矣;不为,则易者亦难矣。

今天多学一份本事,明天少说一句求人的话。

相关文章:

  • c语言进阶 结构体的声明
  • 深度学习Mask R-CNN等实例分割网络
  • [计算机通信网络]网桥与其作用机理举例详解
  • 缓存相关知识点
  • MyBatis映射配置文件结构、标签详解及SQL语句中参数的获取
  • 【SQL刷题】DAY22----增删改操作专项练习
  • 真实场景下的安全专家各项技能详解
  • 为何以太坊合并很重要?
  • 【docker】使用docker安装宝塔面板
  • 【正点原子STM32连载】第四十一章 无线通信实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1
  • C语言 哈希表的简单实现
  • 学习率和BatchSize对模型的影响
  • 小代码大智慧: FilenameUtils.getName 函数分析
  • 基于php理发店管理系统
  • Linux入门之使用 firewalld 防火墙
  • 【JavaScript】通过闭包创建具有私有属性的实例对象
  • Android组件 - 收藏集 - 掘金
  • canvas实际项目操作,包含:线条,圆形,扇形,图片绘制,图片圆角遮罩,矩形,弧形文字...
  • ES6系统学习----从Apollo Client看解构赋值
  • hadoop入门学习教程--DKHadoop完整安装步骤
  • Invalidate和postInvalidate的区别
  • laravel with 查询列表限制条数
  • LeetCode29.两数相除 JavaScript
  • Ruby 2.x 源代码分析:扩展 概述
  • SpriteKit 技巧之添加背景图片
  • SQLServer之创建显式事务
  • Vim Clutch | 面向脚踏板编程……
  • 关于springcloud Gateway中的限流
  • 用Python写一份独特的元宵节祝福
  • hi-nginx-1.3.4编译安装
  • 国内唯一,阿里云入选全球区块链云服务报告,领先AWS、Google ...
  • 微龛半导体获数千万Pre-A轮融资,投资方为国中创投 ...
  • ​​​​​​​GitLab 之 GitLab-Runner 安装,配置与问题汇总
  • # 20155222 2016-2017-2 《Java程序设计》第5周学习总结
  • #我与Java虚拟机的故事#连载03:面试过的百度,滴滴,快手都问了这些问题
  • (多级缓存)缓存同步
  • (附源码)springboot金融新闻信息服务系统 毕业设计651450
  • (力扣)循环队列的实现与详解(C语言)
  • (五)大数据实战——使用模板虚拟机实现hadoop集群虚拟机克隆及网络相关配置
  • (转)JAVA中的堆栈
  • (转)mysql使用Navicat 导出和导入数据库
  • (最完美)小米手机6X的Usb调试模式在哪里打开的流程
  • .NET 8 中引入新的 IHostedLifecycleService 接口 实现定时任务
  • .net core Swagger 过滤部分Api
  • .Net MVC + EF搭建学生管理系统
  • .NET 命令行参数包含应用程序路径吗?
  • .net6+aspose.words导出word并转pdf
  • .net反混淆脱壳工具de4dot的使用
  • .net利用SQLBulkCopy进行数据库之间的大批量数据传递
  • .py文件应该怎样打开?
  • /bin/rm: 参数列表过长"的解决办法
  • @test注解_Spring 自定义注解你了解过吗?
  • [ C++ ] template 模板进阶 (特化,分离编译)
  • [ABP实战开源项目]---ABP实时服务-通知系统.发布模式
  • [Angular 基础] - 数据绑定(databinding)