Java Agent入門實戰(三)-JVM Attach原理與使用

以前的permain方法只能在java程序啓動以前執行,並不能程序啓動以後再執行,可是在實際的不少的狀況下,咱們沒有辦法在虛擬機啓動之時就爲其設定代理,這樣實際上限制了instrument的應用。而Java SE 6的新特性改變了這種狀況,能夠經過Java Tool API中的attach方式來達到這種程序啓動以後設置代理的效果。html

Attach API 不是 Java 的標準 API,而是 Sun 公司提供的一套擴展 API,用來向目標 JVM 「附着」(Attach)代理工具程序的。有了它,開發者能夠方便的監控一個 JVM,運行一個外加的代理程序。java

Attach API 很簡單,只有 2 個主要的類,都在 com.sun.tools.attach包裏面: VirtualMachine 表明一個 Java 虛擬機,也就是程序須要監控的目標虛擬機,提供了 JVM 枚舉,Attach 動做和 Detach 動做(從 JVM 上面解除一個代理)等等 ; VirtualMachineDescriptor 則是一個描述虛擬機的容器類,配合VirtualMachine類完成各類功能。編程

結合上一篇文章和Attach,看看如何使用bash

Agent類增長了2個agentmain()方法,它們的參數不用,2個參數的優先級大於1個參數的,因此這裏只有agentmain (String agentArgs, Instrumentation inst)會被執行微信

public class JpAgent {

    public static void premain(String agentArgs){
        System.out.println("我是一個參數的 Java Agent premain");
    }

    public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
        inst.addTransformer(new JpClassFileTransformerDemo(), true);
        // retransformClasses 是 Java SE 6 裏面的新方法,它跟 redefineClasses 同樣,能夠批量轉換類定義
        inst.retransformClasses(Dog.class);
        System.out.println("我是兩個參數的 Java Agent agentmain");

    }
    public static void agentmain (String agentArgs){
        System.out.println("我是一個參數的 Java Agent agentmain");
    }
}
複製代碼

新增一個Dog類,內容以下,關鍵點在package 路徑要和目標程序同樣,其餘無所謂jvm

package cn.jpsite.learning;

public class Dog {
}
複製代碼

在以前的example01工程中新建一個帶有main函數的類WhileMain.javaide

public class WhileMain {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(new Dog().say());
        int count = 0;
        while (true) {
            // 等待0.5秒
            Thread.sleep(500);
            count++;
            String say = new Dog().say();
            // 輸出內容和次數
            System.out.println(say + count);
            // 內容不對或者次數達到1000次以上輸出
            if (!"dog".equals(say) || count >= 1000) {
                System.out.println("有人偷了個人狗!");
                //break;
            }
        }
    }
}
複製代碼

準備一個修改過的Dog.class,內容以下,單獨保存到目錄D:\learning\Dog.class函數

// 這是一個修改後編譯的.class文件,單獨存放
public class Dog {
    public String say() {
        return "cat";
    }
}
複製代碼

resource/META-INF/MANIFEST.MF新增內容工具

Agent-Class: cn.jpsite.learning.javaagent01.JpAgent
Can-Retransform-Classes: true
複製代碼

到了這裏,準備工做基本已經完成,執行打包構建,此時執行如下不管哪一條,都不會有結果agentmain輸出源碼分析

java -javaagent:jpAgent.jar -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.Main

java -javaagent:jpAgent.jar -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain

java -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain
複製代碼

結果以下,並沒用調用 agentmain 方法,這該怎麼辦呢?

這時候就要用到com.sun.tools.attach來幫助咱們達到虛擬機啓動以後的代理設置,代碼以下:

import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.io.IOException;
import java.util.List;
import java.util.Objects;

/**
 *
 * @author jiangpeng
 * @date 2019/12/1 0001
 */
public class AttachThread extends Thread {
    /**
     * 記錄程序啓動時的 VM 集合
     */
    private final List<VirtualMachineDescriptor> listBefore;
    /**
     要加載的agent.jar
     */
    private final String jar;

    private AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) {
        listBefore = vms;
        jar = attachJar;
    }

    @Override
    public void run() {
        VirtualMachine vm;
        List<VirtualMachineDescriptor> listAfter;
        int count = 0;

        try {
            while (true) {
                listAfter = VirtualMachine.list();
                vm = hasNewVm(listAfter);

                if(vm == null){
                    System.out.println("沒有新jvm程序,請手動指定java pid");
                    try{
                        vm = VirtualMachine.attach("7716");
                    }catch (AttachNotSupportedException e){
                        //System.out.println("拒絕訪問 Disconnected from the target VM");
                    }
                }

                Thread.sleep(1000);
                System.out.println(count++);
                if (null != vm || count >= 100) {
                    break;
                }
            }
            Objects.requireNonNull(vm).loadAgent(jar);
            vm.detach();
        } catch (Exception e) {
            System.out.println("異常:" + e);
        }
    }

    /**
     * 判斷是否有新的 JVM 程序運行
     */
    private VirtualMachine hasNewVm(List<VirtualMachineDescriptor> listAfter) throws IOException, AttachNotSupportedException {
        for (VirtualMachineDescriptor vmd : listAfter) {
            if (!listBefore.contains(vmd)) {
                // 若是 VM 有增長,,咱們開始監控這個 VM
                System.out.println("有新的 vm 程序:"+ vmd.displayName());
                return VirtualMachine.attach(vmd);
            }
        }
        return null;
    }

    public static void main(String[] args)  {
        new AttachThread("D:/learning/jpAgent.jar", VirtualMachine.list()).start();
    }
}
複製代碼

其中while循環部分每隔1秒獲取一次java進程集合,若是沒有的話就會提示手動指定一個java程序進行attach, 當循環了100次或者 獲取到了VirtualMachine ,則退出while(true) 去加載指定的agent.jar

2種測試方法

先執行java -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain啓動example01程序,看到以下結果,還記得咱們以前準備了一份Dog.class,裏面的say()內容是cat的嘛,稍後你就會看到神奇的一幕

接下運行剛剛的AttachThread.java,看到以下內容

使用 jps -l 命令查看全部有運行中的java程序端口號

修改AttachThread.java中的VirtualMachine.attach("7716");代碼爲 VirtualMachine.attach("16304"); 16304爲上圖中WhileMain java 進程id,從新啓動AttachThread.java,結果以下

loader className: cn/jpsite/learning/Dog

我是兩個參數的 Java Agent agentmain

能夠看到agentmain已經執行了,並且原Dog.say()方法裏的內容dog也被改變了,成了cat

第二種

首先啓動AttachThread.java,而後執行java -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain,能夠很快就看到結果被改變了

MANIFEST.MF的其餘屬性

Can-Set-Native-Method-Prefix
System-Class-Path
Boot-Class-Path
複製代碼

Java SE 6 新特性:BootClassPath / SystemClassPath 的動態增補

注意幾點。首先,咱們加入到 classpath 的 jar 文件中不該當帶有任何和系統的 instrumentation 有關的系統同名類,否則,一切都陷入不可預料之中

咱們要注意到虛擬機的 ClassLoader 的工做方式,它會記載解析結果。好比,咱們曾經要求讀入某個類 someclass,可是失敗了,ClassLoader 會記得這一點。即便咱們在後面動態地加入了某一個 jar,含有這個類,ClassLoader 依然會認爲咱們沒法解析這個類,與上次出錯的相同的錯誤會被報告。

再次咱們知道在 Java 語言中有一個系統參數「java.class.path」,這個 property 裏面記錄了咱們當前的 classpath,可是,咱們使用這兩個函數,雖然真正地改變了實際的 classpath,卻不會對這個 property 自己產生任何影響。

點關注,不迷路

文章每週持續更新,能夠微信搜索「 十分鐘學編程 」第一時間閱讀和催更,若是這個文章寫得還不錯,以爲有點東西的話 ~求點贊👍 求關注❤️ 求分享❤️
各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!

相關文章
相關標籤/搜索