Agentes Java

Estándar

matrix-code-agents

En esta ocasión hablaré un poco acerca de un tema avanzado de Java: Los Agentes Java. Un tópico que puede ser desconocido para muchos pero que puede resultar sumamente interesante.

Los Agentes son programas conscientes. Pueden entrar y salir de cualquier software conectado a su sistema. Esto significa que cualquiera que no está desconectado puede ser un agente. Dentro de la Matrix, ellos son todos y no son nadie.
-Morfeo, explicándole qué son los agentes a Neo.

¿Qué son los Agentes Java?

Para ponerlo de una manera simple, se puede decir que los agentes Java son componentes de software que proveen a una aplicación de capacidades de instrumentación de código. En este contexto, por instrumentación me refiero a la capacidad de alterar o redefinir el contenido de una clase en tiempo de ejecución a través de la manipulación del Bytecode de dicha clase.

Así como Neo pudo modificar el código de su entorno para remover una bala del cuerpo de su amada Trinity y resucitarla, de la misma manera un Agente Java puede meterse en la ejecución de una aplicación Java que está corriendo en el JVM y realizar modificaciones al Bytecode. Como pueden imaginarse, esta habilidad es bastante poderosa pero también es peligrosa: se puede hacer prácticamente cualquier cosa pero pueden tirar (fácilmente) el proceso del JVM a la basura si algo sale mal.

Usando sus superpoderes de Instrumentación, un agente puede, entre otras cosas, cambiar la implementación (cuerpos) de métodos o hacer modificaciones al Constant Pool de una clase. Estos cambios no deben añadir, eliminar, o renombrar ningún campo o método, ni cambiar la firma de ningún método de la clase, ni tampoco su herencia.

Cualquier cambio que se realice al Bytecode desde un agente no es verificado antes de su uso. Por tanto, si el Bytecode modificado es erróneo, es responsabilidad del desarrollador la pérdida de millones de dólares por haber explotado el servidor de producción, dejando sin servicio a los usuarios.

 

Estructura de un Agente

Básicamente, un agente es una clase de Java como cualquier otra. De hecho, lo más común es empaquetar un agente dentro de un archivo jar, siguiendo un conjunto de indicaciones que hacen a ese ejecutable ser ejecutado como un agente.

A diferencia de una aplicación regular de Java donde se especifica el punto de entrada a la aplicación definiendo el método main(String[]), un agente debe especificar un clase que implemente un método con la siguiente firma:

public static void premain(String agentArgs, Instrumentation inst)

Este método se convierte en el punto de entrada del agente. Cabe la acotación que la implementación de este método como punto de entrada se usa cuando el agente se ejecuta en conjunto con la JVM de la aplicación, es decir, ambas se empiezan a ejecutar a partir del mismo comando (java JAVA_OPTS -jar ...). Si se quiere ejecutar un agente sobre una JVM que ya ha sido iniciado anteriormente, se debe implementar el siguiente método:

public static void agentmain(String agentArgs, Instrumentation inst)

Bien. Ya tienen una clase que implementa un punto de entrada. ¿Qué más necesitan?. Ahora necesitan una manera de decirle al JVM que el ejecutable es, en realidad, un agente. Esto se logra usando una serie de metadatos ubicados dentro del archivo jar, más específicamente dentro del archivo META-INF/MANIFEST.MF.

Atributos del Manifest

Los siguientes son atributos del manifest definidos para los agentes en archivos jar:

Premain-Class
Cuando se especifica un agente al momento de iniciar un JVM, este atributo especifica la clase agente.
Agent-Class
Si la implementación del JVM soporta el mecanismo para iniciar un agente luego de que el JVM ha iniciado, entonces este atributo especifica la clase agente.
Boot-Class-Path
Una lista de paths usados por el bootstrap class loader.
Can-Redefine-Classes
Boolean que representa la habilidad de redefinir las clases que necesita el agente.
Can-Retransform-Classes
Boolean que representa la habilidad de retransformar clases que necesita el agente.

En la documentación oficial pueden encontrar esta y más información acerca de los agentes con mayor detalle.

 

Mi Primer Agente

Ahora vamos a implementar un pequeño agente para demostrar, de una manera simple, su funcionamiento. Ya sabemos que debemos crear una clase agente con un punto de entrada, y que además este será capaz de modificar el Bytecode de una clase en tiempo de ejecución…mmmm, Bytecode…¿Cómo es eso?.

Pues no es suficiente con saber como crear un agente, también debes saber cómo manipular Bytecode directamente. El API Estándar de Java no provee un mecanismo para manipular el Bytecode, pero afortunadamente existen librerías hechas por la comunidad que ya tienen bastante tiempo y están bastante maduras. Algunas de ellas quizás las han visto como dependencias de otros frameworks pero no siempre nos detenemos a pensar la función de estas dependencias. Tal es el caso de las librerías ASM o Javassist. Estas librerías proveen un API para poder manipular Bytecode, por lo tanto el agente que crearemos dependerá de alguna de ellas para hacer su magia.

Si quieren un poco más de información acerca de la manipulación de Bytecode, pueden leer este artículo y este otro.

El programa principal

Este es el programa que el agente va a modificar en tiempo de ejecución. Para este ejemplo vamos a usar un programa bastante sencillo como es el típico ‘Hola Mundo’. Sin embargo, tomen en consideración que se puede tratar de cualquier ejecutable Java o librería que no deseen modificar directamente y tener que recompilar para aplicar el cambio que ustedes quieran.

La clase que usaremos como programa principal será la siguiente:

	 	 
package matrix;	 	 
public class Person {	 	 
 public static void main(String[] args) {	 	 
   System.out.println("I'm connected to the matrix");	 	 
 }	 	 
}	 	 

Como pueden observar, es una aplicación bastante sencilla. La idea es que esta aplicación ejecute un comportamiento y que luego cuando se ejecute junto al agente, este comportamiento sea modificado.

El agente

Para este ejemplo, vamos a utilizar Javassist porque es una librería con un nivel de abstracción un poco más alto que ASM, haciéndolo especialmente poderoso para este ejemplo.

Clase del agente

package agent;
	 	 
import javassist.ClassPool;	 	 
import javassist.CtClass;	 	 
import javassist.CtMethod;	 	 
import java.lang.instrument.ClassFileTransformer;	 	 
import java.lang.instrument.IllegalClassFormatException;	 	 
import java.lang.instrument.Instrumentation;	 	 
import java.security.ProtectionDomain;
	 	 
public class Smith {
	 	 
  public static void premain(String agentArgs, Instrumentation inst) {	 	 
    inst.addTransformer(new ClassFileTransformer() {
	 	 
    @Override	 	 
    public byte[] transform(ClassLoader classLoader, String className, Class<?> aClass, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {	 	 
      if ("matrix/Person".equals(className)) {	 	 
        try {	 	 
          ClassPool cp = ClassPool.getDefault();	 	 
          CtClass cc = cp.get("matrix.Person");	 	 
          CtMethod m = cc.getDeclaredMethod("main");	 	 
          m.setBody("{System.out.println(\"Now I'm Agent Smith\");}");
	 	 
          byte[] byteCode = cc.toBytecode();	 	 
          cc.detach();	 	 
          return byteCode;	 	 
        } catch (Exception ex) {	 	 
          ex.printStackTrace();	 	 
        }	 	 
      }	 	 
      return null;	 	 
    }	 	 
    });	 	 
  }	 	 
}	 	 

Esta es la clase encargada de modificar el comportamiento de nuestro programa principal. Vamos a describir la estructura de esta clase:

  • Línea 13: inicio de la definición del método premain, requerido para la creación de un agente por el Instrumentation API.
  • Línea 14: Añadimos una implementación de la interfaz ClassFileTransformer (en este caso es Anónima), quien tendrá toda la lógica encargada de modificar nuestro programa principal.
  • Línea 17: hacemos Override del método transform(...), que será invocado por el JVM cuando se empiecen a cargar las clases.
  • Línea 18: esta instrucción condicional garantiza que solamente vayamos a modificar la clase que queremos.

A partir de este punto empezaremos a usar llamados a Javassist.

  • Línea 20, 21, 22: Indicamos a Javassist que cargue la clase “matrix.Person“, y en particular el método main de esa clase.
  • Línea 23: sobreescribimos totalmente el método de la clase cargada. En este caso simplemente estamos reemplazando el cuerpo por una instrucción sencilla que imprima un mensaje por pantalla.
  • Línea 25, 26, 27: liberamos recursos y retornamos la nueva clase modificada para que sea usada en lugar de la original.

Configuración del agente

Ya tenemos tanto el programa principal que se usará en el ejemplo como el agente encargado de modificar al programa principal. Sin embargo, como ya mencioné anteriormente, es necesario declarar explícitamente que el jar que contiene la clase agente es, bueno, un agente. Para eso agregamos estas dos líneas en el archivo MANIFEST.MF dentro del archivo jar.

MANIFEST.MF

Premain-Class: agent.Smith
Can-Retransform-Classes: true

Ejecución del agente

Ahora veremos cómo se ejecuta un agente. Primero que nada vamos a ejecutar la aplicación principal para que veamos el comportamiento que tiene. Esto lo logramos, como ya deben saber, con el siguiente comando para ejecutar archivos ejecutables .jar

java -jar matrix-person-1.0-SNAPSHOT.jar

Este comando ejecutará el archivo y mostrará lo siguiente por la consola:

I'm connected to the matrix

Muy bien. Ya sabemos que nuestra aplicación principal funciona y que desafortunadamente aún sigue enlazada con la Matrix. Ahora vamos a ejecutar el mismo programa principal pero esta vez vamos a ejecutarlo con el agente que hemos creado.

Para ejecutar un agente se debe usar la opción -javaagent:<jarpath>[=<options>]. Entonces, el comando nos va a quedar de la siguiente manera:

java -javaagent:smith-1.0-SNAPSHOT.jar -jar matrix-person-1.0-SNAPSHOT.jar

Un punto importante a tomar en cuenta es que, obligatoriamente, la opción “-javaagent” debe ser colocada antes de la opción “-jar. Si invierte este orden, lo que sucederá es que el texto “-javaagent:...” se le pasará al programa principal como argumento y no al ejecutable java que es el encargado de ejecutar el agente.

Habiendo ejecutado el comando anterior (y en el orden correcto), lo que obtenemos en la consola es lo siguiente:

Now I'm Agent Smith

El agente, convenientemente llamado “Smith”, ha reemplazado el comportamiento del método main con su propia definición. Ha reescrito y transformado la clase del programa principal. Bastante cool, ¿verdad?.

Cabe destacar que se pueden ejecutar múltiples agentes dentro del mismo comando. Solamente hay que especificar múltiples opciones -javaagent al comando y se ejecutarán en el orden en que fueron especificados. De esta manera pueden tener diferentes agentes para diferentes funcionalidades en lugar de tener un único agente cargado de diferentes responsabilidades.

En este ejemplo que hemos creado estamos ejecutando el agente al inicio de la ejecución del JVM, es decir, tanto el JVM como el agente se están iniciando “al mismo tiempo”. Sin embargo, como mencioné anteriormente también es posible que agentes se “adhieran” a procesos del JVM que se encuentran en ejecución, teniendo así la posibilidad de modificar el comportamiento de clases cargadas dentro de JVM que ya lleva mucho tiempo ejecutándose y quizás no sea viable la opción de reiniciarlo para ejecutarlo con el agente.

 

Casos Reales

Muchos de ustedes se preguntarán, ¿Y esto para qué me sirve?. Pues a continuación les muestro algunos casos de uso o proyectos reales que hacen uso de los agentes.

Monitoreo

Este es un caso de uso bastante práctico. En lugar de introducir código en su aplicación principal para realizar mediciones y tomar datos estadísticos, principalmente con la finalidad de obtener indicaciones de los tiempos de respuesta de la aplicación, se puede tener un agente que haga estas mediciones. ¿Cuánto tiempo demora un método en su ejecución?, ¿Cuáles campos son los accesados con mayor frecuencia?. Este tipo de preguntas pueden ser respondidas por un agente diseñado para tal fin. De esta manera tendrán bien separado la lógica de negocios de la aplicación del código recolector de datos.

VMLens

VMLens es un agente Java diseñado para detectar la existencia de “condición de carrera” en los accesos a los campos de las clases.

Colorización de Maven

Este proyecto da color a la salida generada por Maven en la consola. Este es un caso del mundo real donde el agente modifica el comportamiento de Maven sin necesidad de modificar el código fuente del programa principal. Simplemente usando un Agente Java fue posible lograr el comportamiento deseado.

Byteman

Esta herramienta creada por la gente de JBoss permite trazar, monitorear y probar el comportamiento de aplicaciones Java.

 

Conclusiones

Al final hemos visto qué son y para que se usan los Agente Java. Componentes que pueden modificar el comportamiento de las clases en tiempo de ejecución. Sumamente poderosos y peligrosos, pero también interesantes

Pueden descargarse el proyecto de este ejemplo del repositorio GitHub aquí.

Si te gustó, ¡comparte!Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInShare on Reddit