Having talked about the Java class format and run-time access with reflection, this series is the time to go into more advanced topics. This month I'm going to start the second part of this series, where Java class information is just another form of data structure manipulated by the application. I refer to the whole content of this topic as classworking.
I'll start with the Javassist bytecode manipulation Library as a discussion of classworking. Javassist is not only a library that handles bytecode, but also because it has another feature that makes it a good starting point for experimenting with classworking. This is the ability to change the bytecode of a Java class with Javassist without really knowing anything about bytecode or the structure of a Java virtual machine JVM. There are pros and cons to this feature in some ways-I don't generally advocate casual use of the technology I don't know-but it does make bytecode manipulation more feasible than a framework that works on a single instruction level.
Javassist Foundation
Javassist enables you to examine, edit, and create Java binary classes. The inspection aspect is basically the same as using the Reflection API directly in Java, but another way to access this information is useful when you want to modify the classes rather than just execute them. This is because the JVM is not designed to provide any method to access the raw class data after the class is loaded into the JVM, and this work needs to be done outside the JVM.
Javassist uses javassist.ClassPool
classes to track and control the classes that are manipulated. This class works very much like the JVM class loader, but one important difference is that it is not a loaded, executed class as part of the application, and the class pool enables the loaded class to be used as data through the Javassist API. You can use the default class pool, which is loaded from the JVM search path, or you can define a class pool that searches your own list of paths. You can even load a binary class directly from a byte array or stream, and create a new class from scratch.
Classes that are loaded into a class pool are javassist.CtClass
represented by instances. As with standard Java java.lang.Class
classes, CtClass
methods for examining class data, such as fields and methods, are provided. However, this is only CtClass
part of the content, which also defines methods for adding new fields, methods, and constructors to the class, as well as changing classes, parent classes, and interfaces. Strangely, Javassist does not provide any method to delete a field, method, or constructor in a class.
Fields, methods, and constructors javassist.CtField、
javassist.CtMethod
javassist.CtConstructor
are represented by an instance of and respectively. These classes define methods that modify all methods of the objects represented by them, including the actual bytecode content in the method or constructor.
Source code for all bytecode
Javassist allows you to completely replace the bytecode body of a method or constructor, or optionally add bytecode at the beginning or end of an existing body (and add some other variables to the constructor). In either case, the new bytecode is passed as a source code declaration or String
a block in the Java class. The Javassist method compiles the source code you provide efficiently into Java bytecode, and then inserts them into the body of the target method or constructor.
The source code accepted by Javassist is not exactly the same as the Java language, but the main difference is the addition of special identifiers that represent methods or constructor parameters, method return values, and other content that may be used in the inserted code. These special identifiers begin with symbols $
, so they do not interfere with other content in the code.
There are some limitations to what you can do in the source code that is passed to Javassist. The first constraint is the format used, which must be a single statement or block. In most cases this is not a limitation, because any sequence of statements that you need can be placed in a block. The following is an example of using a special Javassist identifier to represent the first two parameters of a method, which is used to show how to use it:
{ System.out.println ("Argument 1:" + $); System.out.println ("Argument 2:" + $);}
A more substantial limitation on source code is that you cannot reference local variables declared outside of the added declaration or block. This means that if you add code at the beginning and end of the method, it is generally not possible to pass information from the code you added at the beginning to the code added at the end. It is possible to circumvent this limitation, but bypassing is very complex-usually you need to try to merge the code that was inserted separately into a block.
Back to top of page
Classworking with a Javassist
As an example of using Javassist, I will use a task that is normally handled directly in the source code: Measuring the time it takes to execute a method. This can be done easily in the source code, as long as the current time is recorded at the beginning of the method, then the current time is checked again at the end of the method and the difference of two values is calculated. Without source code, it is much more difficult to get this timing information. This is a convenient place for classworking-it allows you to make this change to any method, without the need for source code.
Listing 1 shows a (bad) example method that I used as an experimental item in my timing experiment: StringBuilder
The method of the class buildString
. This method uses a master of all Java performance optimizations to make a method that you don't want to use to construct an arbitrary length String
-it produces a longer string by appending a single character to the end of the string repeatedly. Because strings are immutable, this approach means that each new string is constructed with a loop: Using the data copied from the old string and adding new characters at the end. The end result is that it is more expensive to use this method to produce a longer string.
Listing 1. A way to clock
public class stringbuilder{ private String buildstring (int length) { string result = ""; for (int i = 0; i < length; i++) { result + = (char) (i%26 + ' a '); } return result; } public static void Main (string[] argv) { StringBuilder inst = new StringBuilder (); for (int i = 0; i < argv.length; i++) { String result = inst.buildstring (Integer.parseint (Argv[i])); System.out.println ("Constructed string of length" + result.length ());}}}
Add Method Timings
Because there is the source code for this method, I will show you how to add timing information directly. It is also used as a model when using Javassist. Listing 2 shows only the buildString()
method in which the timing function is added. There's not much change here. The added code simply saves the start time as a local variable, and then calculates the duration at the end of the method and prints it to the console.
Listing 2. A method with timing
Private String buildstring (int length) { Long start = System.currenttimemillis (); String result = ""; for (int i = 0; i < length; i++) { result + = (char) (i%26 + ' a '); } System.out.println ("Call to Buildstring took" + (System.currenttimemillis ()-start) + "Ms."); return result; }
Do it with Javassist.
It should not be difficult to use the Javassist operation class bytecode to get the same effect. Javassist provides a way to add code at the beginning and end of a method, and don't forget, I'm doing this by adding timing information to the method.
However, there are still obstacles. When describing how Javassist lets you add code, I mention that the added code cannot refer to local variables defined elsewhere in the method. This restriction makes it impossible for me to implement timing code in Javassist using the same method used in the source code, in which case I define a new local variable in the code that I added at the beginning and reference the variable in the code added at the end.
So are there other ways to get the same effect? Yes, I can add a new member field to the class and use this field instead of the local variable. However, this is a bad solution, with some limitations in general usage. For example, consider what happens in a recursive method. Each time the method calls itself, the last saved start time value is overwritten and is lost.
Fortunately, there is a more concise solution. I can keep the code of the original method unchanged, change the method name only, and then add a new method with the original method name. This Interceptor (Interceptor) method can use the same signature as the original method, including returning the same value. Listing 3 shows what the source code looks like when it's adapted in this way:
Listing 3. Add an interceptor method to the source code
private string Buildstring$impl (int length) { string result = ""; for (int i = 0; i < length; i++) { result + = (char) (i%26 + ' a '); } return result; } Private String buildstring (int length) { Long start = System.currenttimemillis (); String result = Buildstring$impl (length); System.out.println ("Call to Buildstring took" + (System.currenttimemillis ()-start) + "Ms."); return result; }
This method of using the Interceptor method can be well exploited by Javassist. Because the entire method is a block, I can define and use local variables in the body without problem. It is also easy to generate source code for the Interceptor method-only a few substitutions are required for any possible method.
Run interception
The code that implements timing for adding methods uses some of the Javassist APIs described in the Javassist Foundation. Listing 4 shows the code, which is an application with two command-line arguments, giving the class name and the method name to be timed, respectively. main()
the body of the method only gives the class information and then passes it to the addTiming()
method to handle the actual modification. addTiming()
method first by appending the name to the $impl”
existing method, and then creating a copy of the method with the original method name. It then replaces the text of the copy method with a timing code that contains a call to the original method that was renamed.
Listing 4. Add Interceptor method with Javassist
public class Jassisttiming {public static void main (string[] argv) {if (argv.length = = 2) {try { Start by getting the class file and method Ctclass Clas = Classpool.getdef Ault (). Get (Argv[0]); if (clas = = null) {System.err.println ("Class" + argv[0] + "not Found"); } else {//Add timing interceptor to the class addtiming (Clas, ARGV[1]); Clas.writefile (); SYSTEM.OUT.PRINTLN ("Added timing to Method" + argv[0] + "." + argv[1]); }} catch (Cannotcompileexception ex) {ex.printstacktrace (); } catch (Notfoundexception ex) {ex.printstacktrace (); } catch (IOException ex) {ex.printstacktrace (); }} else {System.out.println ("usage:jassisttiming class Method-name"); }} private static void Addtiming (Ctclass clas, String mname) throws Notfoundexception, Cannotcompileexce ption {//Get the method information (throws exception if method with//given name was not Declar Ed directly by this class, returns//arbitrary choice if + than one with the given name) Ctmethod Mold = Clas.getdeclaredmethod (Mname); Rename old method to synthetic name, then duplicate the//method with original name for use as Interceptor String nname = mname+ "$impl"; Mold.setname (Nname); Ctmethod mnew = ctnewmethod.copy (Mold, mname, clas, NULL); Start the body text generation by saving the "Start Time"/"to a" local variable, then call the Timed method; The//actual code generated needs to depend on whether the//timed method returns a value String type = Mold.getreturntype (). GetName (); StringBuffer BODY = new StringBuffer (); Body.append ("{\nlong start = System.currenttimemillis (); \ n"); if (! " void ". Equals (Type)" {body.append (type + "result ="); } body.append (Nname + "($$); \ n"); Finish body text generation with call to print the timing//information, and return saved value (if not void) Body.append ("System.out.println" (\ "call to Method" + Mname + "took \" +\n (System.currenttimemillis ()- Start) + "+" \ "ms.\"); \ n "); if (! " void ". Equals (Type)" {body.append ("return result;\n"); } body.append ("}"); Replace the body of the Interceptor method with generated//code block and add it to class Mnew.setbod Y (body.tostring ()); Clas.addmethod (mnew); Print the generated code block just to show what is done System.out.println ("IntErceptor method Body: "); System.out.println (Body.tostring ()); }}
Constructs the body of the Interceptor method using one java.lang.StringBuffer
to accumulate body text (this shows the String
correct method of constructing the process, as opposed to the StringBuilder
method used in the construct). This change depends on whether the original method has a return value. If it has a return value, the constructed code stores the value in a local variable so that it can be returned at the end of the Interceptor method. If the original method type is void
, then there is nothing to save and no content to return in the Interceptor method.
In addition to the invocation of the original method (renamed), the actual body content looks like standard Java code. This is the line in the code body.append(nname + "($$);\n")
, which nname
is the name of the original method after it was modified. The identifier used in the call $$
is the way Javassist represents a series of parameters for the method being constructed. By using this identifier in a call to the original method, the arguments supplied when the Interceptor method is called can be passed to the original method.
Listing 5 shows the results of running the unmodified program first StringBuilder
, then running the JassistTiming
program to add the timing information, and finally running StringBuilder
the modified program. You can see that the modified StringBuilder
runtime reports the time of execution, and you can see that the time increase caused by the inefficient string construction code is much faster than the increase in the length of the constructed string.
Listing 5. Run this program
[dennis]$ Java StringBuilder 4000 8000 16000Constructed string of length 1000Constructed string of length 2000Co nstructed string of length 4000Constructed string of length 8000Constructed string of length 16000[dennis]$ java-cp Javas Sist.jar:. Jassisttiming StringBuilder Buildstringinterceptor method Body:{long start = System.currenttimemillis (); java.lang.String result = Buildstring$impl ($$); System.out.println ("Call to Method Buildstring took" + (System.currenttimemillis ()-start) + "Ms."); return result; Added timing to Method stringbuilder.buildstring[dennis]$ java StringBuilder, 4000 8000 16000Call to method build String took the Notoginseng Ms. Constructed string of length 1000Call to method Buildstring took Ms. Constructed string of length 2000Call to method Buildstring took 181 Ms. Constructed string of length 4000Call to method Buildstring took 863 Ms. Constructed string of length 8000Call to method Buildstring took 4154 Ms. Constructed string of length 16000
Back to top of page
Can I trust the source code?
Javassist makes classworking easy by letting you manipulate the source code rather than the actual bytecode manifest. But this convenience also has a disadvantage. As I mentioned in the source code of all bytecode, the source code used by Javassist is not exactly the same as the Java language. In addition to identifying special identifiers in code, Javassist implements a more relaxed compile-time code check than the Java language specification requires. Therefore, if you are not careful, you generate bytecode from the source code that can produce surprising results.
As an example, listing 6 shows what happens when the type of the local variable used by the interceptor code at the beginning of the method is long
changed int
. Javassist will accept the source code and convert it to a valid bytecode, but the resulting time is meaningless. If you try to compile the assignment directly in a Java program, you get a compilation error because it violates a rule in the Java language: A narrowing assignment requires a type overlay.
Listing 6. Will A
long
stored in a
int
In
[dennis]$ java-cp Javassist.jar:. Jassisttiming StringBuilder Buildstringinterceptor method Body:{int start = System.currenttimemillis (); java.lang.String result = Buildstring$impl ($$); System.out.println ("Call to Method Buildstring took" + (System.currenttimemillis ()-start) + "Ms."); return result; Added timing to Method stringbuilder.buildstring[dennis]$ java StringBuilder, 4000 8000 16000Call to method build String took 1060856922184 Ms. Constructed string of length 1000Call to method Buildstring took 1060856922172 Ms. Constructed string of length 2000Call to method Buildstring took 1060856922382 Ms. Constructed string of length 4000Call to method Buildstring took 1060856922809 Ms. Constructed string of length 8000Call to method Buildstring took 1060856926253 Ms. Constructed string of length 16000
Depending on the content in the source code, you can even have Javassist generate invalid bytecode. Listing 7 shows an example where I JassistTiming
modify the code to always think of the timing method to return a int
value. Javassist also accepts the source code with no problem, but it cannot be verified when I try to execute the generated bytecode.
Listing 7. Will A
String
stored in a
int
In
[dennis]$ java-cp Javassist.jar:. Jassisttiming StringBuilder Buildstringinterceptor method Body:{long start = System.currenttimemillis (); int result = Buildstring$impl ($$); System.out.println ("Call to Method Buildstring took" + (System.currenttimemillis ()-start) + "Ms."); return result; Added timing to Method stringbuilder.buildstring[dennis]$ java StringBuilder, 4000 8000 16000Exception in thread "Main" Java.lang.VerifyError: (Class:stringbuilder, method:buildstring signature: (I) ljava/lang/string;) Expecting to find integer on stack
Just be careful with the source code provided to Javassist, which is not a problem. However, it is important to realize that Javassist does not capture all errors in the code, so there may be unexpected results that are not foreseen.
Back to top of page
Subsequent content
Javassist is much richer than what we discussed in this article. For the next one months, we'll take a closer look at the special features that Javassist provides for batch modification of classes and for dynamic modification of classes when loading classes at run time. These features make Javassist a great tool for implementation in your application, so be sure to continue to follow us through the full content of this powerful tool.
Original: http://www.ibm.com/developerworks/cn/java/j-dyn0916/index.html
The dynamics of Java programming, Part 4: Class conversion with Javassist-reproduced