Dynamics of Java programming, Part 8: replacing reflection with code generation — reprint

Now that you have seen how to use javassist and BCEL framework for classworking (see), I'll show an actual classworking application. This application replaces reflection with classes generated at run time and immediately loaded into the JVM. During the comprehensive discussion, I'll refer to the first two articles in this series and the discussion of javassist and BCEL, so this article becomes a good example of this long series of articles Summary of.

In, I showed that reflection is many times slower than direct code for both field access and method calls. This delay is not a problem for many applications, but there are always performance critical situations. In this case, reflection can become a real bottleneck. However, replacing reflection with statically compiled code can be confusing, And in some cases (for example, in this framework, the classes or projects accessed by reflection are provided at run time, not as part of this compilation process). It is impossible to replace them without rebuilding the whole application. Classworking gives us the opportunity to combine the performance of statically compiled code with the flexibility of reflection. The basic method here is to run Build a custom class in a way that can be used by general code, which will wrap access to the target class (previously achieved through reflection). After loading the custom class into the JVM, it can run at full speed.

Listing 1 shows the starting point of the application. Here, a simple bean class holderbean and an access class reflectaccess are defined. The access class has a command line parameter, which must be the name of a bean class property with the value int (value1 or Value2). It increases the value of the specified property, and then prints out the values of the two properties before exiting.

public int getValue1() { return m_value1; } public void setValue1(int value) { m_value1 = value; } public int getValue2() { return m_value2; } public void setValue2(int value) { m_value2 = value; } } public class ReflectAccess { public void run(String[] args) throws Exception { if (args.length == 1 && args[0].length() > 0) { // create property name

Now that I've shown the reflective version of the code, I'm going to show how to replace reflection with the generated class. For this substitution to work correctly, there is a subtle problem that goes back to the discussion of class loading in this series. The problem is: I want to generate a class that can be accessed from the statically compiled code of the access class at run time, but because the generated class does not exist for the compiler, I can't directly reference it. So how do you link statically compiled code to the generated classes? The basic solution is to define a base class or interface that can be accessed by statically compiled code, and then the generated class extends the base class or implements the interface. In this way, statically compiled code can directly call methods, even if the methods can only be truly implemented at runtime. In Listing 2, I define an interface IAccess to provide this link to the generated code. This interface consists of three methods. The first method simply sets the target object to access. The other two methods are proxies for the get and set methods used to access the value of an int attribute.

The intention here is to let the generated implementation of IAccess interface provide code to call the corresponding get and set methods of the target class. Listing 3 shows an example of implementing this interface. Suppose I want to access the value1 attribute of the holderbean class in:
public void setTarget(Object target) { m_target = (HolderBean)target; } public int getValue() { return m_target.getValue1(); } public void setValue(int value) { m_target.setValue1(value); } } Interfaces are designed to be used for specific properties of specific types of objects. This interface simplifies the implementation code -- which is always an advantage when dealing with bytecode -- but it also means that the implementation class is very specific. For each type of object and property to be accessed through this interface, a separate implementation class is required, which limits this method as a general alternative to reflection. If you choose to use this technology only when reflection performance is really a bottleneck, this limitation is not a problem.

It's easy to generate an implementation class for the IAccess interface with javassist -- just create a new class that implements the interface, add a member variable for the target object reference, and finally add a parameterless constructor and a simple implementation method. Listing 4 shows the javassist code that completes these steps. It constructs a method call that takes the target class and get / set method information as parameters and returns the binary representation of the constructed class:

// build generator for the new class String tname = tclas. getName(); ClassPool pool = ClassPool. getDefault(); CtClass clas = pool. makeClass(cname); clas. addInterface(pool.get("IAccess")); CtClass target = pool. get(tname); // add target object field to class CtField field = new CtField(target,"m_target",clas); clas. addField(field); // add public default constructor method to class CtConstructor cons = new CtConstructor(NO_ARGS,clas); cons. setBody(";"); clas. addConstructor(cons); // add public setTarget method CtMethod meth = new CtMethod(CtClass.voidType,"setTarget",new CtClass[] { pool.get("java.lang.Object") },clas); meth. setBody("m_target = (" + tclas.getName() + ")$1;"); clas. addMethod(meth); // add public getValue method meth = new CtMethod(CtClass.intType,"getValue",NO_ARGS,clas); meth. setBody("return m_target." + gmeth. getName() + "();"); clas. addMethod(meth); // add public setValue method meth = new CtMethod(CtClass.voidType,"setValue",INT_ARGS,clas); meth. setBody("m_target." + smeth. getName() + "($1);"); clas. addMethod(meth); // return binary representation of completed class return clas. toBytecode(); } I'm not going to discuss this code in detail, because if you've been following this series, most of the operations here are familiar (if you haven't seen this series, please read it now for an overview of using javassist).

Generating IAccess implementation classes with BCEL is not as easy as using javassist, but it is not very complex. Listing 5 shows the corresponding code. This code uses the same set of operations as the javassist code in Listing 4, but runs longer because you need to spell out each bytecode instruction for BCEL. As with javassist, I'll skip the details of the implementation (if there's anything unfamiliar, see the overview of BCEL).

// build generators for the new class String tname = tclas. getName(); ClassGen cgen = new ClassGen(cname,"java.lang.Object",cname + ".java",Constants.ACC_PUBLIC,new String[] { "IAccess" }); InstructionFactory ifact = new InstructionFactory(cgen); ConstantPoolGen pgen = cgen. getConstantPool(); //. add target object field to class FieldGen fgen = new FieldGen(Constants.ACC_PRIVATE,new ObjectType(tname),pgen); cgen. addField(fgen.getField()); int findex = pgen. addFieldref(cname,Utility.getSignature(tname)); // create instruction list for default constructor InstructionList ilist = new InstructionList(); ilist. append(InstructionConstants.ALOAD_0); ilist. append(ifact.createInvoke("java.lang.Object","
",Type.VOID,Type.NO_ARGS,Constants.INVOKESPECIAL)); ilist. append(InstructionFactory.createReturn(Type.VOID)); // add public default constructor method to class MethodGen mgen = new MethodGen(Constants.ACC_PUBLIC,null,cname,ilist,pgen); addMethod(mgen,cgen); // create instruction list for setTarget method ilist = new InstructionList(); ilist. append(InstructionConstants.ALOAD_0); ilist. append(InstructionConstants.ALOAD_1); ilist. append(new CHECKCAST(pgen.addClass(tname))); ilist. append(new PUTFIELD(findex)); ilist. append(InstructionConstants.RETURN); // add public setTarget method mgen = new MethodGen(Constants.ACC_PUBLIC,new Type[] { Type.OBJECT },cgen); // create instruction list for getValue method ilist = new InstructionList(); ilist. append(InstructionConstants.ALOAD_0); ilist. append(new GETFIELD(findex)); ilist. append(ifact.createInvoke(tname,gmeth.getName(),Type. INT,Constants. INVOKEVIRTUAL)); ilist. append(InstructionConstants.IRETURN); // add public getValue method mgen = new MethodGen(Constants.ACC_PUBLIC,cgen); // create instruction list for setValue method ilist = new InstructionList(); ilist. append(InstructionConstants.ALOAD_0); ilist. append(new GETFIELD(findex)); ilist. append(InstructionConstants.ILOAD_1); ilist. append(ifact.createInvoke(tname,smeth.getName(),Constants. INVOKEVIRTUAL)); ilist. append(InstructionConstants.RETURN); // add public setValue method mgen = new MethodGen(Constants.ACC_PUBLIC,cgen); // return bytecode of completed class return cgen. getJavaClass(). getBytes(); }

Having introduced javassist and BCEL versions of method constructs, you can now try them out to see how they work. The fundamental reason for generating code at run time is to replace reflection with something faster, so it's best to add performance comparisons to see improvements in this area. To be more interesting, I'll also compare the time taken to construct the glue class with the two frameworks. Listing 6 shows the main part of the test code used to check performance. The runreflection() method runs the reflection part of the test, runaccess() runs the direct access part, and run() controls the whole process (including printing time results). Both runreflection() and runaccess() take the number of times to be executed as parameters, This parameter is passed on the command line (the code used is not shown in listing, but included in the download). The directloader class (at the end of Listing 6) only provides an easy way to load the generated class.

< num; i++)> // messy usage of Integer values required in loop Object result = gmeth. invoke(obj,gargs); value = ((Integer)result). intValue() + 1; sargs[0] = new Integer(value); smeth. invoke(obj,sargs); } } catch (Exception ex) { ex.printStackTrace(System.err); System.exit(1); } return value; } /* Run timed loop using generated class for access to value. / private int runAccess(int num,IAccess access,Object obj) { access.setTarget(obj); int value = 0; for (int i = 0; i < num; i++) { value = access.getValue() + 1; access.setValue(value); } return value; } public void run(String name,int count) throws Exception { // get instance and access methods
As you can see from Figure 1, in any case, the generated code executes much faster than reflection. The speed advantage of the generated code increases with the number of cycles, about 5:1 at 2K cycles and about 24:1 at 512k cycles. For javassist, Constructing and loading the first glue class takes about 320 milliseconds (MS), compared with 370 MS for BCEL, while constructing the second glue class takes only 4 ms for javassist and 2 ms for BCEL (since the clock resolution is only 1ms, these times are very rough). If you combine these times, you will see that even for 2K cycles, generating a class has better overall performance than using reflection (the total execution time is about 4 ms to 6 ms, while the reflection time is about 14 MS). In addition, the actual situation is more conducive to the generated code than shown in this figure. When the cycle is reduced to 25 cycles, the execution of the reflection code still takes 6 ms to 7 ms, and the generated code runs too fast to be recorded. For relatively few cycles, the reflection takes more time The time reflects that when a threshold is reached, some optimization is carried out in the JVM. If I reduce the number of cycles to less than 20, the reflection code will be too fast to record.

You have now seen what performance runtime classworking can bring to your application. The next time you face a difficult performance optimization problem, remember it -- it may be the key to avoiding big redesign. However, classworking not only has performance benefits, it is also a flexible way to adapt applications to runtime requirements. Even if there is no reason to use it in code, I think it is a Java feature that makes programming interesting. The discussion of a classworking real-world application ends the series of "the dynamics of Java programming". But don't be disappointed -- when I show you some tools built to manipulate Java bytecode, you'll soon have a chance to learn about some other classworking applications in developerWorks. The first will be an article on two testing tools directly launched by mother goose. Original text: http://www.ibm.com/developerworks/cn/java/j-dyn0610/index.html

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>