Intro
In this blog, I would like to describe one of techniques that can be used to flexibly change application logic executed by a Java application server - or, to be more precise, within Java Virtual Machine (JVM) of its server nodes. JVM executes preliminary compiled and deployed platform agnostic bytecode (which is an outcome of Java source code compilation), and the technique described below is based on the concept of bytecode manipulation. Using this technique, it is possible to introduce nearly any changes to already deployed Java application by operating on its bytecode level which is interpreted by JVM at runtime, without modifying application's source code (since latter would mean necessity of re-compilation, re-assembly and re-deployment of an application).
When this technique may be useful and why not to simply make necessary changes in source code of a Java application and promote them to an application server? Here are some examples:
- We don’t have consistent original development project – for example, if original project is not available, and decompilation doesn't allow us to reproduce complete project structure and artefacts necessary for successful build and assembly of an application;
- We need to apply an ad-hoc patch / logic amendment to an already running application in order to make rapid test before developing and assembling complete patch;
- We need to collect runtime specific information about executed classes (all or just selected ones);
- Or we just want to break into already deployed application and hack into its logic.
The blog mostly contains examples illustrating usage of bytecode instrumentation and manipulation technique. Several remarks regarding presented demo applications shall be made upfront:
- In order to avoid irrelevant complexity, examples are based on a standalone Java application. Since the described capability is a part of JVM features and is not specific to a particular application server implementation, it can be adopted and used in real life scenarios in conjunction with various application servers (SAP Application Server Java being one of them);
- All development is simplified, so that amount of code lines is reduced to reasonable minimum and we may stay focused on core subject, even though resulting in heavy usage of hard coded values and simplistic class model design. In real life development, majority of hard coded values shall be exposed as configurable parameters;
- In a standalone program and complementary developed classes, output to console is used intensively to make it transparent and informative, when corresponding objects are called and what their state is. In real life development, such verbose output shall be disabled or implemented using logging framework of an application server with respective appropriate logging level / severity.
In sake of better readability and clarity, following values are inserted before corresponding log entries in console output:
- Output of calls coming from program main class is preceded with "[Application - Main]";
- Output of calls coming from class that is responsible for displaying text in console and that is called from program main class is preceded with "[Application - Text display]";
- Output of calls coming from subsequent instrumentation examples is preceded with "[Instrumentation]";
- Output of calls coming from subsequent agent specific examples are preceded with "[Agent]".
In order to make segregation of functionality used in demonstrations more obvious, developed classes are located in following packages:
- Java application that we are going to break into and instrument, is located in a package vadim.demo.jvm.app;
- Java agent that is used in demonstration of instrumentation via agent loading, is located in a package vadim.demo.jvm.agent;
- Java agent loader application that is used in demonstration of attachment to a running JVM from external application, is located in a package vadim.demo.jvm.agent.loader.
Correspondingly, instrumented Java application, Java agent and Java agent loader are located in three different Java projects, leading to following projects structure:
I will start from a basic application and will gradually enhance implemented features in order to illustrate various practical aspects of discussed topics and techniques, so used projects and their content will change over time across this blog.
Demo application
Let's use following small standalone Java program as a starting point for future enrichment and manipulation. The program consists of two classes: main class DemoApplication and class Text, called from a main class.
Class DemoApplication implements method main() and is an entry point for the called Java program:
package vadim.demo.jvm.app; public class DemoApplication { public static void main(String[] args) { System.out.println("[Application - Main] Start application"); String value = "Demonstration of Java bytecode manipulation capabilities"; Text text = new Text(); System.out.println("[Application - Main] Value passed to text display: " + value); text.display(value); System.out.println("[Application - Main] Complete application"); } }
Class Text is called from a main class and issues given text to console output after waiting for a second:
package vadim.demo.jvm.app; public class Text { public void display(String text) { long sleepTime = 1000; long sleepStartTime; long sleepEndTime; System.out.println("[Application - Text display] Text display is going to sleep for " + sleepTime + " ms"); sleepStartTime = System.nanoTime(); try { Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } sleepEndTime = System.nanoTime(); System.out.println("[Application - Text display] Text display wakes up"); System.out.println("[Application - Text display] Text display sleep time: " + ((sleepEndTime - sleepStartTime) / 1000000) + " ms"); System.out.println("[Application - Text display] Output: " + text); } }
Sample execution of this program will result into following console output:
[Application - Main] Start application [Application - Main] Value passed to text display: Demonstration of Java bytecode manipulation capabilities [Application - Text display] Text display is going to sleep for 1000 ms [Application - Text display] Text display wakes up [Application - Text display] Text display sleep time: 1000 ms [Application - Text display] Output: Demonstration of Java bytecode manipulation capabilities [Application - Main] Complete application
Now let's see what is bytecode instrumentation and how it can be applied.
Bytecode instrumentation and manipulation
Starting from Java 5, JDK provides developers with functionality of so-called instrumentation of bytecode. This technique aims possibility of modification of bytecode that is loaded into JVM and executed by it - for example, its extension with additional instructions or other changes of original bytecode. It shall be noted that bytecode instrumentation doesn't cause any change to original resource of bytecode (class file). It manipulates bytecode on-the-fly at a time when class loader attempts to access and load bytecode of corresponding queried class into JVM, extending or substituting bytecode obtained from an original resource, with its instrumented version. Main interface that is used for instrumentation, is java.lang.instrument.Instrumentation. Instrumentation interface provides capabilities to add a custom transformer implementation that will be triggered when class bytecode is loaded into JVM, and can extend or replace original bytecode of a class with the custom one, submitted on-the-fly. Please pay attention that if a class has already been loaded, manipulation with its bytecode is irrelevant. It is still technically possible to instrument required class, but it will imply necessity of developing enhanced class loader logic for that class and extending it with callable class reloading or unloading functionality - and this may become not trivial task, since standard class loaders do not provide class unload feature.
Manipulation with bytecode is different from editing original Java source code, since we need to operate with compiled JVM instructions, and not with original Java statements. Low level interference into bytecode requires good knowledge of structure of class files containing bytecode. Luckily, there are several libraries that simplify operations with bytecode - below are several most commonly used of them, split by level of abstraction from the generated bytecode:
Level of abstraction from bytecode | Description | Examples |
---|---|---|
Low level | Libraries require manipulation directly on bytecode level. Commonly they provide most feature-rich capabilities, but are also most complex in usage in comparison to other bytecode manipulation tools. | ASM (ASM - Home Page) |
Intermediate level | Libraries provide some level of abstraction from bytecode and simplify its modification. For example, instead of modifying bytecode, changes can be done using Java-like syntax and are then compiled to bytecode and amended to original bytecode by a used library. Commonly they are lacking functionality for modified code verification - which means, errors may pass through unnoticed during modification preparation and are then observed at runtime. | Javassist (Javassist by jboss-javassist) |
High level | Libraries operate with high-level instructions and commonly are equipped with toolset for syntax verification. Unfortunately, highest level of abstraction from modified bytecode commonly leads to loss of some features that are only available during direct modification of bytecode. | AspectJ (The AspectJ Project) |
In further examples in this blog, I will use Javassist library as a compromise between necessity of modifying low level bytecode and getting too abstracted from it.
Let's enhance base logic of the demo application and instrument it. Provided example incorporates several different instrumentation activities and illustrates how we can achieve following modifications:
- Insert extra code before execution of a given method of an instrumented class;
- Insert extra code after execution of a given method of an instrumented class;
- Inject extra code in the middle of a given method of an instrumented class;
- Modify existing code of a given method of an instrumented class.
Few key points to be taken into consideration:
- Javassist provides capability to access compile time class definition (which is rendered version of its bytecode);
- It is then possible to iterate through class methods or access method by its name and descriptor. Please pay attention to notation of method descriptor - it corresponds to bytecode compatible notation rather than the one defined in Java language specification;
- For a given method, it is possible to insert arbitrary code before or after method, or inject code at a given code line. Please pay attention to syntax - injected code lines are Java-like strings with some modifications (like proper escaping of some special characters, possible usage of placeholders, etc.). Here right before call of System.output.println(), we inject assignment of another value to a used variable so that console output contains value which is different from the one passed from a program main class;
- It is also possible to change already existing bytecode by introducing so-called expression editor implementation, which can intercept and replace constructor and method calls, access to class fields, exceptions handling, etc. In this example, we suppress call of a method sleep(), so that program doesn't wait before issuing provided text to console output.
For full documentation on library capabilities and other examples of its usage, please refer to API reference at official web site.
Class DemoApplication is enhanced correspondingly - instrumentation of bytecode is implemented in method enableInstrumentation():
package vadim.demo.jvm.app; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.NotFoundException; import javassist.expr.ExprEditor; public class DemoApplication { public static void main(String[] args) { System.out.println("[Application - Main] Start application"); String value = "Demonstration of Java bytecode manipulation capabilities"; enableInstrumentation(); Text text = new Text(); System.out.println("[Application - Main] Value passed to text display: " + value); text.display(value); System.out.println("[Application - Main] Complete application"); } private static void enableInstrumentation() { String instrumentedClassName = "vadim.demo.jvm.app.Text"; String instrumentedMethodName = "display"; String instrumentedMethodDescriptor = "(Ljava/lang/String;)V"; try { ClassPool cPool = ClassPool.getDefault(); CtClass ctClass = cPool.get(instrumentedClassName); CtMethod ctClassMethod = ctClass.getMethod(instrumentedMethodName, instrumentedMethodDescriptor); ctClassMethod.insertBefore("System.out.println(\"[Instrumentation] Entering instrumented method\");"); ctClassMethod.insertAfter("System.out.println(\"[Instrumentation] Exiting instrumented method\");"); ctClassMethod.insertAt(24, true, "text = \"Original text was replaced by instrumentation from agent\";"); ExprEditor instrumentationExpressionEditor = new DemoExpressionEditor(); ctClassMethod.instrument(instrumentationExpressionEditor); ctClass.toClass(); } catch (NotFoundException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); } } }
(It sometimes happens that syntax highlight function in SCN suppresses empty lines, so just keep in mind that code line 24 that is mentioned in code above, corresponds to an empty line right before call of System.out.println() that issues given text to console output)
Additionally, class DemoExpressionEditor that extends Javassist's class javassist.expr.ExprEditor, was implemented. DemoExpressionEditor contains implementation of logic that is further used to instrument called method:
package vadim.demo.jvm.app; import javassist.CannotCompileException; import javassist.expr.ExprEditor; import javassist.expr.MethodCall; public class DemoExpressionEditor extends ExprEditor { @Override public void edit(MethodCall method) throws CannotCompileException { if (method.getMethodName().contains("sleep")) { System.out.println("[Instrumentation] Suppressing sleep for " + method.getClassName() + "." + method.getMethodName() + " called from " + method.getEnclosingClass().getName()); method.replace("{}"); } } }
Let's now execute DemoApplication again and see how console output differs from an original version:
[Application - Main] Start application [Instrumentation] Suppressing sleep for java.lang.Thread.sleep called from vadim.demo.jvm.app.Text [Application - Main] Value passed to text display: Demonstration of Java bytecode manipulation capabilities [Instrumentation] Entering instrumented method [Application - Text display] Text display is going to sleep for 1000 ms [Application - Text display] Text display wakes up [Application - Text display] Text display sleep time: 0 ms [Application - Text display] Output: Original text was replaced by instrumentation from agent [Instrumentation] Exiting instrumented method [Application - Main] Complete application
From this output, it can be seen, when instrumentation logic was called and how it affected executed program - particularly, called class responsible for displaying text: custom code was executed before and after instrumented method, thread didn't effectively run into sleep state and issued console output that was not originally designed in demo Java application. This all demonstrates how we could introduce major changes to one of application classes logic at runtime without a single change in that class source code. It is true to state that generally speaking, we are not limited to invoke instrumentation logic from this particular application that is making calls of an instrumented class - it could have been literally any other application that is running in the same JVM. Here already developed application was used to invoke instrumentation logic just in sake of simplicity and avoidance of superfluous over-complication.
Java Agent and Attach API
Up to now, we got familiar with some fundamentals of bytecode instrumentation, but example provided above is still not flexible too much - we needed to embed extra logic into an application or would have needed to deploy some other application that would instrument required classes bytecode. Let's take step forward and explore how we can decouple instrumenting application from instrumented application (used demo Java application). Such a concept exists in JVM already for a while and is known as Java agents. Java agent is an application bundled in a specific way and commonly delivered as a standalone JAR file (optionally with extra dependencies required for it), which contains implementation of instrumentation logic and can be attached to Java applications in sake of their instrumentation.
There are two ways how a Java agent can be started and loaded into instrumented JVM (refer to documentation on the package java.lang.instrument - for example, located at java.lang.instrument (Java Platform SE 8 ) - for more information):
- Start agent during JVM startup, also known as static load. This is achieved by using additional JVM argument "-javaagent" and specifying location of a JAR file of an agent as a value of this argument (optionally, if agent accepts any parameters or options, they can also be passed as a part of an argument value): -javaagent:jarpath[=options]. Multiple agents can be loaded using this approach - several entries of an argument "-javaagent" have to be specified, where each one refers to individual loaded agent. If done so, agents will be loaded in a sequence as corresponding arguments "-javaagent" appear in JVM arguments list. Advantage of this approach is that agent code is loaded before JVM calls method main() of a started Java application. Loading instrumentation before instrumented application guarantees that a Java application is instrumented during its complete runtime lifecycle within JVM. Disadvantage is, it is not possible to dynamically instrument already running Java application and if for some reason valid argument "-javaagent" was not specified before initial startup of a Java application, JVM needs to be restarted (for example, server node has to be restarted) before agent can be loaded and instrumentation can take place;
- Start agent after JVM startup and attach it to already running JVM, also known as dynamic load. This is achieved by using Attach API, which is one of diagnostic interfaces exposed by modern JVMs. The idea of this approach is that using Attach API of JVM, we can connect to a JVM (attach to it) and load valid agent from a specific JAR file with necessary optional arguments, literally at any time of Java application execution. Attachment to a running JVM can be invoked from within some Java application running in this JVM, but it can also initiated from a foreign / external JVM process - which gives us possibility to develop an external application that will attach to a running JVM process and load an agent into it (surely, corresponding security implications have to be taken into consideration). Such dynamic agent loading mechanism resolves major disadvantage of the previously described approach - namely, necessity of JVM restart in case we need to instrument Java application, but didn't specify appropriate argument "-javaagent" beforehand, and we also don't need to specify additional JVM properties like "-javaagent" anymore. This approach also has a drawback: since agent implementing instrumentation starts after Java application startup, some Java application classes may have already been loaded by class loaders using their original (non-instrumented) bytecode version - which leads to lack of instrumentation of earlier executed application logic and necessity of managing class reloading/unloading for such affected (already loaded) classes. In order to get better visibility on which classes were loaded, it is possible to enable class loading logging using JVM argument "-verbose:class", and then check from log, if an instrumented class has already been loaded into JVM before agent was loaded, and which resource provided bytecode for a loaded class (please don't enable this argument in production or large scale application servers or logs may be overloaded with entries generated for class loaders activity). For details regarding Attach API, please refer to official documentation - for example, Attach API. Corresponding API implementation is located in library tools.jar, which can be found in JDK distribution.
JVM provides convenient ways to load Java agent using either of approaches described above, but it doesn't provide straightforward ways to unload Java agent. Reason for this is, Java agent itself is a set of specifically organized classes, which are loaded into JVM using class loading mechanism during Java agent startup. And, as mentioned earlier, JVM doesn't provide generic mechanism for class unloading. Which means, if it is necessary to enable Java agent not only to be loaded, but also unloaded, it will be required to develop class unloading logic.
You may have already come across and used Java agents for system and application performance monitoring of SAP Application Server Java based systems like PI/PO, EP, CE - one of good examples is Wily Introscope Agent, which is a part of Wily Introscope infrastructure - de-facto toolset used for continuous real time and retrospective monitoring and analysis of performance of SAP Application Server Java components and applications running on top of it, and providing invaluable information regarding collected metrics and telemetry information on JVM, application server and running applications.
Here I will not go into details of Java agent specification - this is already well described in JDK documentation for the package java.lang.instrument. I will only highlight some major key points:
- Java agent main class has to implement corresponding methods that will be invoked during agent startup: method premain() for agents starting during JVM startup, method agentmain() for dynamically loaded agents. An agent can implement both these methods if it needs to support both approaches of Java agent startup described above;
- Agent class doesn't really implement any specific Java interface, but implemented methods premain() / agentmain() have to conform to an expected method declaration;
- Methods premain() / agentmain() implement invocation of instrumentation / bytecode manipulation logic - most commonly, by adding custom bytecode / class file transformer;
- Java agent is assembled in a JAR file;
- Together with Java agent classes, required other classes and dependencies, assembled JAR file has to contain manifest file that at least specifies corresponding classes containing implementation of methods premain() / agentmain() (it can be the same class or different classes) that will be called at agent startup.
In the example below, I developed a Java agent that can be started in either of described approaches and that implements same instrumentation logic as demonstrated in earlier examples.
Class DemoAgent contains implementation of methods premain() and agentmain():
package vadim.demo.jvm.agent; import java.lang.instrument.Instrumentation; public class DemoAgent { public static void premain(String args, Instrumentation instrumentation) { System.out.println("[Agent] Start agent during JVM startup using argument '-javaagent'"); instrumentation.addTransformer(new DemoClassFileTransformer()); } public static void agentmain(String args, Instrumentation instrumentation) { System.out.println("[Agent] Load agent into running JVM using Attach API"); instrumentation.addTransformer(new DemoClassFileTransformer()); } }
Class DemoClassFileTransformer is called from agent main class and implements class file transformer logic / bytecode instrumentation:
package vadim.demo.jvm.agent; import java.io.ByteArrayInputStream; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.expr.ExprEditor; public class DemoClassFileTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { String instrumentedClassName = "vadim.demo.jvm.app.Text"; String instrumentedMethodName = "display"; byte[] bytecode = classfileBuffer; try { ClassPool cPool = ClassPool.getDefault(); CtClass ctClass = cPool.makeClass(new ByteArrayInputStream(bytecode)); CtMethod[] ctClassMethods = ctClass.getDeclaredMethods(); for (CtMethod ctClassMethod : ctClassMethods) { if (ctClassMethod.getDeclaringClass().getName().equals(instrumentedClassName) && ctClassMethod.getName().equals(instrumentedMethodName)) { ctClassMethod.insertBefore("System.out.println(\"[Instrumentation] Entering method\");"); ctClassMethod.insertAfter("System.out.println(\"[Instrumentation] Exiting method\");"); ctClassMethod.insertAt(24, true, "text = \"Original text was replaced with instrumentation by agent\";"); ExprEditor instrumentationExpressionEditor = new DemoExpressionEditor(); ctClassMethod.instrument(instrumentationExpressionEditor); bytecode = ctClass.toBytecode(); } } } catch (IOException e) { throw new IllegalClassFormatException(e.getMessage()); } catch (RuntimeException e) { throw new IllegalClassFormatException(e.getMessage()); } catch (CannotCompileException e) { throw new IllegalClassFormatException(e.getMessage()); } return bytecode; } }
Since we aim replication of identical instrumentation logic to the one used in previous example, I also replicate class DemoExpressionEditor (implementation of an expression editor used by Javassist library for more sophisticated bytecode modification):
package vadim.demo.jvm.agent; import javassist.CannotCompileException; import javassist.expr.ExprEditor; import javassist.expr.MethodCall; public class DemoExpressionEditor extends ExprEditor { @Override public void edit(MethodCall method) throws CannotCompileException { if (method.getMethodName().contains("sleep")) { System.out.println("[Instrumentation] Suppressing sleep for " + method.getClassName() + "." + method.getMethodName() + " called from " + method.getEnclosingClass().getName()); method.replace("{}"); } } }
Manifest file MANIFEST.MF:
Manifest-Version: 1.0 Premain-Class: vadim.demo.jvm.agent.DemoAgent Agent-Class: vadim.demo.jvm.agent.DemoAgent
After an agent is developed, we compile it, build project and export to a JAR file, let it be "DemoAgent.jar".
We also revert demo Java application to its original version deleting all instrumentation logic we embedded into it afterwards, so that the only instrumentation is done by an agent.
Firstly, let's start this agent at JVM startup using JVM argument "-javaagent". JVM arguments of demo Java application have been adopted in a following way:
Now I run demo application again - and here is console output that is produced by it:
[Agent] Start agent during JVM startup using argument '-javaagent' [Application - Main] Start application [Instrumentation] Suppressing sleep for java.lang.Thread.sleep called from vadim.demo.jvm.app.Text [Application - Main] Value passed to text display: Demonstration of Java bytecode manipulation capabilities [Instrumentation] Entering method [Application - Text display] Text display is going to sleep for 1000 ms [Application - Text display] Text display wakes up [Application - Text display] Text display sleep time: 0 ms [Application - Text display] Output: Original text was replaced with instrumentation by agent [Instrumentation] Exiting method [Application - Main] Complete application
It may be noticed that agent was really started before method main() of a demo application was called.
Another technique that brings us to yet another level of bytecode instrumentation flexibility, is dynamic load of an agent. To make demo more interesting, let's start a pure demo Java application (with no embedded instrumentation or Java agent starting up along with JVM), and then connect / attach to that JVM from another process (that is another Java application - Java agent loader) and load a Java agent implementing bytecode instrumentation logic. To do that, Java agent loader application has to be started on the same host as running JVM process executing demo Java application - this will allow it to identify running JVM and attach to it.
We again revert to original state of demo Java application, removing earlier introduced JVM argument "-javaagent". The only minor change that I will make to demo Java application logic is a wait time - just few seconds - right at the beginning of its execution, so that I will have some time to run Java agent loader application after demo Java application starts and before it completes its work:
package vadim.demo.jvm.app; public class DemoApplication { public static void main(String[] args) { System.out.println("[Application - Main] Start application"); suspend(5000); String value = "Demonstration of Java bytecode manipulation capabilities"; Text text = new Text(); System.out.println("[Application - Main] Value passed to text display: " + value); text.display(value); System.out.println("[Application - Main] Complete application"); } private static void suspend(long sleepTime) { try { Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } } }
I also introduce another application - Java agent loader, that will get a list of locally running JVMs, identify the one that executes demo Java application, attach to it using Attach API, load a Java agent (I will use a Java agent from a previous example), and detach afterwards leaving a target JVM with instrumented bytecode for a specific class:
package vadim.demo.jvm.agent.loader; import java.io.File; import java.util.List; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; public class DemoAgentLoader { public static void main(String[] args) { String agentFilePath = "D:/tmp/DemoAgent.jar"; String jvmAppName = "vadim.demo.jvm.app.DemoApplication"; String jvmPid = null; List<VirtualMachineDescriptor> jvms = VirtualMachine.list(); for (VirtualMachineDescriptor jvm : jvms) { System.out.println("Running JVM: " + jvm.id() + " - " + jvm.displayName()); if (jvm.displayName().equals(jvmAppName)) { jvmPid = jvm.id(); } } if (jvmPid != null) { File agentFile = new File(agentFilePath); if (agentFile.isFile()) { String agentFileName = agentFile.getName(); String agentFileExtension = agentFileName.substring(agentFileName.lastIndexOf(".") + 1); if (agentFileExtension.equalsIgnoreCase("jar")) { try { System.out.println("Attaching to target JVM with PID: " + jvmPid); VirtualMachine jvm = VirtualMachine.attach(jvmPid); jvm.loadAgent(agentFile.getAbsolutePath()); jvm.detach(); System.out.println("Attached to target JVM and loaded Java agent successfully"); } catch (Exception e) { throw new RuntimeException(e); } } } } else { System.out.println("Target JVM running demo Java application not found"); } } }
We are now ready for another test. I firstly run demo Java application, and then immediately switch to Java agent loader application and run it. Below are console outputs that I get for each of them.
Demo Java application:
[Application - Main] Start application [Agent] Load agent into running JVM using Attach API [Instrumentation] Suppressing sleep for java.lang.Thread.sleep called from vadim.demo.jvm.app.Text [Application - Main] Value passed to text display: Demonstration of Java bytecode manipulation capabilities [Instrumentation] Entering method [Application - Text display] Text display is going to sleep for 1000 ms [Application - Text display] Text display wakes up [Application - Text display] Text display sleep time: 0 ms [Application - Text display] Output: Original text was replaced with instrumentation by agent [Instrumentation] Exiting method [Application - Main] Complete application
Java agent loader application:
Running JVM: 10712 - Running JVM: 8904 - vadim.demo.jvm.app.DemoApplication Running JVM: 12156 - vadim.demo.jvm.agent.loader.DemoAgentLoader Attaching to target JVM with PID: 8904 Attached to target JVM and loaded Java agent successfully
It can be noticed that we again succeeded in achieving instrumentation of required class, but this time, as output depicts, Java agent was loaded after demo Java application was started. It shall be noted that bytecode instrumentation of that class was successful, because it wasn't yet loaded into JVM by one of class loaders - and this was because Java agent was loaded and bytecode instrumentation took place before instrumented class was first accessed and loaded (which happened later during creation of an object instance of that class). If Java agent would have been loaded after original bytecode of that class is loaded into JVM, result would be different. This can be easily verified by moving thread sleep call at a later block of demo Java application code, for example, right after new instance of class Text is created:
package vadim.demo.jvm.app; public class DemoApplication { public static void main(String[] args) { System.out.println("[Application - Main] Start application"); String value = "Demonstration of Java bytecode manipulation capabilities"; Text text = new Text(); suspend(5000); System.out.println("[Application - Main] Value passed to text display: " + value); text.display(value); System.out.println("[Application - Main] Complete application"); } private static void suspend(long sleepTime) { try { Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } } }
Let's now repeat test. Following console output will be evidenced.
Demo Java application:
[Application - Main] Start application [Agent] Load agent into running JVM using Attach API [Application - Main] Value passed to text display: Demonstration of Java bytecode manipulation capabilities [Application - Text display] Text display is going to sleep for 1000 ms [Application - Text display] Text display wakes up [Application - Text display] Text display sleep time: 1000 ms [Application - Text display] Output: Demonstration of Java bytecode manipulation capabilities [Application - Main] Complete application
Java agent loader application:
Running JVM: 9156 - vadim.demo.jvm.app.DemoApplication Running JVM: 10712 - Running JVM: 11740 - vadim.demo.jvm.agent.loader.DemoAgentLoader Attaching to target JVM with PID: 9156 Attached to target JVM and loaded Java agent successfully
As seen, Java agent was loaded into JVM, but it was too late for instrumentation to take effect - instrumented class has already been loaded using its original bytecode version.
Variation of this technique usage would be dynamic load of Java agent into running JVM by an application that is executed within the same JVM. In a SAP PI/PO system, this can be, for example, some custom developed application being called from Java Scheduler job, from WebDynpro / SAPUI5 user interface, from an HTTP servlet / JSP, or from variety of other components.
Outro
Hopefully, few examples provided in this blog were illustrative enough to give outlook and perceive principal capabilities of bytecode instrumentation and Java agent / Attach API in JVM. Surely, this is not a technique that is commonly used in day to day activities of PI developer or NetWeaver technology consultant, but it is worth being aware of it. And it is definitely worth for security team to familiarize themselves with this technique and possible consequences of its usage, and implement preventive measures to keep administered Java based systems secure, since unauthorized attachment to running server node's JVM and dynamic agent load combined with malicious instrumentation is obvious security potentially leading to compromising of an application or the whole system. Thus, it is strongly recommended that any bytecode instrumentation attempt is handled with care caution, and evaluated against its impact on deployed application, entire JVM / server node and even system.