Dieses Blog durchsuchen

Wird geladen...

Sonntag, 13. März 2011

Inject java code in existing classes at runtime

This post described how to inject any java code in existing classes at runtime. It is based on my "Example project of how to create a generic Logging Agent with Java Intrumentation API and Javassist (maven project on github)".

In many situations is it helpful to add own functionality to running (production) code - for debugging/tracing purpose or anything else.

In this case we can use the Java Intrumentation API and Javassist to add our functionalities at runtime. The Java Intrumentation API give us a hook point to instrument any loaded classes. Javassist is a bytecode manipulation library, it can modify and enhance any java bytecode.

The agent implementation class of an "Java Intrumentation API implementation" must have a premain()-Method (registered by the Premain-Class-Attribut of the MANIFEST.MF-File). This method is called by the java runtime at startup. And now we can use the Instrumentation-Instance to register a own ClassFileTransformer.
public static void premain(String agentArgs, Instrumentation instrumentation) {
  instrumentation.addTransformer( new LoggerAgent() );
}
The ClassFileTransformer#transform() method is called for any class loaded by the java runtime.
byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException
So now we can start with Javassist to modify/enhance the loaded class and inject some fields/methods and so on. First we must load the bytecode and build a Javasisst class instance, then we can add some fields and/or code that is invoked before a method is called or after a method is called.
ClassPool pool = ClassPool.getDefault();
CtClass cl = pool.makeClass( new java.io.ByteArrayInputStream( b ) );
if( cl.isInterface() == false ) {
 // adds a class field
 CtField field = CtField.make("String myField", cl );
 cl.addField( field, "my initial value" );

 CtBehavior[] methods = cl.getDeclaredBehaviors();
 for( CtBehavior[] method : methods ) {
  if( method.isEmpty() == false ) {
   // inject some code before/after a method is called
   method.insertBefore( "System.out.println(\"called before\");" );
   method.insertAfter( "System.out.println(\"called after\");" );
  }
 }
 b = cl.toBytecode();
}

return b;
So now we are able to build the agent jar. In the maven project it use the maven-assembly-plugin to create a "fat jar" that includes also the dependencies to Javasisst and slf4j. Simply run mvn clean package and take the "fat jar" from the /target folder.

Now start the java runtime command line with the javaagent attribut:
java -javaagent:loggingagent-0.0.1-SNAPSHOT-jar-with-dependencies.jar ...

Donnerstag, 10. Februar 2011

Maven: Optionale Abhängigkeiten und der Dependency Import Scope

Ich habe mich immer nach dem Sinn des Maven Import-Scope gefragt. Jetzt habe ich einen konkreten sinnvollen Anwendungsfall gefunden.

Wenn in einem Maven-Projekt eine benutzte externe Bibliothek eine optionale Abhängigkeit definiert, dann muss diese optionale Abhängigkeit auch in meinem Projekt als reale Abhängigkeit definiert werden.

Beispiel: Wenn in einem Projekt Hibernate verwendet wird, ist die Entscheidung zu treffen, welche Bytecode Manipulation Bibliothek verwendet werden soll. Hier definiert Hibernate die Bibliotheken Javassist und Cglib als optionale Abhängigkeiten in Modul hibertnate-core. Dabei ist die Versionsnummer von Javassist bzw. Cglib im Modul hibernate-parent definiert.

Soll nun Hibernate zusammen mit Javassist benutzt werden, ist Javassist als direkte Abhängigkeit ins eigene Projekt aufzunehmen.

<project>

 <dependencies>
  <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-core</artifactId>
   <version>3.6.1.Final</version>
  </dependency>

  <dependency>
   <groupId>javassist</groupId>
   <artifactId>javassist</artifactId>
   <version>3.12.0.GA</version>
  </dependency>
 </dependencies>

</project>

Dabei fällt auf das die Versionsnummer von Javassist mit angeben werden muss. Ein Problem ergibt sich nun bei einem Wechsel der Hibernate Version, man muss aufpassen die gleiche Javassist Version zu benutzen wie sie in hibernate-parent definiert ist (bei Hibernate 3.5 kommt Javassist in Version 3.9.0 zum Einsatz, bei Hibernate 3.6 wurde auf Javassist 3.12.0 gewechselt).

Hier kommt nun der Import-Scope zum Einsatz.

<project>

 <dependencyManagement>
  <dependencies>
   <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-parent</artifactId>
    <version>3.6.1.Final</version>
    <type>pom</type>
    <scope>import</scope>
   </dependency>
  </dependencies>
 </dependencyManagement>

 <dependencies>
  <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-core</artifactId>
   <version>3.6.1.Final</version>
  </dependency>

  <dependency>
   <groupId>javassist</groupId>
   <artifactId>javassist</artifactId>
  </dependency>
 </dependencies>

</project>

Bei der Angabe des Import-Scope werden alle "gemanagten" Abhänigkeiten eines Artefakts in das Projekt importiert, so dass diese im eigenen Projekt zur Verfügung stehen.

Wie im Beispiel zu sehen ist, ist Javassist als Abhängigkeit ohne Versionsnummer angegeben. Die Versionskennung für Javassist wird durch das Importieren der Abhängigkeiten aus Modul hibernate-parent aufgelöst.

Das Beispiel kann über github.com/kreyssel geladen werden.