Wednesday, July 8, 2020

Java Agents

Java Agents, Oracle Java Tutorial and Material, Oracle Java Exam Prep, Oracle Java Certification

1. Introduction


We are going to talk about Java agents, a real black magic for regular Java developers out there. Java agents are able to “intrude” into the execution of Java applications running on the JVM at runtime by performing the direct modifications of the bytecode. Java agents are extremely as powerful as dangerous: they can do mostly everything however if something goes wrong, they can easily crash the JVM.

The goal of this part is to demystify Java agents by explaining how they work, how to run them and showing off some simple examples where Java agents come as a clear advantage.

2. Java Agent Basics


In its essence, a Java agent is a regular Java class which follows a set of strict conventions. The agent class must implement a public static void premain(String agentArgs, Instrumentation inst) method which becomes an agent entry point (similar to the main method for regular Java applications).

Once the Java Virtual Machine (JVM) has initialized, each such premain(String agentArgs, Instrumentation inst) method of every agent will be called in the order the agents were specified on JVM start. When this initialization step is done, the real Java application main method will be called.

However, if the class does not implement public static void premain(String agentArgs, Instrumentation inst) method, the JVM will try to look up and invoke another, overloaded version, public static void premain(String agentArgs). Please notice that each premain method must return in order for the startup phase to proceed.

Last but not least, the Java agent class may also have a public static void agentmain(String agentArgs, Instrumentation inst) or public static void agentmain(String agentArgs) methods which are used when the agent is started after JVM startup.

It looks simple on the first glance but there is one more thing which Java agent implementation should provide as part of its packaging: manifest. Manifest files, usually located in the META-INF folder and named MANIFEST.MF, contain a various metadata related to package distribution.

We have not talked about manifests along this tutorial because most of the time they are not required, however this is not the case with Java agents. The following attributes are defined for Java agents who are packaged as a Java archive (or simply JAR) files:

Manifest Attribute Description 
Premain-Class   When an agent is specified at JVM launch time this attribute defines the Java agent class: the class containing the premain method. When an agent is specified at JVM launch time this attribute is required. If the attribute is not present the JVM will abort.
Agent-Class   If an implementation supports a mechanism to start Java agents sometime after the JVM has started then this attribute specifies the agent class: the class containing the agentmain method. This attribute is required and the agent will not be started if this attribute is not present. 
Boot-Class-Path   A list of paths to be searched by the bootstrap class loader. Paths represent directories or libraries. 
Can-Redefine-Classes   A value of true or false, case-insensitive and defines if the ability to redefine classes needed by this agent. This attribute is optional, the default is false. 
Can-Retransform-Classes   A value of true or false, case-insensitive and defines if the ability to retransform classes needed by this agent. This attribute is optional, the default is false. 
Can-Set-Native-Method-Prefix   A value of true or false, case-insensitive and defines if the ability to set native method prefix needed by this agent. This attribute is optional, the default is false. 

3. Java Agent and Instrumentation


The instrumentation capabilities of Java agents are truly unlimited. Most noticeable ones include but are not limited to:

◉ Ability to redefine classes at run-time. The redefinition may change method bodies, the constant pool and attributes. The redefinition must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance.

◉ Ability to retransform classes at run-time. The retransformation may change method bodies, the constant pool and attributes. The retransformation must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance.

◉ Ability to modify the failure handling of native method resolution by allowing retry with a prefix applied to the name.

Please notice that retransformed or redefined class bytecode is not checked, verified and installed just after the transformations or redefinitions have been applied. If the resulting bytecode is erroneous or not correct, the exception will be thrown and that may crash JVM completely.

4. Writing Your First Java Agent


In this section we are going to write a simple Java agent by implementing our own class transformer. With that being said, the only disadvantage working with Java agents is that in order to accomplish any more or less useful transformation, direct bytecode manipulation skills are required. And, unfortunately, theJava standard library does not provide any API (at least, documented ones) to make those bytecode manipulations possible.

To fill this gap, creative Java community came up with a several excellent and at this moment very mature libraries, such as Javassist and ASM, just to name a few. Of those two, Javassist is much simpler to use and that is why it became the one we are going to employ as a bytecode manipulation solution. So far this is a first time we were not able to find the appropriate API in Java standard library and had no choice other than to use the one provided by the community.

The example we are going to work on is rather simple but it is taken from real-world use case. Let us say we would like to capture URL of every HTTP connection opened by the Java applications. There are many ways to do that by directly modifying the Java source code but let us assume that the source code is not available due to license policies or whatnot. The typical example of the class which opens the HTTP connection may look like that:

public class SampleClass {
    public static void main( String[] args ) throws IOException {
        fetch("http://www.google.com");
        fetch("http://www.yahoo.com");
    }
 
    private static void fetch(final String address) 
            throws MalformedURLException, IOException {
 
        final URL url = new URL(address);                
        final URLConnection connection = url.openConnection();
         
        try( final BufferedReader in = new BufferedReader(
                new InputStreamReader( connection.getInputStream() ) ) ) {
             
            String inputLine = null;
            final StringBuffer sb = new StringBuffer();
            while ( ( inputLine = in.readLine() ) != null) {
                sb.append(inputLine);
            }       
             
            System.out.println("Content size: " + sb.length());
        }
    }
}

Java agents fit very well into solving such kind of challenges. We just need to define the transformer which will slightly modify sun.net.www.protocol.http.HttpURLConnection constructors by injecting the code to produce output to the console. Sounds scary but with ClassFileTransformer and Javassist it is very simple. Let us take a look on such transformer implementation:

public class SimpleClassTransformer implements ClassFileTransformer {
  @Override
  public byte[] transform( 
      final ClassLoader loader, 
      final String className,
      final Class<?> classBeingRedefined, 
      final ProtectionDomain protectionDomain,
      final byte[] classfileBuffer ) throws IllegalClassFormatException {
         
    if (className.endsWith("sun/net/www/protocol/http/HttpURLConnection")) {
      try {
        final ClassPool classPool = ClassPool.getDefault();
        final CtClass clazz = 
          classPool.get("sun.net.www.protocol.http.HttpURLConnection");
                 
        for (final CtConstructor constructor: clazz.getConstructors()) {
          constructor.insertAfter("System.out.println(this.getURL());");
        }
     
        byte[] byteCode = clazz.toBytecode();
        clazz.detach();
               
        return byteCode;
      } catch (final NotFoundException | CannotCompileException | IOException ex) {
        ex.printStackTrace();
      }
    }
         
    return null;
  }
}

The ClassPool and all CtXxx classes (CtClass, CtConstructor) came from Javassist library. The transformation we have done is quite naïve but it is here for demonstrational purposes. Firstly, because we were interested in HTTP communications only, the sun.net.www.protocol.http.HttpURLConnection is the class from standard Java library being responsible for that.

Please notice that instead of ‘.’ separator, the className has the ‘/’ one. Secondly, we looked for HttpURLConnection class and modified all its constructors by injecting the System.out.println(this.getURL()); statement at the end. And lastly, we returned the new bytecode of the transformed version of the class so it is going to be used by JVM instead of original one.

With that, the role of Java agent premain method would be just to add the instance of SimpleClassTransformer class to the instrumentation context:

public class SimpleAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        final SimpleClassTransformer transformer = new SimpleClassTransformer();
        inst.addTransformer(transformer);
    }
}

That’s it. It looks quite easy and somewhat frightening at the same time. To finish up with Java agent, we have to supply the proper MANIFEST.MF so the JVM will be able to pick the right class.

Manifest-Version: 1.0
Premain-Class: com.oraclejavacertified.advanced.agent.SimpleAgent

With that, out first Java agents is ready for a real battle. In the next section of the tutorial we are going to cover one of the ways to run Java agent along with your Java applications.

5. Running Java Agents


When running from the command line, the Java agent could be passed to JVM instance using -javaagent argument which has following semantic:

-javaagent:<path-to-jar>[=options]

Where <path-to-jar> is the path to locate Java agent JAR archive, and options holds additional options which could be passed to the Java agent, more precisely through agentArgs argument. For example, the command line for running our Java agent from the section Writing Your First Java Agent (using Java 7 version of it) will look like that (assuming that the agent JAR file is located in the current folder):

java -javaagent:advanced-java-part-15-java7.agents-0.0.1-SNAPSHOT.jar

When running the SampleClass class along with the advanced-java-part-15-java7.agents-0.0.1-SNAPSHOT.jar Java agent, the application is going to print on the console all the URLs (Google and Yahoo! ) which were attempted to be accessed using HTTP protocol (followed by the content size of the Google and Yahoo! search home web pages respectively):

http://www.google.com
Content size: 20349
http://www.yahoo.com
Content size: 1387

Running the same SampleClass class without Java agent specified is going to output on the console only content size, no URLs (please notice the content size may vary):

Content size: 20349
Content size: 1387

JVM makes it simple to run Java agents. However, please be warned, any mistakes or inaccurate bytecode generation may crash JVM, possibly losing important data your applications may hold at this moment.

Related Posts

0 comments:

Post a Comment