A few weeks ago I was cobbling together a web service for API authentication, and figured I could simply expose an existing class as a simple XML-over-HTTP web service. Since this was the beginning of a larger project at our company to provide API access to a some legacy internal services, I didn't want to manually write a wrapper for each service class. To avoid that onerous task, I figured I'd hack together a code generator or a generic server that could dynamically create endpoints from these classes via reflection (similar, but simpler, than Apache Axis, JAX-WS or other RPC/remoting frameworks).
Java's reflection API is a pretty handy way of iterating over a given class's available methods, and you can use this facility to create and map web service calls to class instances in a given runtime. If a method name is, say getNewsItems() (which you can determine via the Method class object's getName() method), and has a return type of List<String> (which you can determine via the Method class's getReturnType() method) you could, perhaps, create a virtual endpoint at runtime at the path /news-items/ and return an XML representation of a list of strings for each HTTP GET request received by your server.
What you do not have access to via reflection, at least as of 1.6, is access to to the names assigned to the parameters in the source code of the method definition. This is problematic if you are attempting to provide a semantically sensible message or serialization format for a method signature containing one or more parameters. Frameworks that operate purely on reflection (like Axis) tend to generate stubs and skeletons with parameter names like "s0" or "i2" relying on an impoverished metadata set consisting of type and ordering information. However, an easy way to add metadata to a class or method is to create a custom annotation...for our purposes, here is a simple annotation that contains an array of Strings in a property called "parameterNames":
package org.hccp.annotations; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) @interface ParameterNames { String[] parameterNames(); }We can now use this annotation in normal Java classes to add information that we can inspect at runtime:
package org.hccp.dummy; import org.hccp.annotations.ParameterNames; public class Dummy { @ParameterNames(parameterNames = {"foo", "bar"}) public String annotationTest(String foo, int bar) { return foo + bar; } }Now we can query the class for this info:
Method[] methods = org.hccp.dummy.Dummy.class.getMethods(); for (int i = 0; i < methods.length; i++) { Method method = methods[i]; if (method.isAnnotationPresent(org.hccp.annotations.ParameterNames.class)) { org.hccp.annotations.ParameterNames parameterNames = method.getAnnotation(org.hccp.annotations.ParameterNames.class); Class<?>[] parameterTypes=method.getParameterTypes(); for (int i = 0; i < parameterTypes.length; i++) { Class<?> paramTypeClass = classes[i]; String parameterName = parameterNames[i]; // these array are gonna be the same length, right? System.out.println("Parameter " + + "is of type " + paramTypeClass.getName()); } } }An obvious weakness in the above metadata convention is that we are relying on developer diligence to keep the metadata in sync as the method parameter list changes, evolves, is refactored, etc. We can work around this by adding a runtime check (i.e. parameterTypes.length == parameterNames.length) to ensure that the list length of the annotated parameter names is the same as the parameter types returned by the reflection query, however this means that 1) we would defer detection of a length mismatch until runtime when it might too late to take corrective action and 2) it won't actually detect deviations or mismatches that preserve the length but change order or replace variables outright. Compile time is a much better place to check the correctness of the metadata, especially given that we have access to the source code (and therefore the original parameter names). Lucky for us the annotations framework provides a mechanism for processing annotated source (via the poorly named apt tool in JDK 1.5, and with javac as of JDK 1.6). So let's dive right in and create an implementation of javax.annotation.processing.Processor, the 1.6 interface that defines the hooks for compile-time annotation, uh, processing.
package org.hccp.annotations; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.*; import javax.tools.Diagnostic; import java.util.List; import java.util.Set; @SupportedAnnotationTypes(value = {"org.hccp.annotations.ParameterNames"}) @SupportedSourceVersion(SourceVersion.RELEASE_6) public class ParameterNamesProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(org.hccp.annotations.ParameterNames.class); for (Element element : annotatedElements) { if (ElementKind.METHOD.equals(element.getKind())) { ParameterNames parameterNamesAnnotation = element.getAnnotation(ParameterNames.class); String[] parameterNames = parameterNamesAnnotation.parameterNames(); ExecutableElement executableElement = (ExecutableElement) element; List<? extends VariableElement> parameters = executableElement.getParameters(); if (parameterNames.length != parameters.size()) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Mismatch in number of annotated" + "parameter names and the actual parameters declared in the annotated method."); return true; } int i = 0; for (VariableElement parameter : parameters) { if (!parameter.getSimpleName().toString().equals(parameterNames[i])) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Asserted paramter name at position " + i + " is " + parameterNames[i] + " and does not match actual parameter name at same position: " + parameter.getSimpleName().toString()); return true; } i++; } processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Asserted parameter names successfully" + "matched against annotated method."); return true; } } return false; } }To use this processor at compile time, we simply invoke javac with the -processor flag to indicate what processor implementation we should be using. Here is what a successful run would look like:
> javac -cp path/including/compiled/processor -processor org.hccp.annotations.ParameterNamesProcessor src/org/hccp/dummy/Dummy.java > Note: Asserted parameter names successfully matched against annotated method. >And here is what a failure might look at if a parameter name had been changed (in the Dummy.java example above, we have changed "bar" to "yikes" but did not update the annotation):
> javac -cp path/including/compiled/processor -processor org.hccp.annotations.ParameterNamesProcessor src/org/hccp/dummy/Dummy.java > error: Asserted paramter name at position 1 is bar and does not match actual parameter name at same position: yikes > 1 error >
By ensuring correctness of metadata at compile time, you can enforce implicit runtime contracts in your code. Dependency on convention becomes less risky, as assertions and relationships can be tested during build of classes and components.