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:
-
Um aplicativo de caixa eletrônico que permite sacar dinheiro
-
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.