|
|
||
Eamonn McManus's BlogBuild your own interface - dynamic code generationPosted by emcmanus on October 18, 2006 at 06:26 AM | Comments (6)Sometimes static code isn't enough and you need to build code dynamically, at run time. That's usually a hefty proposition, but if the code you need to build is just an interface, it's actually relatively simple. Here are some of the reasons you might want to build interfaces at run time and how you might go about it. Static and dynamic codeThe first thing to remember before launching into dynamic code
generation is that it is nearly always a much better idea to
use static code generation if you can. Static code
generation just means writing code as To make the distinction clearer, suppose you have an interface that looks like this:
public interface SomeInterface {
public void doSomething();
}
If
SomeInterface object = ...whatever...;
object.doSomething();
Suppose, though, that your application creates the
Class<?> someInterfaceClass =
Class.forName("SomeInterface", false, someClassLoader);
...get an instance of someInterfaceClass somehow...
Method doSomethingMethod = someInterfaceClass.getMethod("doSomething");
doSomethingMethod.invoke(instance);
I'm deliberately omitting a lot of detail for the moment. The point is just that the code to invoke the method looks completely different, and a lot more complicated. Hence my advice to avoid dynamic code generation if you can. The class java.lang.reflect.ProxyThe Java SE platform already contains some support for dynamic
code generation, notably in the class java.lang.reflect.Proxy.
This class allows you to take any interface and produce a class
that implements that interface, where calling any method results in
a call to an
The way MBeanServer proxy = (MBeanServer) Proxy.newProxyInstance(...details omitted..., myHandler); proxy.registerMBean(someObject, someObjectName); Then the call to
myHandler.invoke(proxy, method, new Object[] {someObject, someObjectName});
where Dynamically generating an interface suddenly becomes more
interesting in conjunction with this Dynamic code generation and the JMX APIThe JMX API uses reflection heavily. If we can dynamically generate an interface, then we can give that interface to a JMX method that will use reflection on it. Here are a couple of ways we can exploit this. Recall that a Standard MBean is a Java class that implements an interface called the MBean interface. That interface determines which methods in the class are management methods. When you register an instance of the class with the JMX Agent (MBean Server), these methods can be called by a management client such as JConsole. Here's an example of an MBean class and its accompanying interface:
public class Cache implements CacheMBean {
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public int getUsed() {
return used;
}
...
}
public interface CacheMBean {
public int getSize();
public void setSize(int size);
public int getUsed();
}
It can be troublesome to maintain two different source files,
for the public class
public class Cache {
@Managed
public int getSize() {
return size;
}
@Managed
public void setSize(int size) {
this.size = size;
}
@Managed
public int getUsed() {
return used;
}
...
}
How might we implement that? Model MBeansOne possibility is to use Model MBeans. Model MBeans
allow you to link MBean attributes and operations to arbitrary
public methods of any class. We could use reflection to pick
out the methods with the That's fine as far as it goes, but suppose we want to add a
further feature where you can add
public class Cache {
@Managed
private int used;
@Managed
public int getSize() {
return size;
}
@Managed
public void setSize(int size) {
this.size = size;
}
...
}
RequiredModelMBean allows you to expose public methods of a class, but not fields (public or otherwise). So it is not powerful enough to handle this extension. Annotation processorsA second possibility is to use an
annotation processor to generate an MBean wrapper from the
public interface Cache$WrapperMBean {
public int getSize();
public void setSize(int size);
public int getUsed();
}
public class Cache$Wrapper implements Cache$WrapperMBean {
private final Cache wrapped;
private final Field usedField;
public Cache$Wrapper(Cache wrapped) {
this.wrapped = wrapped;
try {
usedField = Cache.class.getDeclaredField("used");
usedField.setAccessible(true);
} catch (Exception e) {
throw new IllegalArgumentException("Field 'used' inaccessible", e);
}
}
public int getSize() {
return wrapped.getSize();
}
public void setSize(int size) {
wrapped.setSize(size);
}
public int getUsed() {
try {
return usedField.getInt(wrapped);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Field 'used' inaccessible", e);
}
}
}
We use reflection to bypass the usual Java language checks that
would prevent the generated We're still talking about static code generation here, because
annotation processors run at compile time.
Dynamically generating the MBean interfaceA third possibility, then, is to generate the equivalent of the
So how do we go about generating the interface dynamically? There are at least three ways.
Generating the byte code for an interface(If you're not interested in the details of byte code generation, you'll still need to know how to load the generated class, which is the subject of the next section.) Before generating byte code, you do need to have an understanding of the class file format. The reference here is Chapter 4 of the Java VM Spec. (There's an update containing the changes needed for Generics, but that doesn't concern us here.) The main difficulty is that the class file is in two main parts, the constant pool, and "everything else". Everything else here is the class properties (such as its name and superclass) and the contained fields and methods. Every time there is a class name, string, or other constant in this second part, it is actually a reference to the constant pool, as illustrated below.
So this means that when generating a class file, you are actually generating two blocks of data in parallel, the constant pool and the rest. Typically this means generating everything in a buffer while recording the constant pool entries, then writing the constant pool followed by the buffer. Without further ado, let's look at the outline of the
public class InterfaceBuilder {
public static byte[] buildInterface(String name, XMethod[] methods) {...}
}
The idea here is that you call
The
/**
* Object encapsulating the same information as a Method but that we can
* instantiate explicitly.
*/
public class XMethod {
public XMethod(Method m) {
this(m.getName(), m.getParameterTypes(), m.getReturnType());
}
public XMethod(String name, Class<?>[] paramTypes, Class<?> returnType) {
this.name = name;
this.paramTypes = paramTypes;
this.returnType = returnType;
}
public String getName() {
return name;
}
public Class<?>[] getParameterTypes() {
return paramTypes.clone();
}
public Class<?> getReturnType() {
return returnType;
}
// ...define equals, hashCode, toString here...
private final String name;
private final Class<?>[] paramTypes;
private final Class<?> returnType;
}
Now let's look at
public class InterfaceBuilder {
private static final int CONSTANT_Utf8 = 1, CONSTANT_Class = 7;
/**
* Return the byte code for an interface called {@code name} that
* contains the given {@code methods}. Every method in the generated
* interface will be declared to throw {@link Exception}.
*/
public static byte[] buildInterface(String name, XMethod[] methods) {
try {
return new InterfaceBuilder().build(name, methods);
} catch (IOException e) {
// we're only writing arrays, so this "can't happen"
throw new RuntimeException(e);
}
}
private InterfaceBuilder() {
}
private byte[] build(String name, XMethod[] methods) throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
dout.writeInt(0xcafebabe); // u4 magic
dout.writeShort(0); // u2 minor_version
dout.writeShort(45); // u2 major_version (Java 1.0.2)
byte[] afterConstantPool = buildAfterConstantPool(name, methods);
writeConstantPool(dout);
dout.write(afterConstantPool);
return bout.toByteArray();
}
// ...
private final Map<List<?>, Integer> poolMap =
new LinkedHashMap<List<?>, Integer>();
private int poolIndex = 1;
}
This corresponds to the approach described above, where we
generate everything into a buffer ( I've somewhat capriciously chosen to write a class file in the format from Java 1.0.2, way back when the Java platform didn't even have reflection. In fact nothing we are going to use has changed in the class file format since then. But you might prefer to use the format from Java 1.n here, in which case you should use 44+n instead of 45 here, for example 48 for Java 1.4. Now let's take a look at
private byte[] buildAfterConstantPool(String name, XMethod[] methods)
throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
dout.writeShort(Modifier.PUBLIC|Modifier.INTERFACE|Modifier.ABSTRACT);
// u2 access_flags
dout.writeShort(classConstant(name));
// u2 this_class
dout.writeShort(classConstant(Object.class.getName()));
// u2 super_class
dout.writeShort(0); // u2 interfaces_count
dout.writeShort(0); // u2 fields_count
dout.writeShort(methods.length); // u2 methods_count
for (int i = 0; i < methods.length; i++) {
dout.writeShort(Modifier.PUBLIC|Modifier.ABSTRACT);
// u2 access_flags
dout.writeShort(stringConstant(methods[i].getName()));
// u2 name_index
dout.writeShort(stringConstant(methodDescriptor(methods[i])));
// u2 descriptor_index
dout.writeShort(1); // u2 attributes_count
dout.writeShort(stringConstant("Exceptions"));
// u2 attribute_name_index
dout.writeInt(4); // u4 attribute_length:
dout.writeShort(1); // (u2 number_of_exceptions
dout.writeShort(classConstant(Exception.class.getName()));
// + u2 exception_index)
}
dout.writeShort(0); // u2 attributes_count (for class)
return bout.toByteArray();
}
We generate the equivalent of a
This method requires a couple of other methods to generate the cryptic codes that are used for method signatures in the class file format.
private String methodDescriptor(XMethod method) {
StringBuilder sb = new StringBuilder("(");
for (Class<?> param : method.getParameterTypes())
sb.append(classCode(param));
sb.append(")").append(classCode(method.getReturnType()));
return sb.toString();
}
private String classCode(Class<?> c) {
if (c == void.class)
return "V";
Class<?> arrayClass = Array.newInstance(c, 0).getClass();
return arrayClass.getName().substring(1).replace('.', '/');
}
The Finally, we need to manage the constant pool. There are 11
different types of constant and the obvious approach would be
to define an interface or abstract class The constants are stored in a Map so that if the same constant is needed more than once we can reuse it. The use of List for each constant gives us the right behaviour for lookups. A LinkedHashMap ensures that the constants come out in the right order when we iterate through the Map to write the constant pool.
private int stringConstant(String s) {
return constant(CONSTANT_Utf8, s);
}
private int classConstant(String s) {
int classNameIndex = stringConstant(s.replace('.', '/'));
return constant(CONSTANT_Class, classNameIndex);
}
private int constant(Object... data) {
List<?> dataList = Arrays.asList(data);
if (poolMap.containsKey(dataList))
return poolMap.get(dataList);
poolMap.put(dataList, poolIndex);
return poolIndex++;
}
private void writeConstantPool(DataOutputStream dout) throws IOException {
dout.writeShort(poolIndex);
int i = 1;
for (List<?> data : poolMap.keySet()) {
assert(poolMap.get(data).equals(i++));
int tag = (Integer) data.get(0);
dout.writeByte(tag); // u1 tag
switch (tag) {
case CONSTANT_Utf8:
dout.writeUTF((String) data.get(1));
break; // u2 length + u1 bytes[length]
case CONSTANT_Class:
dout.writeShort((Integer) data.get(1));
break; // u2 name_index
default:
throw new AssertionError();
}
}
}
private final Map<List<?>, Integer> poolMap =
new LinkedHashMap<List<?>, Integer>();
private int poolIndex = 1;
Loading the generated interfaceWe now have a byte array containing a class file. How do we go
from that to an actual We're going to need a ClassLoader.
The job of a ClassLoader is to take a class name (such as
The simplest way to manage this is to write the byte array to a
file with the appropriate name, say
OutputStream out =
new FileOutputStream("/tmp/code/com/example/Cache$WrapperMBean");
out.write(bytes);
out.close();
URL codeDir = new File("/tmp/code").toURI().toURL();
ClassLoader loader = new URLClassLoader(new URL[] {codeDir});
Class<?> c =
Class.forName("com.example.Cache$WrapperMBean", false, loader);
But in fact it is not much harder to create your own ClassLoader that directly fabricates and loads the byte code without requiring any files:
public class InterfaceClassLoader extends ClassLoader {
public InterfaceClassLoader(ClassLoader parent) {
super(parent);
}
public Class<?> findOrBuildInterface(String name, XMethod[] methods) {
Class<?> c;
c = findLoadedClass(name);
if (c != null)
return c;
byte[] classBytes = InterfaceBuilder.buildInterface(name, methods);
return defineClass(name, classBytes, 0, classBytes.length);
}
}
MBeans from annotationsWe now have nearly everything we need in order to be able to
build MBeans from
public class Cache {
@Managed
public int getSize() {
return size;
}
@Managed
public void setSize(int size) {
this.size = size;
}
@Managed
public int getUsed() {
return used;
}
...
}
...we want to build a Standard MBean interface like this...
public interface Cache$WrapperMBean {
public int getSize();
public void setSize(int size);
public int getUsed();
}
...and implement that interface to make a Standard MBean that
we can register in the MBean Server. Rather than generating a
class The class
ClassLoader thisLoader = this.getClass().getClassLoader();
MBeanBuilder builder = new MBeanBuilder(thisLoader);
...
Object cacheMBean = builder.buildMBean(cache);
ObjectName cacheMBeanName = ...;
mbeanServer.registerMBean(cacheMBean, cacheMBeanName);
For best results, when a class constructs an
Here's what
public class MBeanBuilder {
private final InterfaceClassLoader loader;
public MBeanBuilder(ClassLoader parentLoader) {
loader = new InterfaceClassLoader(parentLoader);
}
public StandardMBean buildMBean(Object x) {
Class<?> c = x.getClass();
Class<?> mbeanInterface = makeInterface(c);
InvocationHandler handler = new MBeanInvocationHandler(x);
return makeStandardMBean(mbeanInterface, handler);
}
private static <T> StandardMBean makeStandardMBean(Class<T> intf,
InvocationHandler handler) {
Object proxy =
Proxy.newProxyInstance(intf.getClassLoader(),
new Class<?>[] {intf},
handler);
T impl = intf.cast(proxy);
try {
return new StandardMBean(impl, intf);
} catch (NotCompliantMBeanException e) {
throw new IllegalArgumentException(e);
}
}
private Class makeInterface(Class implClass) {
String interfaceName = implClass.getName() + "$WrapperMBean";
try {
return Class.forName(interfaceName, false, loader);
} catch (ClassNotFoundException e) {
// OK, we'll build it
}
Set<XMethod> methodSet = new LinkedHashSet<XMethod>();
for (Method m : implClass.getMethods()) {
if (m.isAnnotationPresent(Managed.class))
methodSet.add(new XMethod(m));
}
if (methodSet.isEmpty()) {
throw new IllegalArgumentException("Class has no @Managed methods: "
+ implClass);
}
XMethod[] methods = methodSet.toArray(new XMethod[0]);
return loader.findOrBuildInterface(interfaceName, methods);
}
}
The first time it sees a given class, such as
The @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Managed { } In this version, we don't allow The
public class MBeanInvocationHandler implements InvocationHandler {
public MBeanInvocationHandler(Object wrapped) {
this.wrapped = wrapped;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Class<?> wrappedClass = wrapped.getClass();
Method methodInWrapped =
wrappedClass.getMethod(method.getName(), method.getParameterTypes());
try {
return methodInWrapped.invoke(wrapped, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
private final Object wrapped;
}
That's everything you need! I've collected the various classes into a zip file for convenience. MXBean mappingsAnother possible application of dynamic interface generation appears in Java SE 6 with the addition of MXBeans to the platform. MXBeans map arbitrary Java types to a fixed set of types, the so-called Open Types defined in javax.management.openmbean. But the MXBean framework operates on MXBean interfaces, not on individual types. It sometimes happens that you need to find out the mapping for a certain type. If you know the type at compile time, you can just put it in an interface, create an MXBean that implements that interface. Suppose you want to know the mapping for java.lang.management.MemoryUsage, for example. Then you can create an interface like this...
public interface MemoryUsageTestMXBean {
public MemoryUsage getMemoryUsage();
public void setMemoryUsage(MemoryUsage x);
}
...and implement it like this...
public class MemoryUsageTest implements MemoryUsageTestMXBean {
volatile MemoryUsage memoryUsage;
public MemoryUsage getMemoryUsage() {
return memoryUsage;
}
public void setMemoryUsage(MemoryUsage x) {
memoryUsage = x;
}
}
Then you can make an MXBean like this...
MemoryUsageTest memoryUsageTest = new MemoryUsageTest();
StandardMBean mxbean =
new StandardMBean(memoryUsageTest, MemoryUsageTestMXBean.class, true);
...and use it like this...
// Discover the OpenType that MemoryUsage is mapped to
MBeanAttributeInfo ai = mxbean.getMBeanInfo().getAttributes()[0];
assert(ai.getName().equals("MemoryUsage");
OpenType<?> memoryUsageOpenType = (OpenType<?>)
ai.getDescriptor().getFieldValue("openType");
// Convert a MemoryUsage into a value of this Open Type
MemoryUsage mu = ...something...;
memoryUsageTest.memoryUsage = mu;
Object openValue = mxbean.getAttribute("MemoryUsage");
// Convert a value of the Open Type back into a MemoryUsage
mxbean.setAttribute(new Attribute("MemoryUsage", openValue));
mu = memoryUsageTest.memoryUsage;
We can make this work for an arbitrary type discovered at run
time, by generating the equivalent of the
public class MXBeanMapper {
private final StandardMBean mxbean;
private final MXBeanInvocationHandler handler;
public MXBeanMapper(Class<?> originalType) {
InterfaceClassLoader loader =
new InterfaceClassLoader(originalType.getClassLoader());
XMethod getter = new XMethod("getX", new Class<?>[0], originalType);
XMethod setter = new XMethod("setX", new Class<?>[] {originalType},
void.class);
Class<?> mxbeanInterface =
loader.findOrBuildInterface("X", new XMethod[] {getter, setter});
handler = new MXBeanInvocationHandler();
mxbean = makeMXBean(mxbeanInterface, handler);
}
private static <T> StandardMBean makeMXBean(Class<T> intf,
InvocationHandler handler) {
Object proxy =
Proxy.newProxyInstance(intf.getClassLoader(),
new Class<?>[] {intf},
handler);
T impl = intf.cast(proxy);
return new StandardMBean(impl, intf, true);
}
public OpenType<?> getOpenType() {
MBeanAttributeInfo ai = mxbean.getMBeanInfo().getAttributes()[0];
assert(ai.getName().equals("X"));
return (OpenType<?>) ai.getDescriptor().getFieldValue("openType");
}
public synchronized Object toOpenValue(Object javaValue) {
handler.javaValue = javaValue;
try {
return mxbean.getAttribute("X");
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
public synchronized Object fromOpenValue(Object openValue) {
try {
mxbean.setAttribute(new Attribute("X", openValue));
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
return handler.javaValue;
}
private static class MXBeanInvocationHandler implements InvocationHandler {
volatile Object javaValue;
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.getName().equals("getX"))
return javaValue;
else if (method.getName().equals("setX")) {
javaValue = args[0];
return null;
} else
throw new AssertionError("Bad method name " + method.getName());
}
}
}
ConclusionsGenerating interfaces is possible and allows us to solve some interesting problems. This is especially true for APIs like the JMX API that use reflection heavily. The source code for the classes above is in this zip file, with the exception of MXBeanMapper.java, which I separated because it requires the Java SE 6 platform. Bookmark blog post: CommentsComments are listed in date ascending order (oldest first) | Post Comment
| ||
|
|