Guia de instrumentação Java

Guia de instrumentação Java

1. Introdução

Neste tutorial, vamos falar sobreJava Instrumentation API.. Ele fornece a capacidade de adicionar byte-code a classes Java compiladas existentes.

Também falaremos sobre agentes java e como os usamos para instrumentar nosso código.

2. Configuração

Ao longo do artigo, construiremos um aplicativo usando instrumentação.

Nossa aplicação consistirá em dois módulos:

  1. Um aplicativo de caixa eletrônico que permite sacar dinheiro

  2. E um agente Java que nos permitirá medir o desempenho de nosso caixa eletrônico medindo o tempo investido gastando dinheiro

O agente Java irá modificar o byte-code ATM, permitindo-nos medir o tempo de retirada sem ter que modificar o aplicativo ATM.

Nosso projeto terá a seguinte estrutura:

com.example.instrumentation
base
1.0.0
pom

    agent
    application

Antes de entrarmos muito nos detalhes da instrumentação, vamos ver o que é um agente java.

3. O que é um Agente Java

Em geral, um agente java é apenas um arquivo jar especialmente criado. It utilizes the Instrumentation API that the JVM provides to alter existing byte-code that is loaded in a JVM.

Para um agente funcionar, precisamos definir dois métodos:

  • premain - carregará estaticamente o agente usando o parâmetro -javaagent na inicialização da JVM

  • agentmain - carregará dinamicamente o agente na JVM usando oJava Attach API

Um conceito interessante a ser lembrado é que uma implementação da JVM, como Oracle, OpenJDK e outras, pode fornecer um mecanismo para iniciar agentes dinamicamente, mas não é um requisito.

Primeiro, vamos ver como usaríamos um agente Java existente.

Depois disso, veremos como podemos criar um do zero para adicionar a funcionalidade de que precisamos em nosso código de bytes.

4. Carregando um Agente Java

Para poder usar o agente Java, primeiro precisamos carregá-lo.

Temos dois tipos de carga:

  • static - faz uso depremain para carregar o agente usando a opção -javaagent

  • dinâmico - faz uso doagentmain para carregar o agente na JVM usando oJava Attach API

A seguir, vamos dar uma olhada em cada tipo de carga e explicar como funciona.

4.1. Carga estática

O carregamento de um agente Java na inicialização do aplicativo é chamado de carga estática. Static load modifies the byte-code at startup time before any code is executed.

Lembre-se de que a carga estática usa o métodopremain, que será executado antes que qualquer código do aplicativo seja executado, para fazê-lo funcionar, podemos executar:

java -javaagent:agent.jar -jar application.jar

É importante notar que devemos sempre colocar o sparameter -javaagent antes do sparameter -jar .

Abaixo estão os logs para o nosso comando:

22:24:39.296 [main] INFO - [Agent] In premain method
22:24:39.300 [main] INFO - [Agent] Transforming class MyAtm
22:24:39.407 [main] INFO - [Application] Starting ATM application
22:24:41.409 [main] INFO - [Application] Successful Withdrawal of [7] units!
22:24:41.410 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!
22:24:53.411 [main] INFO - [Application] Successful Withdrawal of [8] units!
22:24:53.411 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!

Podemos ver quando o métodopremain foi executado e quandoMyAtm class foi transformado. Também vemos os dois logs de transações de retirada de caixas eletrônicos que contêm o tempo que cada operação levou para ser concluída.

Lembre-se de que em nosso aplicativo original não tínhamos esse tempo de conclusão de uma transação, ele foi adicionado por nosso agente Java.

4.2. Carga dinâmica

The procedure of loading a Java agent into an already running JVM is called dynamic load. O agente é anexado usandoJava Attach API.

Um cenário mais complexo é quando já temos nosso aplicativo ATM em execução na produção e queremos adicionar o tempo total de transações dinamicamente sem tempo de inatividade para nosso aplicativo.

Vamos escrever um pequeno pedaço de código para fazer exatamente isso e chamaremos essa classe deAgentLoader. Para simplificar, colocaremos essa classe no arquivo jar do aplicativo. Portanto, nosso arquivo jar do aplicativo pode iniciar nosso aplicativo e anexar nosso agente ao aplicativo ATM:

VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();

Agora que temos nossoAgentLoader, iniciamos nosso aplicativo nos certificando de que, na pausa de dez segundos entre as transações, anexaremos nosso agente Java dinamicamente usandoAgentLoader.

Vamos também adicionar a cola que nos permitirá iniciar o aplicativo ou carregar o agente.

Chamaremos essa classe deLauncher e será nossa classe de arquivo jar principal:

public class Launcher {
    public static void main(String[] args) throws Exception {
        if(args[0].equals("StartMyAtmApplication")) {
            new MyAtmApplication().run(args);
        } else if(args[0].equals("LoadAgent")) {
            new AgentLoader().run(args);
        }
    }
}

Iniciando o aplicativo

java -jar application.jar StartMyAtmApplication
22:44:21.154 [main] INFO - [Application] Starting ATM application
22:44:23.157 [main] INFO - [Application] Successful Withdrawal of [7] units!

Anexando Agente Java

Após a primeira operação, anexamos o agente java à nossa JVM:

java -jar application.jar LoadAgent
22:44:27.022 [main] INFO - Attaching to target JVM with PID: 6575
22:44:27.306 [main] INFO - Attached to target JVM and loaded Java agent successfully

Verifique os registros do aplicativo

Agora que anexamos nosso agente à JVM, veremos que temos o tempo total de conclusão da segunda operação de retirada de caixa eletrônico.

Isso significa que adicionamos nossa funcionalidade rapidamente, enquanto nosso aplicativo estava em execução:

22:44:27.229 [Attach Listener] INFO - [Agent] In agentmain method
22:44:27.230 [Attach Listener] INFO - [Agent] Transforming class MyAtm
22:44:33.157 [main] INFO - [Application] Successful Withdrawal of [8] units!
22:44:33.157 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!

5. Criação de um agente Java

Depois de aprender a usar um agente, vamos ver como podemos criar um. Veremoshow to use Javassist para alterar o código de byte e combinaremos isso com alguns métodos de API de instrumentação.

Como um agente Java faz uso deJava Instrumentation API, antes de nos aprofundarmos na criação de nosso agente, vamos ver alguns dos métodos mais usados ​​nesta API e uma breve descrição do que eles fazem:

  • addTransformer - adiciona um transformador ao motor de instrumentação

  • getAllLoadedClasses - retorna uma matriz de todas as classes atualmente carregadas pela JVM

  • retransformClasses - facilita a instrumentação de classes já carregadas, adicionando byte-code

  • removeTransformer - cancela o registro do transformador fornecido

  • redefineClasses - redefine o conjunto de classes fornecido usando os arquivos de classe fornecidos, o que significa que a classe será totalmente substituída, não modificada como comretransformClasses

5.1. Crie os métodosPremaineAgentmain

Sabemos que todo agente Java precisa de pelo menos um dos métodospremain ouagentmain. O último é usado para carregamento dinâmico, enquanto o primeiro é usado para carregar estaticamente um agente java em uma JVM.

Vamos definir ambos em nosso agente para que possamos carregar este agente tanto estática quanto dinamicamente:

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

    LOGGER.info("[Agent] In premain method");
    String className = "com.example.instrumentation.application.MyAtm";
    transformClass(className,inst);
}
public static void agentmain(
  String agentArgs, Instrumentation inst) {

    LOGGER.info("[Agent] In agentmain method");
    String className = "com.example.instrumentation.application.MyAtm";
    transformClass(className,inst);
}

Em cada método, declaramos a classe que queremos alterar e, em seguida, nos aprofundamos para transformar essa classe usando o métodotransformClass.

Abaixo está o código para o métodotransformClass que definimos para nos ajudar a transformar a classeMyAtm.

Neste método, encontramos a classe que queremos transformar e usando o métodotransform . Além disso, adicionamos o transformador ao mecanismo de instrumentação:

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: [" + clazz.getName() + "]", ex);
    }
}

Com isso fora do caminho, vamos definir o transformador para a classeMyAtm.

5.2. Definindo nossoTransformer

Um transformador de classe deve implementarClassFileTransformere implementar o método de transformação.

UsaremosJavassist para adicionar byte-code à classeMyAtm e adicionar um log com o tempo total de transação de retirada do ATW:

public class AtmTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(
      ClassLoader loader,
      String className,
      Class classBeingRedefined,
      ProtectionDomain protectionDomain,
      byte[] classfileBuffer) {
        byte[] byteCode = classfileBuffer;
        String finalTargetClassName = this.targetClassName
          .replaceAll("\\.", "/");
        if (!className.equals(finalTargetClassName)) {
            return byteCode;
        }

        if (className.equals(finalTargetClassName)
              && loader.equals(targetClassLoader)) {

            LOGGER.info("[Agent] Transforming class MyAtm");
            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);
                m.addLocalVariable("opTime", CtClass.longType);
                endBlock.append(
                  "endTime = System.currentTimeMillis();");
                endBlock.append(
                  "opTime = (endTime-startTime)/1000;");

                endBlock.append(
                  "LOGGER.info(\"[Application] Withdrawal operation completed in:" +
                                "\" + opTime + \" seconds!\");");

                m.insertAfter(endBlock.toString());

                byteCode = cc.toBytecode();
                cc.detach();
            } catch (NotFoundException | CannotCompileException | IOException e) {
                LOGGER.error("Exception", e);
            }
        }
        return byteCode;
    }
}

5.3. Criando um arquivo de manifesto do agente

Por fim, para obter um agente Java funcional, precisaremos de um arquivo de manifesto com alguns atributos.

Portanto, podemos encontrar a lista completa de atributos do manifesto na documentação oficial doInstrumentation Package.

No arquivo jar do agente Java final, adicionaremos as seguintes linhas ao arquivo de manifesto:

Agent-Class: com.example.instrumentation.agent.MyInstrumentationAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.example.instrumentation.agent.MyInstrumentationAgent

Nosso agente de instrumentação Java agora está completo. Para executá-lo, consulte a seçãoLoading a Java Agent deste artigo.

6. Conclusão

Neste artigo, falamos sobre a API de instrumentação Java. Examinamos como carregar um agente Java em uma JVM de maneira estática e dinâmica.

Também analisamos como criaríamos nosso próprio agente Java do zero.

Como sempre, a implementação completa do exemplo pode ser encontradaover on Github.