java 探针

关于java agent这里只是做一个 简单的介绍,因为详细的介绍官网上有很多地址:https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html,为了节省大家的时间。所以重点介绍应用场景已经应用方式。

案例:对一个应用程序的指定方法的调用增加耗时监控(在不修改原来应用代码的情况下)

premain方式

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

premain 顾名思义是在需要被代理的应用main方法执行前执行。但是个人认为这种方式的局限性太大了。如果需要对一个应用进行处理,需要停止应用。这在生产环境中危险是很大的。实用场景较少,所以本文不会重点对它进行说明。但是也会贴上一个简单的应用的实现代码。因为坑相对于另一种方式较少。所以只贴代码不进行详细说明了。

agentTest工程:


image.png

MyTest:code

public class MyTest {

    public static void main(String[] args) {
        //MyTest myTest = new MyTest();

        sayHello();
        sayHello2("hello world11");


    }

    public static void sayHello() {
        try {
            Thread.sleep(2000);
            System.out.println("hello world!!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void sayHello2(String hello) {
        try {
            Thread.sleep(1000);
            System.out.println(hello);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

MANIFEST.MF

Manifest-Version: 1.0
Main-Class: test.demo.MyTest

javaagent4工程


image.png

AgentDemo

public class AgentDemo {

    /**
     * 该方法在main方法之前运行,与main方法运行在同一个JVM中
     *
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("=========premain方法执行1========");
        System.out.println(agentArgs);
        // 添加Transformer
        inst.addTransformer(new MyTransformer());
    }

    /**
     * 如果不存在 premain(String agentArgs, Instrumentation inst)
     * 则会执行 premain(String agentArgs)
     *
     */
    public static void premain(String agentArgs) {
        System.out.println("=========premain方法执行2========");
        System.out.println(agentArgs);

    }

}

MyTransformer
```java
public class MyTransformer implements ClassFileTransformer {


    final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";
    final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";

    final static Map<String, List<String>> classMapName = new ConcurrentHashMap<>();


    public MyTransformer(){
        add("test.demo.MyTest.sayHello");
        add("test.demo.MyTest.sayHello2");
    }

    private  void  add(String className){
        String classNameStr = className.substring(0,className.lastIndexOf("."));
        String methodName = className.substring(className.lastIndexOf(".")+1);
        List<String> lists = classMapName.get(classNameStr);
        if(null == lists){
            lists = new ArrayList<>();
            classMapName.put(classNameStr,lists);
        }
        lists.add(methodName);
    }
    
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // 该路径显示方式
        className = className.replace("/",".");
        // 判断传入的类路径是否在监控中
        if( classMapName.containsKey(className)) {
            CtClass ctclass = null;
            try{
                // 根据类全名获取字节码类信息
                ctclass = ClassPool.getDefault().get(className);
                for (String methodName : classMapName.get(className)) {
                    String outputStr = "\nSystem.out.println(\"this method " + methodName
                            + " cost:\" +(endTime - startTime) +\"ms.\");";
                    System.out.println(outputStr);
                    // 根据方法名得到这方法实例
                    CtMethod ctMethod = ctclass.getDeclaredMethod(methodName);
                    // 新定义一个方法叫做比如sayHello$old
                    String newMethodName = methodName + "$old";
                    // 将原来的方法名字修改
                    ctMethod.setName(newMethodName);
                    // 创建新的方法,复制原来的方法,名字为原来的名字
                    CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctclass, null);
                    // 构建新的方法体
                    StringBuilder bodyStr = new StringBuilder();
                    bodyStr.append("{");
                    bodyStr.append(prefix);
                    // 调用原有代码,类似于method();($$)表示所有的参数
                    bodyStr.append(newMethodName + "($$);\n");
                    bodyStr.append(postfix);
                    bodyStr.append(outputStr);
                    bodyStr.append("}");
                    // 替换新方法
                    newMethod.setBody(bodyStr.toString());
                    // 增加新方法
                    ctclass.addMethod(newMethod);
                }

                return ctclass.toBytecode();

            }catch (Exception e){
                System.out.println(e.getMessage());
                
            }
        }
        return null;
    }
}

MANIFEST.MF

Manifest-Version: 1.0
Created-By: 0.0.1 (Demo Inc.)
Premain-Class: agent.AgentDemo  

Premain-Class:指定步骤 1 当中编写的那个带有 premain 的 Java 类
用如下方式运行带有 Instrumentation 的 Java 程序:

java -javaagent:jar 文件的位置 [= 传入 premain 的参数 ]

agentmain方式

优势:premain是静态修改,在类加载之前修改; attach是动态修改,在类加载后修改要使premain生效重启应用,而attach不重启应用即可修改字节码并让其重新加载。
和premain类似 agentmain也有两个类似的方法

public static void agentmain (String agentArgs, Instrumentation inst);     //     [1] 
public static void agentmain (String agentArgs);         //   [2]
//[1] 的优先级比 [2] 高,将会被优先执行

agentmain 与 premain 不同在于agentmain需要在 main 函数开始运行后才启动,既然是要在main函数开始运行后才启动,那他的启动时机如何确定,这就需要引出一个概念 Java SE 6 当中提供的 Attach API。
Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面: VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 ; VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。整个过程其实和premain方式类似,主要的区别在于执行时机的不同。
先贴代和效果图,最后在来说在实现过程中遇到的坑,以及解决方案。
两个应用的结构非常简单,因为重点不是这里所以随意了些


image.png

MyApplication

public class MyApplication {

    private static Logger logger = LogManager.getLogger(MyApplication.class);


    public void run() throws  Exception{
        logger.info("run 运行...");
        Run run = new Run();
        for(;;){
            run.run();
        }
    }

}

Launcher

public class Launcher {
    // 主函数
    public static void main(String[] args) throws Exception {
        MyApplication myApplication = new MyApplication();
        myApplication.run();
    }
}

Run

public class Run {
    
    private static final Logger logger = LogManager.getLogger(Run.class);

    public void run() throws InterruptedException{
        long sleep = (long)(Math.random() * 1000 + 200);
        Thread.sleep(sleep);
        logger.info("run in [{}] millis!", sleep);
    }
}

MANIFEST.MF

Main-Class: com.demo.application.Launcher

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.demo</groupId>
    <artifactId>agent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <build>
        <finalName>myAgent</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <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> <!-- this is used for inheritance merges -->
                        <phase>package</phase> <!-- bind to the packaging phase -->
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <!-- Project dependencies -->
    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.11.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.11.1</version>
        </dependency>
    </dependencies>

</project>
##打包命令
mvn clean package
##执行命令
java   -jar myAgent-jar-with-dependencies.jar

这个工程就是作为我们在生产上运行的应用实例,虽然不会这么简单。这里没有什么问题。我们甚至可以用springboot构建,只是表现形式不同而已。接下来重点来了

image.png

先贴代码:
Launcher

public class Launcher {

    private static Logger logger = LogManager.getLogger(Launcher.class);


    public static void main(String[] args) {
        //指定jar路径
        String agentFilePath = "myAcctach-jar-with-dependencies.jar";

        //需要attach的进程标识
        String applicationName = "myAgent";

        //查到需要监控的进程
        Optional<String> jvmProcessOpt = Optional.ofNullable(VirtualMachine.list()
                .stream()
                .filter(jvm -> {
                    logger.info("jvm:{}", jvm.displayName());
                    return jvm.displayName().contains(applicationName);
                })
                .findFirst().get().id());

        if(!jvmProcessOpt.isPresent()) {
            logger.error("Target Application not found");
            return;
        }
        File agentFile = new File(agentFilePath);
        try {
            String jvmPid = jvmProcessOpt.get();
            logger.info("Attaching to target JVM with PID: " + jvmPid);
            VirtualMachine jvm = VirtualMachine.attach(jvmPid);
            jvm.loadAgent(agentFile.getAbsolutePath());
            jvm.detach();
            logger.info("Attached to target JVM and loaded Java agent successfully");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

MyInstrumentationAgent

public class MyInstrumentationAgent {

    private static Logger logger = LogManager.getLogger(MyInstrumentationAgent.class);

    public static void agentmain(String agentArgs, Instrumentation inst) {
        logger.info("[Agent] In agentmain method");

        //需要监控的类
        String className = "com.demo.application.Run";
        transformClass(className, inst);
    }

    private static void transformClass(String className, Instrumentation instrumentation) {
        Class<?> targetCls = null;
        ClassLoader targetClassLoader = null;
        // see if we can get the class using forName
        try {
            targetCls = Class.forName(className);
            targetClassLoader = targetCls.getClassLoader();
            transform(targetCls, targetClassLoader, instrumentation);
            return;
        } catch (Exception ex) {
            logger.error("Class [{}] not found with Class.forName");
        }
        // otherwise iterate all loaded classes and find what we want
        for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
            if(clazz.getName().equals(className)) {
                targetCls = clazz;
                targetClassLoader = targetCls.getClassLoader();
                transform(targetCls, targetClassLoader, instrumentation);
                return;
            }
        }
        throw new RuntimeException("Failed to find class [" + className + "]");
    }

    private static void transform(Class<?> clazz, ClassLoader classLoader, Instrumentation instrumentation) {
        MyTransformer dt = new MyTransformer(clazz.getName(), classLoader);
        instrumentation.addTransformer(dt, true);
        try {
            instrumentation.retransformClasses(clazz);
        } catch (Exception ex) {
            throw new RuntimeException("Transform failed for class: [" + clazz.getName() + "]", ex);
        }
    }
}

MyTransformer

public class MyTransformer implements ClassFileTransformer {

    private static Logger logger = LogManager.getLogger(MyTransformer.class);

    //需要监控的方法
    private static final String WITHDRAW_MONEY_METHOD = "run";

    /** The internal form class name of the class to transform */
    private String targetClassName;
    /** The class loader of the class we want to transform */
    private ClassLoader targetClassLoader;

    public MyTransformer(String targetClassName, ClassLoader targetClassLoader) {
        this.targetClassName = targetClassName;
        this.targetClassLoader = targetClassLoader;
    }



    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        byte[] byteCode = classfileBuffer;
        String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/"); //replace . with /
        if (!className.equals(finalTargetClassName)) {
            return byteCode;
        }

        if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
            logger.info("[Agent] Transforming class" + className);
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get(targetClassName);
                CtMethod m = cc.getDeclaredMethod(WITHDRAW_MONEY_METHOD);

                // 开始时间
                m.addLocalVariable("startTime", CtClass.longType);
                m.insertBefore("startTime = System.currentTimeMillis();");

                StringBuilder endBlock = new StringBuilder();

                // 结束时间
                m.addLocalVariable("endTime", CtClass.longType);
                endBlock.append("endTime = System.currentTimeMillis();");

                // 时间差
                m.addLocalVariable("opTime", CtClass.longType);
                endBlock.append("opTime = endTime-startTime;");

                // 打印方法耗时
                endBlock.append("logger.info(\"completed in:\" + opTime + \" millis!\");");

                m.insertAfter(endBlock.toString());

                byteCode = cc.toBytecode();
                cc.detach();
            } catch (Exception e) {
                logger.error("Exception", e);
            }
        }
        return byteCode;
    }
}

MANIFEST.MF

Main-Class: com.acttach.agent.Launcher
Agent-Class: com.acttach.agent.MyInstrumentationAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Permissions: all-permissions

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.acttch</groupId>
    <artifactId>acttch</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <finalName>myAcctach</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>

                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <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> <!-- this is used for inheritance merges -->
                        <phase>package</phase> <!-- bind to the packaging phase -->
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>

    </build>

    <!-- Project dependencies -->
    <dependencies>
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>

        </dependency>


        <!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.24.1-GA</version>
        </dependency>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.11.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.11.1</version>
        </dependency>

    </dependencies>

</project>
##打包命令
mvn clean package
##执行命令
java -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Djava.ext.dirs="%JAVA_HOME%\lib" -jar myAcctach-jar-with-dependencies.jar

坑点1:
在打包完执行jar包时,最开始我是直接用
java -jar myAcctach-jar-with-dependencies.jar
出现了下面的错误

D:\litter\acttch\target>java  -jar myAcctach-jar-with-dependencies.jar
Exception in thread "main" java.lang.NoClassDefFoundError: com/sun/tools/attach/VirtualMachine
        at com.acttach.agent.Launcher.main(Launcher.java:31)
Caused by: java.lang.ClassNotFoundException: com.sun.tools.attach.VirtualMachine
        at java.net.URLClassLoader.findClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        ... 1 more

tools.jar因为是jre环境中的本地包,所以我们在打完包之后,实际上这个jar包是没有被打进去的。所以在执行的时候要指定-Djava.ext.dirs 在网上找了很多文章他们都是这样写的
-Djava.ext.dirs=${JAVA_HOME}\lib -jar 说对于linux windows都可以。我也不知道他们有没有验证,反正这种方式在windows上行不通的。 我的windows上 只有 ava -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Djava.ext.dirs="%JAVA_HOME%\lib" -jar myAcctach-jar-with-dependencies.jar 这样才行,至于为什么需要在%JAVA_HOME%\lib外层加上引号,因为我的jdk路径是在C:\Program Files 大家发现没有中间有一个空格,如果你不加引号当做一个整体,windows下会给你切分。


image.png

前面那一部分没有了。

坑2:

VirtualMachine jvm = VirtualMachine.attach(jvmPid);
// 要注意这里是加载自身的jar进去 来对需要代理的应用进行处理。这里不要弄混了。
// 本人就是在这个地方被磨了很久,一直报找不到jar.....~~~~(>_<)~~~~
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();
image.png

上面的截图是随机休眠一段时间并打印睡眠时间的方法

public class Run {


    private static final Logger logger = LogManager.getLogger(Run.class);

    public void run() throws InterruptedException{
        long sleep = (long)(Math.random() * 1000 + 200);
        Thread.sleep(sleep);
        logger.info("run in [{}] millis!", sleep);
    }
}

现在有一个需求在不改原来的代码基础上增加监控统计开始结束时间


image.png

这是在执行了另外一个应用之后产生的效果。

需要注意的地方基本上就是上面这几个了。其实仔细想想这个技术还是挺有应用场景的。有兴趣的不妨去学学。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容