Search |
||
Using annotation processors to save method parameter namesPosted by emcmanus on June 13, 2006 at 8:41 AM PDT
The Java compiler doesn't save parameter names in the class
files it generates. This is a problem for Standard MBeans,
because we'd like to show those names in management clients. I talked
about this in
an earlier blog entry, where I suggested using a
[Updated 4 August 2006 to incorporate recent API changes.] Annotation processors were introduced in the Tiger JDK, via the
Annotation processors cannot change the code that the compiler will generate for a given source file. (This is consistent with the idea that annotations cannot change Java language semantics.) However, they can generate new files based on what they find in the source files being compiled. In particular, they can generate new Java source files, and those new files will also be compiled in the current run. The annotation processors will be run on these new files too, potentially resulting in several "rounds" of source file generation. Another interesting thing that annotation processors can do is
to make checks. For example, the A processor for MBean operation parameter namesThe name annotation processor is a bit of a misnomer. It's actually a general-purpose compiler plugin mechanism. You can arrange for it to analyze all source files that are being compiled, whether or not they contain annotations. We can use this ability to define a compiler plugin that extracts the parameter names out of MBean operations defined in Standard MBeans. Here's the idea. Suppose we have the following Standard MBean interface (from the earlier blog entry):
package com.example.myapp;
public interface CacheControllerMBean {
/** Drop the n oldest entries whose size matches the given constraints. */
public int dropOldest(int n, int minSize, int maxSize);
}
We would like to generate another Java interface defined like this:
package com.example.myapp;
public interface CacheControllerMBeanPNames {
public static final String[] dropOldestPNames = {
"n", "minSize", "maxSize",
};
}
Using this, we will be able to modify the
Before looking at how that can be done, let's look at some of the other ways we could achieve the same thing. One of them is to use some sort of script that picks out the relevant information and generates the needed files. The problem with this sort of script is that it's very difficult to do the required pattern matching correctly. Will we be able to write a regular expression that recognizes a Java interface no matter how it is defined? Even if it is defined like this for example?
public // for now
interface
/* Name may change in later version */ CacheControllerMBean
{
If so, then we'll basically be reinventing the parser from the Java compiler. Why not just use that parser, as annotation processors allow us to do? Another possibility is to define a doclet
that you can plug into the Assuming we do use an annotation processor, we can choose between generating Java source code and generating plain text (or binary) files. We could generate an XML file with the parameter names, for example. There are several advantages to generating a source file, however. One is that the compiler will do some basic sanity checking on what our processor generates. Another is that we don't have to do anything special to arrange for the generated class to be packaged up in our jar file; it just gets put there along with all the other compiled classes. A third advantage is that we can retrieve the information at runtime using the same Reflection API that we are already using to introspect the MBean interface. Writing the annotation processorUp until now I've been talking about the A standard annotation processor facility is being defined by JSR
269, "Pluggable Annotation Processing API". This will be part
of the Mustang (Java SE 6) platform, which means that a Java
compiler from any Mustang implementation can support it.
Processing is no longer handled by a separate non-standard tool
like The Mustang documentation for annotation processors is still a bit raw, and I found the slides from the BOF on the subject at the latest JavaOne to be immensely helpful. The Java classes that are of interest when writing a processor
are in A processor is a Java class that implements the javac -processor com.example.processors.MBeanPNameProcessor source-files To shorten the text, I'll use wildcard imports in what follows. In practice, you're always much better off getting your IDE to spell out the imports explicitly. Here's the beginning of the processor:
package com.example.processors;
import java.io.*;
import java.util.*;
import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.lang.model.util.*;
import javax.tools.*;
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class MBeanPNameProcessor extends AbstractProcessor {
public MBeanPNameProcessor() {
}
Most processors will subclass The The
Every processor must have a public no-arg constructor, as here. The next thing is a handy method for debugging the processor:
private static final boolean silent = false;
private void note(String msg) {
if (!silent)
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);
}
This is the equivalent of We can set Here, Here's the
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
note("process: annotations=" + annotations + ", roundEnv=" + roundEnv);
checkForMBeanInterfaces(roundEnv.getRootElements());
return false;
}
The On the first round, We're not concerned with the details of rounds for this processor. Whatever classes we see, we'll analyze. If there are no classes, we won't analyze anything. This processor will handle MBean interfaces that are top-level members of a package, but also that are defined within other classes. Defining an MBean interface as a nested class isn't recommended in general but I often do it for tests so that the whole test fits into one source file. The processor could be simplified slightly if we only supported top-level MBean interfaces. The following method will initially be given the list of classes being compiled. Then for each class in the list it will be invoked recursively with all of that class's members, namely constructors, fields, methods, and nested classes. Of these, only nested classes interest us, so we'll filter out all the others. Finally we'll call another method on every class we see, including nested classes, in order to pick out the MBean interfaces.
private void checkForMBeanInterfaces(Collection<? extends Element> elements) {
note("checkForMBeanInterfaces: " + elements);
Collection<? extends TypeElement> typeElements =
ElementFilter.typesIn(elements);
for (TypeElement type : typeElements) {
checkForMBeanInterfaces(type.getEnclosedElements());
checkForMBeanInterface(type);
}
}
Here's the method that detects an MBean interface. An MBean interface must match the following criteria:
private void checkForMBeanInterface(TypeElement type) {
// name must end in MBean
if (!type.getQualifiedName().toString().endsWith("MBean"))
return;
// must be an interface
if (type.getKind() != ElementKind.INTERFACE)
return;
// must be public
if (!type.getModifiers().contains(Modifier.PUBLIC))
return;
// must not have type parameters
if (!type.getTypeParameters().isEmpty())
return;
writePNamesInterface(type);
}
If the type met all those conditions, then we're ready to
analyze it and write out the generated
private void writePNamesInterface(TypeElement type) {
note("Found " + type);
String pnamesInterfaceName =
processingEnv.getElementUtils().getBinaryName(type) + "PNames";
try {
writePNamesInterface(type, pnamesInterfaceName);
} catch (IOException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Could not create source file for " + pnamesInterfaceName +
": " + e);
}
}
If we get an exception while we're trying to write the new file, then we convert that into a compiler error as shown above. Generally speaking, processors should not throw exceptions. We use Here's the code that actually generates the new interface.
private void writePNamesInterface(TypeElement type, String pnamesInterfaceName)
throws IOException {
Filer filer = processingEnv.getFiler();
OutputStream os =
filer.createSourceFile(pnamesInterfaceName).openOutputStream();
PrintWriter pw = new PrintWriter(os);
int lastDot = pnamesInterfaceName.lastIndexOf('.');
String baseName = pnamesInterfaceName.substring(lastDot + 1);
pw.println("// " + baseName + ".java - generated by " +
MBeanPNameProcessor.class.getName());
pw.println();
if (lastDot > 0) {
pw.println("package " + pnamesInterfaceName.substring(0, lastDot) + ";");
pw.println();
}
pw.println("public interface " + baseName + " {");
for (ExecutableElement method :
ElementFilter.methodsIn(type.getEnclosedElements())) {
writeMethodPNames(pw, method);
}
pw.println("}");
pw.close();
os.close();
}
private void writeMethodPNames(PrintWriter pw, ExecutableElement method) {
pw.println(" public static final String[] " + method.getSimpleName() +
"PNames = {");
pw.print(" ");
for (VariableElement param : method.getParameters())
pw.print("\"" + param.getSimpleName() + "\", ");
pw.println();
pw.println(" };");
}
}
That's it! This is a complete processor that can generate a
package com.example.myapp;
public interface CacheControllerMBeanPNames {
public static final String[] dropOldestPNames = {
"n", "minSize", "maxSize",
};
}
(It's worth noting that the generated interface will be incorrect if the MBean interface contains overloaded methods, i.e. more than one method with the same name. We strongly recommend against including overloaded methods in MBeans so "this could be construed as a feature.") Using the generated interfaceAll we need to do now is to modify
@Override
protected String getParameterName(MBeanOperationInfo op,
MBeanParameterInfo param,
int paramNo) {
String name = param.getName();
Method m = methodFor(getMBeanInterface(), op);
if (m != null) {
PName pname = getParameterAnnotation(m, paramNo, PName.class);
if (pname != null)
name = pname.value();
else {
String name1 = getNameFromPNames(op, name, paramNo);
if (name1 != null)
name = name1;
}
}
return name;
}
And here's the new method getNameFromPNames:
private String getNameFromPNames(MBeanOperationInfo op, String name, int paramNo) {
try {
Class<?> pnamesClass =
Class.forName(getMBeanInterface().getName() + "PNames");
Field namesField = pnamesClass.getField(op.getName() + "PNames");
String[] names = (String[]) namesField.get(null);
return names[paramNo];
} catch (Exception e) {
// no PNames class, or malformed
return null;
}
}
Phew! I hope that's been of interest. My thanks to Joe Darcy, who took the trouble to give me some very detailed explanations. »
Related Topics >>
Open JDK Comments
Comments are listed in date ascending order (oldest first)
|
||
|
|