The dynamics of Java programming, part 7th: Designing bytecode with BCEL-reproduced

Source: Internet
Author: User
Tags wrappers

In the last three articles of this series, I showed how to manipulate classes with the Javassist framework. This time I'm going to manipulate bytecode in a very different way-using Apache byte code Engineering Library (BCEL). Unlike the source code interfaces supported by Javassist, BCEL operates at the actual JVM instruction level. The underlying approach makes BCEL useful when you want to control every step of a program's execution, but it also makes BCEL more complex to use than Javassist when both are competent.

I'll discuss BCEL basic architecture first, and then much of this article will discuss the example of rebuilding my first Javassist class operation with BCEL. Finally, a brief introduction to some of the tools provided in the BCEL package and some applications built by developers with BCEL.

BCEL class Access

BCEL enables you to have all the basic capabilities that Javassist provides for analyzing, editing, and creating Java binary classes. A notable difference in BCEL is that each content is designed to work at the level of the JVM assembly language, not the source code interface provided by the Javassist. In addition to the superficial differences, there are some deeper differences, including the use of two different hierarchies of components in BCEL-one for checking existing code and the other for creating new code. I assume that the reader has been familiar with Javassist through the previous articles in this series (see Sidebar do not miss the rest of this series). So I'll focus on the differences that might confuse you when you start using BCEL.

Like Javassist, BCEL's functionality in class analysis is essentially repetitive with the functionality that the Java platform provides directly through the Relfection API. This repetition is necessary for the class operations Toolbox because it is generally not desirable to load the classes you want to manipulate before they are modified.

BCEL org.apache.bcel provides some basic constant definitions in the package, but in addition to these definitions, all the parsing-related code is in the org.apache.bcel.classfile package. The starting point in this package is the JavaClass class. This class acts as a function of using BCEL to access class information when used with regular Java reflection java.lang.Class . JavaClassdefines the fields and method information for getting this class, and the methods for structuring information about the parent class and interface. java.lang.Class 不同, JavaClass It also provides access to the internal information of the class, including constant pools and attributes, as well as the full binary class representation of the byte stream.

JavaClassInstances are typically created by parsing the actual binary class. The BCEL provides org.apache.bcel.Repository classes for handling parsing. By default, BCEL parses and buffers the class representation found in the JVM classpath, org.apache.bcel.util.Repository getting the actual binary class representation from the instance (note the difference in Package name). org.apache.bcel.util.Repositoryis actually the interface of the source code represented by the binary class. Where the classpath is used in the default source code, you can replace it with other paths to the query class file or other methods that access the class information.

Change class

In addition to accessing the reflection form of a class component, a org.apache.bcel.classfile.JavaClass method for changing the class is provided. You can use these methods to set any component to a new value. However, they are not generally used directly because the other classes in the package do not support building a new version of the component in any reasonable way. Instead, org.apache.bcel.generic there is a completely separate set of classes in the package that provides an editable version of the org.apache.bcel.classfile same component as the class represents.

is 像 org.apache.bcel.classfile.JavaClass to use BCEL to analyze the starting point of an existing class, which org.apache.bcel.generic.ClassGen is the starting point for creating a new class. It is also used to modify existing classes-in order to handle this situation, there is a JavaClass constructor that takes an instance as a parameter and initializes the ClassGen class information with it. Once the class has been modified, it can be represented by invoking a returned JavaClass method from the instance to the ClassGen available class, and it can be converted to a binary class representation.

Sounds a little messy? I think so. In fact, it is one of the main drawbacks of using BCEL to go back and forth between two packages. Repeating class structures are always a bit of a handicap, so if you use BCEL frequently, you might want to write a wrapper class that hides some of the differences. In this article, I'll use the org.apache.bcel.generic package class primarily, and avoid using wrappers. But keep this in mind as you develop yourself.

In addition ClassGen , the org.apache.bcel.generic package defines a class that manages the structure of a non-homogeneous component. These structure classes include the processing of constant pools ConstantPoolGen , the FieldGen and and processing of a MethodGen series of JVM directives for fields and methods InstructionList . Finally, the org.apache.bcel.generic package also defines a class that represents each type of JVM directive. You can create instances of these classes directly, or use helper classes in some cases org.apache.bcel.generic.InstructionFactory . InstructionFactorythe advantage of using it is that it handles the bookkeeping details of many instruction constructs (including adding items to the constant pool as required by the Directive). In the following section you will see how to make all these classes work together.

Back to top of page

Using BCEL for class operations

As an example of using BCEl, I will use one of the Javassist examples in section 4-Measuring the time to execute a method. I even used the same approach as when using Javassist: Create a copy of the original method to be timed with a changed name, and then replace the body of the original method with the code that wraps the time calculation by invoking the renamed method.

Select a test item

Listing 1 shows a method for demonstrating the purpose of the example method: StringBuilder class buildString . As I said in Part 4, this approach uses a way that all Java performance experts warn you not to use to build a String --it repeats appending a single character to a string to create a longer string. Because the string is immutable, this means that a new string is constructed each time the loop is copied, copying the data from the old string and adding a single character at the end. The overall effect is that when you create a longer string with this method, it will incur increasing overhead.

Listing 1. The method to be timed

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 ());}}}    

Listing 2 shows the source code equivalent to changing the class operation with BCEL. Here the wrapper method simply saves the current time, then calls the original method after renaming and prints the time report before returning the result of calling the original method.

Listing 2. Adding timings to the original method

public class stringbuilder{    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$impl took" +            (System.currenttimemillis ()-start) + "Ms.");        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 ());}}}    
Writing Conversion Code

The code that adds the method timing is implemented with the BCEL API that I described in the BCEL Class Access section. The operation at the JVM instruction level makes the code much longer than the Javassist example in part 4th, so I'm going to introduce it in a paragraph before providing a complete implementation. In the final code, all fragments form a method that has two parameters: cgen --It is org.apache.bcel.generic.ClassGen an instance of the class, initialized with the existing information of the class being modified, and the method-an instance of the method to be timed org.apache.bcel.classfile.Method .

Listing 3 is the first piece of code for the conversion method. As you can see from the note, the first part initializes only the basic BCEL component to be used, including initializing a new instance with information about the method to be timed org.apache.bcel.generic.MethodGen . I MethodGen set an empty manifest for this, and I'll populate it with the actual timing code later. In the 2nd part, I created the second instance using the original method org.apache.bcel.generic.MethodGen , and then removed the original method from the class. In the second MethodGen instance, I simply add the name to the "$impl" suffix and call it getMethod() to convert the modifiable method information into a fixed-form org.apache.bcel.classfile.Method instance. It is then called addMethod() to add the renamed method to the class.

Listing 3. Adding interception methods

Set up the construction toolsinstructionfactory ifact = new Instructionfactory (Cgen); instructionlist ilist = new Instru Ctionlist (); Constantpoolgen Pgen = Cgen.getconstantpool (); String cname = Cgen.getclassname (); Methodgen Wrapgen = new Methodgen (method, CNAME, Pgen); Wrapgen.setinstructionlist (IList);    Rename a copy of the original Methodmethodgen Methgen = new Methodgen (method, CNAME, Pgen); Cgen.removemethod (method); String iname = methgen.getname () + "$impl"; Methgen.setname (Iname); Cgen.addmethod (Methgen.getmethod ());

Listing 4 shows the next piece of code for the conversion method. The first part here calculates the space occupied by the method invocation parameter on the stack. This code is needed because in order to store the start time on the stack frame before the wrapper method is called, I need to know what offset value the local variable can use (note that I can get the same effect with BCEL's local variable processing, but in this article I choose to use an explicit method). The second part of this code generates a java.lang.System.currentTimeMillis() call to get the start time and save it to the calculated local variable offset at the stack frame.

You might wonder why you should check if the method is static at the start of the parameter size calculation, and if it is static, initialize the stack frame slot to 0 (not static exactly the opposite). This approach is related to how Java handles method calls. For non-static methods, the first (hidden) parameter of each invocation is a reference to the target object this , which I want to take into account when calculating the full parameter set size in the stack frame.

Listing 4. To set the call for a wrapper

Compute the size of the calling parameterstype[] types = Methgen.getargumenttypes (); int slot = Methgen.isstatic ()? 0:1;for (int i = 0; i < types.length; i++) {    slot + types[i].getsize ();}    Save time prior to Invocationilist.append (Ifact.createinvoke ("Java.lang.System",    "Currenttimemillis", Type.long, Type.no_args,     constants.invokestatic)); Ilist.append (Instructionfactory.createstore, slots));

Listing 5 shows the code that generates a call to the wrapper method and saves the result, if any. The first part of this code checks again if the method is static. If the method is not static, generate the this code that mounts the object reference to the stack, while setting the method invocation type to virtual (not static ). It then loops through the for code that copies all the call parameter values to the stack, createInvoke() generating an actual call to the wrapped method, and the final if statement saves the resulting value to another local variable in the stack frame if the result type is not void .

Listing 5. Methods for calling Wrappers

Call the wrapped methodint offset = 0;short invoke = Constants.invokestatic;if (!methgen.isstatic ()) {    ilist.append (Instructionfactory.createload (type.object, 0));    offset = 1;    Invoke = Constants.invokevirtual;} for (int i = 0; i < types.length; i++) {    Type type = Types[i];    Ilist.append (instructionfactory.createload (type, offset));    Offset + = Type.getsize ();} Type result = Methgen.getreturntype (); Ilist.append (Ifact.createinvoke (CNAME,     iname, result, types, invoke));    Store result for return Laterif (Result! = type.void) {   ilist.append (Instructionfactory.createstore (result, slot+ 2));}

Start packing now. Listing 6 generates the number of milliseconds after the actual calculation start time and prints out the code as a well-formatted message. This section looks complicated, but most operations actually write out the parts of the output message. It does show several types of operations that I didn't use in the previous code, including field access (to java.lang.System.out ) and several different instruction types. If you think of the JVM as a stack-based processor, most of it is easy to understand, so I'm not going to elaborate here.

Listing 6. Calculate and print the time that is used

Print time required for method Callilist.append (ifact.createfieldaccess ("Java.lang.System", "Out", New ObjectType (" Java.io.PrintStream "), constants.getstatic); Ilist.append (instructionconstants.dup); Ilist.append ( Instructionconstants.dup); String Text = "Call to Method" + methgen.getname () + "took"; Ilist.append (new PUSH (Pgen, text)); Ilist.append (ifact.creat Einvoke ("Java.io.PrintStream", "print", type.void, new type[] {type.string}, constants.invokevirtual)); Ilist.append ( Ifact.createinvoke ("Java.lang.System", "Currenttimemillis", Type.long, Type.no_args, constants.invokestatic)); ILIs T.append (Instructionfactory.createload (Type.long, slot)); Ilist.append (instructionconstants.lsub); Ilist.append ( Ifact.createinvoke ("Java.io.PrintStream", "print", type.void, new type[] {Type.long}, constants.invokevirtual)); ILIs T.append (New PUSH (Pgen, "Ms.")), Ilist.append (Ifact.createinvoke ("Java.io.PrintStream", "println", type.void, New TYP E[] {type.string}, Constants.invokeVIRTUAL)); 

After the timing message code is generated, it is left to listing 7 for the call result value (if any) that holds the wrapped method, and then ends the wrapper method of the build. The last part involves several steps. The call stripAttributes(true) simply tells BCEL not to generate debug information for the built method, setMaxStack() and to setMaxLocals() call the calculate and set stack usage information for the method. Once you have completed this step, you can actually generate the final version of the method and add it to the class.

Listing 7. Complete the wrapper

Return result from wrapped method callif (Result! = type.void) {    ilist.append (instructionfactory.createload ( result, slot+2));} Ilist.append (Instructionfactory.createreturn (Result));    Finalize the constructed methodwrapgen.stripattributes (true); Wrapgen.setmaxstack (); Wrapgen.setmaxlocals (); Cgen.addmethod (Wrapgen.getmethod ()); Ilist.dispose ();
The complete code

Listing 8 shows the complete code (slightly changing the format to fit the display width), including the method that takes the name of the class file main() and the method to be converted:

Listing 8. Full conversion Code

public class bceltiming{private static void Addwrapper (Classgen Cgen, method) {//Set up the CO        Nstruction Tools Instructionfactory ifact = new Instructionfactory (Cgen);        Instructionlist ilist = new Instructionlist ();        Constantpoolgen Pgen = Cgen.getconstantpool ();        String cname = Cgen.getclassname ();        Methodgen Wrapgen = new Methodgen (method, CNAME, Pgen);                Wrapgen.setinstructionlist (IList);        Rename a copy of the original method Methodgen Methgen = new Methodgen (method, CNAME, Pgen);        Cgen.removemethod (method);        String iname = methgen.getname () + "$impl";        Methgen.setname (Iname);        Cgen.addmethod (Methgen.getmethod ());                Type result = Methgen.getreturntype ();        Compute the size of the calling parameters type[] types = Methgen.getargumenttypes (); int slot = Methgen.isstatic ()?        0:1;          for (int i = 0; i < types.length; i++) {  Slot + = Types[i].getsize (); }//Save time prior to invocation ilist.append (Ifact.createinvoke ("Java.lang.System", "C        Urrenttimemillis ", Type.long, Type.no_args, constants.invokestatic));            Ilist.append (instructionfactory.                CreateStore (Type.long, slot));        Call the wrapped method int offset = 0;        Short invoke = Constants.invokestatic;                if (!methgen.isstatic ()) {Ilist.append (instructionfactory.            Createload (type.object, 0));            offset = 1;        Invoke = Constants.invokevirtual;            } for (int i = 0; i < types.length; i++) {Type type = Types[i];                Ilist.append (instructionfactory.            Createload (type, offset));        Offset + = Type.getsize ();                } ilist.append (Ifact.createinvoke (CNAME, iname, result, types, invoke)); Store result for return later if (reSult! = type.void) {ilist.append (instructionfactory.        CreateStore (result, slot+2));            }//Print time required for method call Ilist.append (Ifact.createfieldaccess ("Java.lang.System",        "Out", New ObjectType ("Java.io.PrintStream"), constants.getstatic));        Ilist.append (Instructionconstants.dup);        Ilist.append (Instructionconstants.dup);        String Text = "Call to Method" + methgen.getname () + "took";        Ilist.append (New PUSH (Pgen, text));            Ilist.append (Ifact.createinvoke ("Java.io.PrintStream", "print", type.void, new type[] {type.string},        constants.invokevirtual));             Ilist.append (Ifact.createinvoke ("Java.lang.System", "Currenttimemillis", Type.long, Type.no_args,        constants.invokestatic));            Ilist.append (instructionfactory.        Createload (Type.long, slot));        Ilist.append (instructionconstants.lsub); IlIst.append (Ifact.createinvoke ("Java.io.PrintStream", "print", type.void, new type[] {Type.long}, C Onstants.        invokevirtual));        Ilist.append (New PUSH (Pgen, "Ms."));            Ilist.append (Ifact.createinvoke ("Java.io.PrintStream", "println", Type.void, new type[] {type.string},                    constants.invokevirtual));                Return result from wrapped method, call if (result! = type.void) {ilist.append (instructionfactory.        Createload (result, slot+2));                } ilist.append (Instructionfactory.createreturn (result));        Finalize the constructed method Wrapgen.stripattributes (true);        Wrapgen.setmaxstack ();        Wrapgen.setmaxlocals ();        Cgen.addmethod (Wrapgen.getmethod ());    Ilist.dispose ();            } public static void Main (string[] argv) {if (argv.length = = 2 && argv[0].endswith (". Class")) { try {Javaclass Jclas = new Classparser (Argv[0]). Parse ();                Classgen Cgen = new Classgen (Jclas);                Method[] methods = Jclas.getmethods ();                int index;                        for (index = 0; index < methods.length; index++) {if (Methods[index].getname (). Equals (Argv[1])) {                    Break                    }} if (Index < methods.length) {Addwrapper (Cgen, Methods[index]);                    FileOutputStream fos = new FileOutputStream (argv[0]);                    Cgen.getjavaclass (). dump (FOS);                Fos.close ();                } else {System.err.println ("Method" + argv[1] + "not found in" + argv[0]);            }} catch (IOException ex) {ex.printstacktrace (system.err); }} else {System.out.println ("Usage:bceltiming Class-file Method-name "); }    }}
Give it a try.

Listing 9 shows the results of running the program for the first time in an unmodified form StringBuilder , then running the BCELTiming program to join the timing information and finally running the modified StringBuilder program. You can see StringBuilder how the report execution time is started after the modification, and why the time is increasing faster than the string length of the build, due to the inefficiency of the string build code.

Listing 9. 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 bcel. Jar:. bceltiming Stringbuilder.class buildstring[dennis]$ java StringBuilder 4000 8000 16000Call to method buildstring $impl took Ms. Constructed string of length 1000Call to method Buildstring$impl took Ms. Constructed string of length 2000Call to method Buildstring$impl took Ms. Constructed string of length 4000Call to method Buildstring$impl took 879 Ms. Constructed string of length 8000Call to method Buildstring$impl took 3875 Ms. Constructed string of length 16000

Back to top of page

Packaging BCEL

BCEL has more features than the basic class operations I've described in this article. It also includes a complete validator implementation to ensure that the binary class is valid for the JVM specification (see org.apache.bcel.verifier.VerifierFactory ), a disassembler that generates a well-framed and linked JVM-level binary Class View, or even a BCEL program generator that outputs the source code to let the BCEL program compile the provided class. (The org.apache.bcel.util.BCELifier class is not included in the Javadocs, so its usage depends on the source code.) This is an attractive feature, but the output may be too cryptic for most developers.

When I use BCEL myself, I find HTML disassembler particularly useful. To try it, simply execute the class in the BCEL JAR org.apache.bcel.util.Class2HTML , using the path of the class file you want to disassemble as a command-line argument. It generates an HTML file in the current directory. For example, below I will disassemble the classes used in the timing example StringBuilder :

[dennis]$ java-cp bcel.jar org.apache.bcel.util.Class2HTML stringbuilder.classprocessing stringbuilder.class ... Done.

Figure 1 is a screenshot of the sub-frame output generated by the disassembly program. In this snapshot, the large frame in the upper-right corner shows the StringBuilder decomposition of the timing wrapper method added to the class. There is full HTML output in the download file--if you want to actually watch it, just open the stringbuilder.html file in the browser window.

Figure 1. Disassembly StringBuilder

Currently, BCEL is probably the most used framework for Java class operations. Other projects that use BCEL are listed on the Web site, including the Xalan XSLT compiler, the AspectJ extension of the Java programming language, and several JDO implementations. Many other unlisted items also use BCEL, including my own JiBX XML data binding project. However, several projects listed by BCEL have shifted to other libraries, so don't use this list as an absolute basis for BCEL popularity.

The biggest benefit of BCEL is its commercial-friendly Apache license and its rich JVM instruction-level support. These features combine their stability and longevity, making it a very popular choice for class-operating applications. However, BCEL does not seem to be designed to be very good speed or easy to use. In most cases, Javassist provides a more friendly API and has similar speeds (even faster), at least in my simple tests. If your project can use the Mozilla public License (MPL) or the GNU Lesser general public License (LGPL), then Javassist may be a better option (it can be used under both licenses).

Back to top of page

Next article

I've introduced Javassist and BCEL, and the next article in this series will delve deeper into the application of classes that are more useful than what we've already covered. In the 2nd part, I showed that the method call reflection is much slower than the direct call. In part 8th, I'll show you how to use Javassist and BCEL to replace reflection calls with dynamically generated code at run time, which can greatly improve performance. Come back next month to see the dynamics of another Java programming to learn more.

Original: http://www.ibm.com/developerworks/cn/java/j-dyn0414/index.html

The dynamics of Java programming, part 7th: Designing bytecode with BCEL-reproduced

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.