Instrumentation 应用简介

引用: java-instrumentation
引用:Instrumentation 新功能

简介

java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。

场景

  • 一个ATM应用程序,允许我们取钱
  • 还有一个Java代理,它允许我们通过衡量投入的时间来衡量我们的ATM的性能

Java代理将修改ATM字节码,允许我们测量提取时间,而无需修改ATM应用程序。

什么是java agent

通常,java代理只是一个特制的jar文件。它利用JVM提供的Instrumentation API来更改JVM中加载的现有字节代码。
要使代理工作,我们需要定义两个方法:

  • premain: 将在JVM启动时使用-javaagent参数静态加载代理
  • agentmain: 将使用Java Attach API将代理动态加载到JVM中。JVM实现(如Oracle,OpenJDK等)都提供动态启动代理的机制。

首先,让我们看看我们如何使用现有的Java代理。
在那之后,我们将看看我们如何从头开始创建一个在字节码中添加所需的功能。

加载 agent

为了能够使用Java代理,我们必须首先加载它。我们有两种加载方式:

  • static: 使用premain来使用-javaagent选项加载代理
  • dynamic: 使用agentmain使用Java Attach API将代理加载到JVM中

接下来,我们将看看每种类型的负载并解释它是如何工作的。

静态加载

在应用程序启动时加载Java代理称为静态加载
静态加载可以在main方法执行前,修改任意代码的字节码。
静态加载使用premain方法,该方法将在任何应用程序代码运行之前运行,
通过如下命令,可以开启静态加载:

# 我们应该始终将-javaagent参数放在-jar参数之前。
# agent_param 可选参数,与main方法接收参数不同,它只能接收一个string类型的参数.
java -javaagent:agent.jar [agent_param] -jar application.jar

动态加载

将Java代理加载到已运行的JVM中的过程称为动态加载
代理程序通过Java Attach API附加到应用程序。

更为复杂的情况是,当我们的ATM应用程序已经在生产环境中运行,我们希望在不停机的情况下,动态的添加监控项,如:动态添加事务的总时间。
下面给出关键代码片段:

//pid 为需要监控的应用程序pid
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();

具体的操作步骤是:

  1. 首先启动应用程序
  2. 在确保应用程序正常的启用一段时间后,获取该应用的pid,
  3. 启动agent程序,启动时需传递应用程序pid

coding

应用程序

ATM
ATM类,并提供withdrawMoney取款方法。

public class ATM {
    private static Logger LOGGER = LoggerFactory.getLogger(ATM.class);

    private static int TOTAL_MONEY = 10000;

    /**
     * 取钱
     *
     * @param amount
     * @throws InterruptedException
     */
    public static void withdrawMoney(int amount) throws InterruptedException {
        //模拟一个取钱的动作
        Thread.sleep(ThreadLocalRandom.current().nextLong(1000));

        int rest = TOTAL_MONEY -= amount;
        LOGGER.info("[Application] 取款 [{}] 元,余额[{}]!", amount, rest);

        //当账户余额不足时,退出系统
        if (rest <= 0) {
            System.exit(1);
        }
    }
}

App
应用启动类App,用来启动应用程序.

public class App {
    private static Logger LOGGER = LoggerFactory.getLogger(App.class);

    /**
     * 应用主程序
     *
     * @param args:
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        LOGGER.info("**************************************************");
        LOGGER.info("===========欢迎使用xx银行ATM无人取款机=============");
        LOGGER.info("**************************************************\n");

        while (true){
            ATM.withdrawMoney(new Random().nextInt(100));
            TimeUnit.SECONDS.sleep(2);
        }
    }
}

打包,执行
将上述两个应用程序打包成可执行jar包,执行命令java -jar application.jar,效果如下图:
Instrumentation 应用简介_第1张图片

Maven生成可以直接运行的jar包的多种方式

静态加载

在编写加载类前,需要先了解下如下几点知识:

premain函数
编写一个 Java 类,包含如下两个方法当中的任何一个:

//[1] 的优先级比 [2] 高,将会被优先执行([1] 和 [2] 同时存在时,[2] 被忽略)

public static void premain(String agentArgs, Instrumentation inst);  //[1]
public static void premain(String agentArgs); //[2]

ClassFileTransformer-接口

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

通过这个方法,代理可以得到虚拟机载入的类的字节码(通过 classfileBuffer 参数)。代理的各种功能一般是通过操作这一串字节码得以实现的。

字节码编辑器:AtmTransformer

使用javassist 来操作字节码

public class AtmTransformer implements ClassFileTransformer {

    private static Logger LOGGER = LoggerFactory.getLogger(AtmTransformer.class);

    private static final String WITHDRAW_MONEY_METHOD = "withdrawMoney";

    private String targetClassName;
    private ClassLoader targetClassLoader;

    public AtmTransformer(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 ATM");
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get(targetClassName);
                CtMethod m = cc.getDeclaredMethod(WITHDRAW_MONEY_METHOD);
                //添加局部变量 startTime
                m.addLocalVariable("startTime", CtClass.longType);

                //方法头部,为startTime赋值
                m.insertBefore("startTime = System.currentTimeMillis();");

                //添加局部变量 endTime , opTime
                m.addLocalVariable("endTime", CtClass.longType);
                m.addLocalVariable("opTime", CtClass.longType);

                StringBuilder endBlock = new StringBuilder();
                endBlock.append("endTime = System.currentTimeMillis();");
                endBlock.append("opTime = (endTime-startTime);");

                endBlock.append("LOGGER.info(\"[Application] 取款总计耗时:\" + opTime + \" ms!\");");


                //方法尾部,为endTime,optTime赋值,并打印日志
                m.insertAfter(endBlock.toString());
                
                //返回 更改后的字节码
                byteCode = cc.toBytecode();
                cc.detach();
            } catch (NotFoundException | CannotCompileException | IOException e) {
                LOGGER.error("Exception", e);
            }
        }

        return byteCode;
    }
}

计算取款耗时代理类:TimeInstrumentationAgent

public class TimeInstrumentationAgent {
    private static Logger LOGGER = LoggerFactory.getLogger(TimeInstrumentationAgent.class);
    /**
     *
     * @param agentArgs 通过命令:java -javaagent:agent.jar [agent_param] -jar application.jar
     *                  传递的代理参数,只有一个以字符串形式接受
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        LOGGER.info("[Agent] In premain method");

        String className = "cn.jhs.application.service.ATM";
        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) {
        AtmTransformer dt = new AtmTransformer(clazz.getName(), classLoader);
        instrumentation.addTransformer(dt, true);
        try {
            instrumentation.retransformClasses(clazz);
        } catch (Exception ex) {
            throw new RuntimeException("Transform failed for class: [" + clazz.getName() + "]", ex);
        }
    }

}

打包:agent.jar
将agent程序打包,并在MANIFEST.MF指定Premain-Class,
如下图:
Instrumentation 应用简介_第2张图片

注意 : 经常有人会忘记指定Can-Retransform-ClassesCan-Redefine-Classes
而抛出类似java.lang.UnsupportedOperationException: adding retransformable transformers is not supported in this environment异常.

执行
使用命令:java -javaagent:agent.jar -jar application.jar执行,效果如下:
Instrumentation 应用简介_第3张图片

动态加载

在 Java SE 5 当中,开发者只能通过premain来修改完成代理工作。
但是必须在应用启动前指定Instrumentation,这样的方式存在一定的局限性。

在 Java SE 5 的基础上,Java SE 6 针对这种状况做出了改进,开发者可以在应用程序启动之后,启动自己的Instrumentation程序。

在 Java SE 6 的 Instrumentation 当中,有一个跟premain功能类似的agentmain方法,可以在 main 函数运行之后再运行。

agentmain函数

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

Premain-Class类似,开发者必须在manifest文件里面设置Agent-Class来指定包含 agentmain 函数的类。

修改:TimeInstrumentationAgent
修改字节码的逻辑不变,唯一的变化就是在TimeInstrumentationAgent添加agentmain方法:

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

     String className = "cn.jhs.application.service.ATM";
     transformClass(className,inst);
 }

打包:agent.jar
指定Agent-Class,打包后效果如下图:
Instrumentation 应用简介_第4张图片

编写监听程序

public class AttachTask implements Runnable{
    private static Logger LOGGER = LoggerFactory.getLogger(AttachTask.class);

    private String agentJarFullPath;
    private String applicationName;

    /**
     *
     * @param agentJarFullPath agent.jar 完整路径
     * @param applicationName 需要添加代理的目标程序
     */
    public AttachTask(String agentJarFullPath, String applicationName) {
        this.agentJarFullPath = agentJarFullPath;
        this.applicationName = applicationName;
    }



    @Override
    public void run() {
        while(true) {
            Optional<String> jvmProcessOpt = Optional.ofNullable(VirtualMachine.list()
                    .stream()
                    .filter(jvm -> {
                        LOGGER.info("jvm:{}", jvm.displayName());
                        return jvm.displayName().contains(applicationName);
                    })
                    .findFirst().get().id());

            //如果没有找到目标应用程序,sleep ,然后从新监听
            if(!jvmProcessOpt.isPresent()) {
                try {
                    LOGGER.info("未捕获到应用程序: " + applicationName);
                    TimeUnit.SECONDS.sleep(5L);
                } catch (InterruptedException e) {
                    //
                }
                break;
            }

            //
            File agentFile = new File(agentJarFullPath);
            try {
                String jvmPid = jvmProcessOpt.get();
                LOGGER.info("捕获到应用程序[{}], JVM PID:[{}] ",applicationName,jvmPid);
                VirtualMachine jvm = VirtualMachine.attach(jvmPid);
                jvm.loadAgent(agentFile.getAbsolutePath());
                jvm.detach();
                LOGGER.info("目标应用程序添加代理成功!");
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            //添加完代理之后,直接退出..
            return ;
        }

    }


    public static void main(String[] args) {
        new Thread(new AttachTask("D:/instrumentation/agent.jar","application.jar")).start();
    }
}

执行
1.启动应用 : java -jar application.jar
Instrumentation 应用简介_第5张图片

2.启动监听程序
Instrumentation 应用简介_第6张图片
3.监听启动后,应用程序日志变化
Instrumentation 应用简介_第7张图片

你可能感兴趣的:(虚拟机)