• Aucun résultat trouvé

Instrumenting Methods

Instrumentingis inserting new bytecode or augmenting the existing bytecode of a class.

Products that produce runtime performance metrics of executing Java applications rely on instrumentation to collect the data. To get some practical experience, let’s develop a frame-work that produces a log of method invocations at runtime. Omniscient Debugger, covered in Chapter 9, “Cracking Code with Unorthodox Debuggers,” uses a similar technique to record the program execution so it can be viewed later. Recording the method invocations at runtime provides the benefit of having a detailed log of the code, executed by the JVM.

To test the implementation, we’ll use a class called SimpleClassdefined in package covertjava.bytecode, with a mainmethod that is shown in Listing 17.7.

LISTING 17.7 SimpleClass’s main()Method public static void main(String[] args) {

int i = 0;

i = i + 1;

System.out.println(i);

}

To keep the example simple, we are not going to write the entire invocation logging frame-work. Instead, we’ll limit the implementation to the InvocationRegistryclass with a static method, as shown in Listing 17.8.

175 Instrumenting and Generating Bytecode

BCEL CLASS DESCRIPTION

LISTING 17.8 Entry Point into the Method Logging Framework public static void methodInvoked(String methodName) {

System.out.println(“*** method invoked “ + methodName);

}

methodInvoked()is the entry point into the method logging framework, and it is used to log a method invocation. For each thread, it can store a call stack of methods, which can be saved or printed at the end of the application run. For now, the implementation just prints the method name to indicate that the framework was called for that method.

With the foundation laid, we can embark on implementing the class that will do the method bytecode instrumentation. We’ll call it MethodInstrumentorand have its main()method take in the name of the class and the methods we want to instrument from the command line.

When executed, MethodInstrumentorwill load the given class, instrument the methods whose names match the given regular expression pattern by adding a call to

InvocationRegistry.methodInvoked(), and then save the class under a new name.

Running the new version of the class should log its method invocations in the Registry.

MethodInstrumentoris located in the covertjava.bytecodepackage, and we are going to use a top-down approach to develop it. The main()method of MethodInstumentoris shown in Listing 17.9.

LISTING 17.9 MethodInstrumentor’s main()Method

public static void main(String[] args) throws IOException { if (args.length != 2) {

System.out.println(“Syntax: MethodInstrumentor “ +

“<full class name> <method name pattern>”);

System.exit(1);

}

JavaClass cls = Repository.lookupClass(args[0]);

MethodInstrumentor instrumentor = new MethodInstrumentor();

instrumentor.instrumentWithInvocationRegistry(cls, args[1]);

cls.dump(“new_” + cls.getClassName() + “.class”);

}

After checking the command-line syntax, the MethodInstrumentorattempts to load the given class using BCEL’s Repositoryclass. The Repositoryuses the application class path to locate and load the class, which is just one of many alternatives to loading a class with BCEL. For some inexplicable reason, BCEL returns nullon error conditions instead of throwing an exception, but for the sake of code clarity we won’t check for it. After the class is loaded, an instance of MethodInstrumentoris created and its instrumentWithInvocationRegistry()

method is called to perform the transformations. When finished, the class is saved to a file with a new name. Let’s look at the implementation of instrumentWithInvocationRegistry shown in Listing 17.10.

LISTING 17.10 instrumentWithInvocationRegistryImplementation public void instrumentWithInvocationRegistry(JavaClass cls,

String methodPattern) { ConstantPoolGen constants = new ConstantPoolGen(cls.getConstantPool());

Method[] methods = cls.getMethods();

for (int i = 0; i < methods.length; i++) {

// Instrument all methods that match the given criteria if (Pattern.matches(methodPattern, methods[i].getName())) {

methods[i] = instrumentMethod(cls, constants, methods[i]);

} }

cls.setMethods(methods);

cls.setConstantPool(constants.getFinalConstantPool());

}

Because we are going to be adding invocation of a method from a different class, we must refer to it by name. Recall that all names are stored in the constants pool, which means we’ll have to add new constants to the existing pool. To add new elements to structures in BCEL, we must rely on the generator classes, which have a suffix Genin their names. The code creates an instance of ConstantPoolGenthat is initially populated with constants from the existing pool; then it iterates all the methods, harnessing the power of regular expressions to test which methods must be instrumented. When all the methods are processed, the class is updated with the new methods and the new pool of constants. The actual job of instrument-ing is done ininstrumentMethod(), as shown in Listing 17.11.

LISTING 17.11 instrumentMethod()Implementation

public Method instrumentMethod(JavaClass cls, ConstantPoolGen constants, Method oldMethod) {

System.out.println(“Instrumenting method “ + oldMethod.getName());

MethodGen method = new MethodGen(oldMethod, cls.getClassName(), constants);

InstructionFactory factory = new InstructionFactory(constants);

InstructionList instructions = new InstructionList();

// Append two instructions representing a method call instructions.append(new PUSH(constants, method.getName()));

Instruction invoke = factory.createInvoke(

177 Instrumenting and Generating Bytecode

“covertjava.bytecode.InvocationRegistry”,

“methodInvoked”, Type.VOID,

new Type[] {new ObjectType(“java.lang.String”)}, Constants.INVOKESTATIC

);

instructions.append(invoke);

method.getInstructionList().insert(instructions);

instructions.dispose();

return method.getMethod();

}

As you can see, instrumentMethod()programmatically creates bytecode instructions that correspond to a method call. The easiest way to select the correct JVM instructions and their parameters is to write the code in Java first, compile it, and then use something like the jClassLib viewer to see how it is translated to the bytecode. Then the corresponding bytecode can be constructed using BCEL objects.

The first thinginstrumentMethod()does is instantiate a MethodGenobject that is used to store the new bytecode. Then a factory to create and a list in which to store the instructions are created. If you have paid attention to this chapter and played with the jClassLib Bytecode Viewer, you might recall that a Java method call is represented by several bytecode instruc-tions. First, the method parameters must be pushed onto the operands stack, and then the invokevirtualinstruction is issued to transfer the control to the method (refer to Listing 17.2 for an example of method call bytecode). This is precisely what we have to insert into the method code before its existing bytecode. If we were working with the bytecode directly, we’d have to insert two constants into the constants pool: covertjava.bytecode.

InvocationRegistryfor the class name and methodInvokedfor the method name. Luckily, BCEL does this for us because we are using the high-level classes such as InstructionFactory and PUSH, which automatically add constants to the pool. After the instructions are created, they are appended to the instruction list. When the code generation part is finished, the list is inserted into the generated method instructions and the method structure is returned.

To test that the instrumentation works, compile the classes and run MethodInstrumentoron SimpleClass.classusing the following command line:

java covertjava.bytecode.MethodInstrumentor covertjava.bytecode.SimpleClass .*

A new class file called new_covertjava.bytecode.SimpleClass.classshould be created in the current directory. Copy this class to the classes directory, overriding the existing

LISTING 17.11 Continued

SimpleClass.classfile; then run the SimpleClass main()method. If all works well, you should see the following on the console:

C:\Projects\CovertJava\classes>java covertjava.bytecode.SimpleClass

*** method invoked main 1

As you can see, the instrumented class starts by calling InvocationRegistry, which outputs the first line; then it executes its own body, which outputs 1.