 | Level: Intermediate Dennis Sosnoski (dms@sosnoski.com), President, Sosnoski Software Solutions, Inc.
10 Jun 2004 Earlier in this article series, you learned how reflection performance is many times slower than direct access, and then learned about classworking with Javassist and the Apache Byte Code Engineering Library (BCEL). Java consultant Dennis Sosnoski wraps up his Java programming dynamics series by demonstrating how you can use runtime classworking to replace reflection code with generated code that runs at full speed ahead. Now that you've seen how to use the Javassist and BCEL frameworks for classworking (see the listing of previous articles in this series), I'm going to show you a practical classworking
application. This application is replacing the use of reflection with classes
generated at runtime and immediately loaded into the JVM. In the process of
putting it together, I'm going to refer back to the first two articles of the
series as well as the Javassist and BCEL coverage, so it makes a nice wrap-up for what's turned out to be a long series of articles.
Reflections on performance
Back in Part 2,
I showed how reflection is many times slower than direct code for
both field access and method calls. This sluggishness is not a problem for many applications,
but there are always going to be cases where performance is critical. In these
cases, reflection can represent a real bottleneck. Replacing reflection with
statically compiled code can be very messy, though, and in some cases (as in
frameworks where the classes or items accessed by reflection are supplied at runtime,
rather than as part of the same build process) may even be impossible without
restructuring the whole application.
Classworking gives you an alternative that combines the performance of
statically compiled code with the flexibility of reflection. The basic approach
here is to construct a custom class at runtime that will wrap access to the target
classes (previously reached by reflection) in a way that can be used by your
general-purpose code. After loading the custom class into the JVM, you're then
set to run at full speed.
Setting the stage
Listing 1 gives a starting point for the application. Here I've defined a simple
bean class, HolderBean, and an access class, ReflectAccess. The access class takes a single command line
argument that must be the name of one of the int-valued
bean class properties (value1 or value2). It increments the value of the named property, then
prints out both property values before exiting.
Listing 1. Reflecting a bean
public class HolderBean
{
private int m_value1;
private int m_value2;
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
char lead = args[0].charAt(0);
String pname = Character.toUpperCase(lead) +
args[0].substring(1);
// look up the get and set methods
Method gmeth = HolderBean.class.getDeclaredMethod
("get" + pname, new Class[0]);
Method smeth = HolderBean.class.getDeclaredMethod
("set" + pname, new Class[] { int.class });
// increment value using reflection
HolderBean bean = new HolderBean();
Object start = gmeth.invoke(bean, null);
int incr = ((Integer)start).intValue() + 1;
smeth.invoke(bean, new Object[] {new Integer(incr)});
// print the ending values
System.out.println("Result values " +
bean.getValue1() + ", " + bean.getValue2());
} else {
System.out.println("Usage: ReflectAccess value1|value2");
}
}
}
|
Here's a pair of sample runs of ReflectAccess to
illustrate the results:
[dennis]$ java -cp . ReflectAccess value1
Result values 1, 0
[dennis]$ java -cp . ReflectAccess value2
Result values 0, 1
|
Building a glue class
Now that I've demonstrated the reflection version of the code, I'll show you how to
substitute a generated class for the use of reflection. There's a subtle problem
involved in making this substitution work properly that goes back to the discussion of classloading in
Part 1 of this series. The problem is that I'm going to generate a class at runtime that I want to access from the statically compiled code of the access class, but
because the generated class doesn't exist for the compiler, there's no way to
reference it directly.
So how can I link the statically compiled code to the generated class? The
basic solution is to define a base class or interface that can be accessed by
the statically compiled code, then extend that base class or implement that
interface in the generated class. The statically compiled code can then make
direct calls to methods, even though the methods won't actually be implemented
until runtime.
In Listing 2, I've defined an interface, IAccess,
intended to provide this link for the generated code. The interface includes
three methods. The first method just sets a target object to be accessed. The
other two methods are proxies for the get and set methods used to access an int property value.
Listing 2. Interface to the glue class
public interface IAccess
{
public void setTarget(Object target);
public int getValue();
public void setValue(int value);
}
|
The intent here is that the generated implementation of the IAccess interface will provide the code to call the
appropriate get and set methods of a target class. Listing 3 shows a
sample of how this interface could be implemented, assuming that I want to access the value1 property of the Listing 1 HolderBean class:
Listing 3. Glue class sample implementation
public class AccessValue1 implements IAccess
{
private HolderBean m_target;
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);
}
}
|
The Listing 2 interface is designed to be used with a particular property of
a particular type of object. This interface keeps the implementation code simple -- always
an advantage when working with bytecode -- but means that the implementation
class is very specific. There will need to be a separate implementation class for
each type of object and property I want to access through this interface, which
limits the use of this approach as a general replacement ffor reflection.
This limitation isn't a problem as long as you only apply the technique selectively in cases where reflection
performance is really a bottleneck.
Generating with Javassist
Generating the implementation class for the Listing 2 IAccess interface with Javassist is easy -- I just need to create a
new class that implements the interface, add a member variable for the target
object reference, and finish by adding a no-argument constructor and the simple
implementation methods. Listing 4 shows the Javassist code to complete these steps, structured as a method call that takes the target class and get/set
method information and returns the binary representation of the constructed class:
Listing 4. Javassist glue class construction
/** Parameter types for call with no parameters. */
private static final CtClass[] NO_ARGS = {};
/** Parameter types for call with single int value. */
private static final CtClass[] INT_ARGS = { CtClass.intType };
protected byte[] createAccess(Class tclas, Method gmeth,
Method smeth, String cname) throws Exception {
// 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 run through this code in any detail because, if you've been
following this series, most of the operations will already look familiar (and if
you haven't been following the series, check out
Part 5 now for an
overview of working with Javassist).
Generating with BCEL
Generating the implementation class for the Listing 2 IAccess with BCEL is not quite as easy as with Javassist,
but it's still not terribly complicated. Listing 5 gives the code for this purpose.
This code uses the same sequence of operations as the Listing 4 Javassist code,
but runs somewhat longer because of the need to spell out each bytecode
instruction for BCEL. As with the Javassist version, I'm going to skip over the
details of the implementation (refer back to
Part 7
for an overview of BCEL if anything looks unfamiliar).
Listing 5. BCEL glue class construction
/** Parameter types for call with single int value. */
private static final Type[] INT_ARGS = { Type.INT };
/** Utility method for adding constructed method to class. */
private static void addMethod(MethodGen mgen, ClassGen cgen) {
mgen.setMaxStack();
mgen.setMaxLocals();
InstructionList ilist = mgen.getInstructionList();
Method method = mgen.getMethod();
ilist.dispose();
cgen.addMethod(method);
}
protected byte[] createAccess(Class tclas,
java.lang.reflect.Method gmeth, java.lang.reflect.Method smeth,
String cname) {
// 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), "m_target", pgen);
cgen.addField(fgen.getField());
int findex = pgen.addFieldref(cname, "m_target",
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", "<init>",
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, Type.VOID,
Type.NO_ARGS, null, "<init>", 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, Type.VOID,
new Type[] { Type.OBJECT }, null, "setTarget", cname,
ilist, pgen);
addMethod(mgen, 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, Type.NO_ARGS, Constants.INVOKEVIRTUAL));
ilist.append(InstructionConstants.IRETURN);
// add public getValue method
mgen = new MethodGen(Constants.ACC_PUBLIC, Type.INT,
Type.NO_ARGS, null, "getValue", cname, ilist, pgen);
addMethod(mgen, 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(),
Type.VOID, INT_ARGS, Constants.INVOKEVIRTUAL));
ilist.append(InstructionConstants.RETURN);
// add public setValue method
mgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID,
INT_ARGS, null, "setValue", cname, ilist, pgen);
addMethod(mgen, cgen);
// return bytecode of completed class
return cgen.getJavaClass().getBytes();
}
|
 |
Performance check
Now that I've got code for both Javassist and BCEL versions of the method
construction, I can try them out to see how well they work. My original reason
for generating code at runtime was to replace reflection with something faster, so it would be good to
include a performance comparison to see how well I've succeeded. Just to make
it interesting, I'll also look at the time it takes to construct the glue class
with each of the frameworks.
Listing 6 shows the main parts of the test code I'll use to check out the
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 the timing results). Both runReflection()
and runAccess() take the number of loops to be executed as a parameter, which is in
turn passed in from the command line (using code not shown in this listing, but
included in the download). The DirectLoader class (at
the end of Listing 6) just provides an easy way of loading the generated
classes.
Listing 6. Performance test code
/** Run timed loop using reflection for access to value. */
private int runReflection(int num, Method gmeth, Method smeth,
Object obj) {
int value = 0;
try {
Object[] gargs = new Object[0];
Object[] sargs = new Object[1];
for (int i = 0; i < 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
HolderBean bean = new HolderBean();
String pname = name;
char lead = pname.charAt(0);
pname = Character.toUpperCase(lead) + pname.substring(1);
Method gmeth = null;
Method smeth = null;
try {
gmeth = HolderBean.class.getDeclaredMethod("get" + pname,
new Class[0]);
smeth = HolderBean.class.getDeclaredMethod("set" + pname,
new Class[] { int.class });
} catch (Exception ex) {
System.err.println("No methods found for property " + pname);
ex.printStackTrace(System.err);
return;
}
// create the access class as a byte array
long base = System.currentTimeMillis();
String cname = "IAccess$impl_HolderBean_" + gmeth.getName() +
"_" + smeth.getName();
byte[] bytes = createAccess(HolderBean.class, gmeth, smeth, cname);
// load and construct an instance of the class
Class clas = s_classLoader.load(cname, bytes);
IAccess access = null;
try {
access = (IAccess)clas.newInstance();
} catch (IllegalAccessException ex) {
ex.printStackTrace(System.err);
System.exit(1);
} catch (InstantiationException ex) {
ex.printStackTrace(System.err);
System.exit(1);
}
System.out.println("Generate and load time of " +
(System.currentTimeMillis()-base) + " ms.");
// run the timing comparison
long start = System.currentTimeMillis();
int result = runReflection(count, gmeth, smeth, bean);
long time = System.currentTimeMillis() - start;
System.out.println("Reflection took " + time +
" ms. with result " + result + " (" + bean.getValue1() +
", " + bean.getValue2() + ")");
bean.setValue1(0);
bean.setValue2(0);
start = System.currentTimeMillis();
result = runAccess(count, access, bean);
time = System.currentTimeMillis() - start;
System.out.println("Generated took " + time +
" ms. with result " + result + " (" + bean.getValue1() +
", " + bean.getValue2() + ")");
}
/** Simple-minded loader for constructed classes. */
protected static class DirectLoader extends SecureClassLoader
{
protected DirectLoader() {
super(TimeCalls.class.getClassLoader());
}
protected Class load(String name, byte[] data) {
return super.defineClass(name, data, 0, data.length);
}
}
|
For a simple timing test, I call the run() method
twice, once for each of the properties in the Listing 1 HolderBean
class. Running the two test passes is important for a reasonably fair test -- the first pass through
the code is going to load all the necessary classes, which adds a lot of
overhead to both the Javassist and BCEL class generation process. This overhead
isn't required on the second pass, though, giving you a better estimate of
how long the class generation would require when used within a real system.
Here's a sample of the generated output when the test is executed:
[dennis]$$ java -cp .:bcel.jar BCELCalls 2000
Generate and load time of 409 ms.
Reflection took 61 ms. with result 2000 (2000, 0)
Generated took 2 ms. with result 2000 (2000, 0)
Generate and load time of 1 ms.
Reflection took 13 ms. with result 2000 (0, 2000)
Generated took 2 ms. with result 2000 (0, 2000)
|
Figure 1 shows the results of this timing test when called with loop counts
ranging from 2K to 512K (tests run on an Athlon 2200+ XP system running Mandrake
Linux 9.1, using Sun's 1.4.2 JVM). Here I've included both the reflection and
generated code times for the second property within each test run (so the pair
of times when using the Javassist code generation are first, followed by the
same pair of times when using the BCEL code generation). The execution
times are about the same regardless of whether Javassist or BCEL is used to
generate the glue classes, which is what I'd expect to see -- but it's always
good to have confirmation!
Figure 1. Reflection vs. generated code speed (time in milliseconds)
As you can see from Figure 1, the generated code executes much faster than
reflection in every case. The speed advantage for generated code increases
as the number of loops goes up, starting out at roughly 5:1 with 2K loops and
going up to about 24:1 with 512K loops. Constructing and loading the first glue
class took about 320 milliseconds (ms) for Javassist, and 370 ms for BCEL, while constructing
the second glue class took only about 4 ms for Javassist and 2 ms for BCEL
(though the clock resolution is only 1 ms, so these times are very rough). If
you combine these times, you'll see that even for the 2K loop case generating a
class is going to give better performance overall than using reflection (with a
total execution time of about 4 ms to 6 ms, versus about 14 ms for reflection).
In actuality, the situation is even more biased in favor of the generated
code than this chart seems to indicate. When I tried going as low as 25 loops, the
reflection code still took 6 ms to 7 ms to execute, while the generated code was too
fast to register. The time taken by reflection for relatively small loop counts
appears to reflect some optimizations going on within the JVM when a threshold
is reached; if I lowered the loop count below about 20, the reflection code also
became too fast to register.
Speeding you on your way
Now you've seen the kind of performance that runtime classworking can deliver
for your applications. Keep it in mind the next time you're facing an
intractable performance optimization problem -- it may just be the magic bullet
that can save you a major redesign. Classworking is good for more than just
performance, though. It's also a uniquely flexible approach to tailoring your
application to runtime requirements. Even if you never have cause to use it in
your code, I think it's one of the features of Java that keeps programming fun
and interesting.
This venture into a real-world application of classworking wraps up the
series on Java programming dynamics. But don't despair -- you'll soon get the chance
to sample some other classworking applications at the developerWorks buffet
when I present some of the tools that have been built
around Java bytecode manipulation. First up will be an article on a pair of test
tools straight out of Mother Goose.
Download | Name | Size | Download method |
|---|
| j-dyn0610.zip | | HTTP |
Resources
About the author  | 
|  | Dennis Sosnoski is the founder and lead consultant of Seattle-area Java
consulting company Sosnoski Software Solutions, Inc., specialists in
J2EE, XML, and Web services support. His
professional software development experience spans over 30 years, with the last
several years focused on server-side Java technologies.
Dennis is a frequent speaker on XML and Java technologies at
conferences nationwide, and chairs the Seattle Java-XML SIG and Eclipse SIG.
Contact Dennis at dms@sosnoski.com. |
Rate this page
|  |