This article demonstrates two techniques that are frequently asked about: creating Java classes at runtime, and evaluating a mathematical expression that is given as a string. While these two don't have much to do with one another at first sight, the former can be used to implement a well-performing solution to the latter. Performance matters in this case, because usually formulas must be evaluated repeatedly for a range of parameters, at which point approaches that are based on interpreters quickly become a limiting factor. On the other hand, approaches that compile the formula to a Java class –like the one presented below– will show much better performance.
We start out with creating a basic class that evaluates a formula given a single parameter. Step by step the class is extended to be more developer-friendly and/or more powerful, until at the end it handles multiple parameters while being easy to program with, and easy to extend, at the same time.
The accompagnying zip file contains all example codes and the Javassist library, which is used for dynamically creating classes. Nothing else is needed to get the examples to run.
The Beginning
The following code shows the basic steps to create classes at runtime using the Javassist library. Javassist is part of JBoss these days, but is otherwise independent of it; it can be downloaded from SourceForge.
1) ClassPool pool = ClassPool.getDefault();
2) CtClass evalClass = pool.makeClass("Eval");
3) evalClass.addMethod(
CtNewMethod.make(
"public double eval (double x) { return (" + args[0] + ") ; }",
evalClass));
4) Class clazz = evalClass.toClass();
5) Object obj = clazz.newInstance();
6) Class[] formalParams = new Class[] { double.class };
Method meth = clazz.getDeclaredMethod("eval", formalParams);
7) Object[] actualParams = new Object[] { new Double(17) };
double result = ((Double) meth.invoke(obj, actualParams)).doubleValue();
System.out.println(result);
Let's go through the steps one by one and look briefly at what they do. The following examples are based on it, so it's important to understand the basic concepts now.
1) Javassists's ClassPool
represents the set of all classes in a JVM.
It needs to be retrieved once, but nothing further is done with it except from adding the
new class to it.
2) This creates a new class named 'Eval
'. CtClass
is the most
important of Javassist's classes. It's the equivalent to java.lang.Class
,
and is used to carry out just about all modifications one might want to apply
to a new or existing class.
If the newly created class should be part of a package, then its name must contain
the fully qualified class name, like it would be used in an import
statement.
3) A method is created and added to the class. The body of the method is
public double eval (double x) { return (" + args[0] + ") ; }
as if it was part of a regular source file. The formula to be evaluated is passed in as an args
parameter, and becomes part of the method. The method takes a double
parameter and returns a double
result.
4) Once we're done creating the class, we can retrieve its java.lang.Class
object from the CtClass
object. This concludes the use of Javassist - we can now use the new class like we would use any other class that was loaded dynamically.
5) We instantiate it, ...
6) ... use reflection to get a handle to the eval method, ...
7) ... and finally call the method with a parameter of 17 and print the result.
The example can be run using
make run1
(if you have make
available), or directly via
java -classpath .:javassist-3.4.jar Example1 "x + 42"
The expression to be evaluated is passed to the code as a parameter. If you use this formula, the result should be 59 (= 17 + 42).
Case closed? Of course not. The reflection code is ugly, tedious to write, and
–if executed many times– not very performant. We want to write code like
result = obj.eval(17)
instead. The problem with that is that we can't
use the class Eval
in the code directly, because the class doesn't yet
exist when the code is compiled.
Making It Nice
Interfaces to the rescue! The class we create has only a single method we care about
(eval
), and we know what its method signature will be. So we can move the
method into an interface, and have the to-be-created class implement this interface.
Then we can cast the object to the interface in step 5, and call the eval
method directly. This is the interface we'll implement:
public interface Evaluator {
public double eval (double x);
}
This is easy to do with Javassist. The following line declares that our class will
implement Evaluator
:
evalClass.setInterfaces(
new CtClass[] { pool.makeClass("Evaluator") });
Then we can cast to it, and call eval:
Evaluator obj = (Evaluator) clazz.newInstance();
double result = obj.eval(17);
System.out.println(result);
This is what Example2
does. It is run in the same way as Example1
,
except that it also takes the numeric argument from the command line, so that you can evaluate
the formula using other values besides 17.
One small additional change is that a timestamp is appended to the classname. This doesn't change anything visibly –because we don't really care about the class name– but it serves as a minimal guard against threading problems in case the class-constructing code is ever run in a multi-threaded environment.
Adding Convenience
After using this code for a while you might want to evaluate more complex formulas, containing
constants and functions. No problem – the following lets us use "pi
" instead of
having to type "3.141592
..." in our formulas all the time:
evalClass.addField(
CtField.make("private double pi = Math.PI;", evalClass));
java.util.Random
class. The following lines add a field for a Random
object, a constructor that initializes it, and a rnd()
method that returns a random number between 0 and 1.
evalClass.addField(
CtField.make("private java.util.Random rnd;", evalClass));
CtConstructor constr = CtNewConstructor.defaultConstructor(evalClass);
constr.setBody("{ rnd = new java.util.Random(System.currentTimeMillis()); }");
evalClass.addConstructor(constr);
evalClass.addMethod(
CtNewMethod.make("public double rnd() { return rnd.nextDouble(); }", evalClass));
Example3
shows this in action. Note that both adding a field and adding
a body to the default constructor works in much the same way as adding methods:
you supply the source code directly to the appropriate make
method,
and add the result to evalClass
.
Even More Convenience
Once you've added a few more functions and constants in the way described above, you're probably wondering if there isn't a more convenient way to do this. Dealing with Javassist's various classes, and having to maintain source code as part of a string inside of other source code is not exactly fun.
Subclassing to the rescue! So far, the code has implemented the Evaluator
interface, which didn't allow us to supply any methods to go along with the eval
method. But nothing stops us from creating a class that extends some other class that has
implementations of all kinds of nifty functions. Look at the source code of the Expr
class – it does just that.
Example4
implements this. We just need to get a CtClass
object
for the Expr
class:
CtClass exprClass = pool.get("Expr");
And then make that the superclass of our expression-evaluating class:
evalClass.setSuperclass(exprClass);
That was easy. Now any additional functions can easily be maintained as part of the
Expr
class. An alternative to implementing Evaluator
in this
scenario would be to make eval
an abstract method of Expr
instead.
Whether one or the other makes more sense depends on how the class will be used;
functionally the approaches are equivalent.
Further Possibilities
Expression evaluation isn't limited to functions taking a single parameter, of course.
Example5
shows a program that takes either 1 or 2 parameters, and generates the
class accordingly.
These examples have only scratched the surface of what Javassist can do. In addition to creating new classes, it's also possible to modify existing ones, and to remove fields or methods from a class.
The CtClass
class also has methods for writing the bytecode of a class
to disk or obtaining it in a byte[]
, in case it's necessary to preserve it
beyond its one-time use.
Sometimes a class depends on other classes; in those cases there are ways to tell the
ClassPool
about what to use as classpath.
There's also a low-level API for dealing with bytecode directly.
Conclusion
This article showed how to create Java classes dynamically, based on information available at runtime. It then used those classes to evaluate mathematical expressions that were not known beforehand. Both these are problems one doesn't encounter frequently, but they do occur, and neither is trivial to solve. But knowing that it's not all that hard, additional ways to make use of them probably suggest themselves.