To Annotate or Not? A rebuttal

Mike Keith has written a piece, To Annotate or Not?, in which he openly criticizes some of the decisions of the JSR-175 committee. As a member of said committee, I thought it might be somewhat constructive to discuss his criticisms and offer up a (potentially) contrary point of view.

He opens his article by offering up a comparison of two metadata-capture schemes, one using XML files to describe a fictitious Remotable component and its methods, and the other using JSR175/JDK 1.5 metadata. At the end of the description, he offers

    I think it is obvious which approach is less verbose, easier to specify, and more effectively conveyed.

We thought so too, Mike. 🙂 Thanks.

    Other advantages of coupling the metadata with the source code are purely practical. For example, if the application needs to change the addNewItem method to accept an additional backOrder parameter, the application or the entity that changed the addNewItem method must also remember to change the XML file, which may be stored in an entirely different location from the class. This requirement to remember to change the XML file is simply because the method parameters were part of the contextual XML information required to specify the method. The maintenance incentive for using annotations, then, is fairly obvious.

It gets worse, too, when you consider that on any non-trivial application source base, a given programmer may not be the only one to work and modify a given piece of source code. Thus, when a modification gets made, a given programmer (particularly if they’re new to the system or the source base) makes a modification, they may not even realize that the code needs modification in multiple places (source code and descriptor). This my classic "college intern" scenario, since it’s often the college intern who (a) doesn’t realize the need to make the change, and (b) often draws these little one-off kinds of changes.

    Similarly, XML that is not connected to the code is also not an integrated part of the same version-controlled element as the code. Changing one element and creating a new version of it does not intrinsically imply creating a new version of the other, although it is possible to configure some version control systems to do such a thing. Although there are cases in which changing one does not require changing the other, even a dependency in one direction (that is, the metadata on the code) points to the more appropriate coupling of the two.

It gets deeper than this, though. If the metadata is stored outside the code, it becomes painful to try and access the metadata in a consistent format. As it stands in his XML-driven format, to access the method in question and discover if it is Remotable requires both Reflection calls and XML parser calls (assuming we’ve already established a well-known location for that file at runtime, which of course all of the J2EE specs have done, buried deep inside the .jar/.ear/.war files as appropriate). Correlating across the two APIs becomes a pain, never mind the problems of programmers accidentally fat-fingering the "addNweItem" method name in the XML metadata file.

He then goes on to offer up some criticisms, though:

    So what is the cost of brevity? Specifying the metadata beside the source instead of in a decoupled XML file has certain repercussions. … Take the example of a tool for adding metadata to existing classes. At the first step of the process, we immediately hit the first and most obvious problem. What if only the class files are present but the source code is not available? Annotations may only be read at runtime, not added. Annotating the classes is not an option, and the tool is forced to use XML or some other mechanism external to the class.

We discussed (for some time) the idea of adding an "At Arm’s Length API", so that one could consume metadata without having to go through the Reflection APIs (and thus load the class, rendering it immutable at that point), but had to drop the idea since Java currently has no such API for reading class files as a whole. Or rather, no such API within the J2SE boundaries–the Apache BCEL library serves that role quite impressively (in fact, it’s buried deep inside the Sun J2SE implementation, if you go looking for it–they renamed the packages from "org.apache" to "com.sun.org.apache", but it’s all there), and it was our expectation that BCEL would evolve to fill that role post-release. In the meantime, this lack of low-level access is an obvious hole that should probably be fixed in another JSR, but that’s another debate for another day. In the meantime, it’s entirely feasible to imagine writing a tool that uses BCEL to consume the annotations and modify the .class structures on disk without the corresponding source code… assuming you have some way of even stating what it is you want to modify, and where (which Mike never really suggests a concrete use case for).

    The next step is to actually add the annotations. This becomes a fairly intensive process for the tool, because the source needs to be parsed and the new annotations added at the correct position. Whereas using XML was a simple matter of having to specify the context and then write out the XML according to the given schema, we now have to find the source code for the element, parse it, add the correct syntactical annotation pieces, and then rewrite it all back out. Just explaining it is exhausting.

Pshaw. Write an "apt-let" (a plugin for the apt, or Annotation Processing Tool, utility) that sucks in the metadata and spits it back out in either source (easier but slower) or binary (harder but faster) form. I’m not suggesting either of these is easy, but again, Mike’s use-case here is that you want to take an existing class and hack up its metadata in a third-party form. Which, if you ask me, is really sort of a ludicrous case, unless you’re a tool vendor (like Oracle). See, the idea of metadata was largely to allow programmers, the authors of those code files, to express customized messages to entities that wanted to consume those messages in an out-of-band and nonintrusive format, like Serialization does. It was never intended as a generalized utility for third parties (like system administrators) to add metadata to the source "after the fact".

    Once the annotations have been added, the classes need to be recompiled. For this to be possible, the definitions for the annotations inserted into the source code need to be on the class path. Although this may not be an issue for immediate recompilation, it may be problematic later on. Annotations are, like an XML file, quite happy to exist in environments in which they get completely ignored. If the processing layer for which the annotations were added in the first place uses runtime reflection as a means of interpreting the metadata (which is the typical scenario), the annotations will clearly need to be preserved in the class files until runtime. Fortunately, the VM is more forgiving at class load time of elements that have annotations for which the definitions are not on the class path. It would be nice if the compile-time dependencies were similarly relaxed and any annotations for which interfaces were not found were simply ignored, having a net semantic of a SOURCE RetentionPolicy (that is, the annotations would not be retained in the class files). The XML artifact equivalent to the annotation interface definitions is the schema, which is required only when the XML file is read.

The "annotations are required on the classpath" part of the annotations spec was easily by far the most contentious, and if I recall the discussions correctly, it was the Oracle rep who said that if we required the annotations to be present on the classpath at compilation time, "this will render annotations entirely useless to us". The problem is, though, we couldn’t come up with a scheme that would allow for some kind of lazy-evaluation processing of annotations’ format against the source, and still remain a strongly-typed language. In almost every proposal, it was defeated by the simple question, "But what happens if the programmer fat-fingers the annotation typename? How will we know when the annotation doesn’t exist?" If we never throw some kind of error, we end up with a subtle source of bugs that would plague all of Java programmer-kind until the end of days. Java is a strongly-typed language, Mike, and that means when you compile against something, you need to have that something handy at compile-time to validate against it. If this upsets you, there’s always ECMAScript….

Then we start getting into the heat of his arguments:

    If you liken annotations to a coup, it would have to be one in which no capable or trained government is ready to take power. For metadata programming, annotations are currently on the immature side of the spectrum. Some of the obvious features are missing, and others are inadequate for day-to-day use.

Now THEM’s fightin’ words. 🙂

    Inheritance. There is none. … If it were possible for annotation interfaces to extend other interfaces, either the SyncEvent or AsyncEvent could be used to annotate an event method and an optional debug string could be specified to be printed to the event log if debugging were enabled. Then, just as in code, if another common member were required, you could easily add a common member by adding it to the Event superinterface. Instead, you must choose one of a few unsightly alternatives. The obvious one is to duplicate the member in both of the events. This produces a potential maintenance headache and goes against the commonly accepted software antipractice of cloning code.

Well, to start with, annotations aren’t really interfaces, so we could never allow annotation types to extend interfaces, but if you replace the word "interfaces" in the above with "annotation types", your concern is largely justified. What Mike fails to note is that annotations do compose somewhat cleanly, and what I would suggest as an alternative would be:

public @interface DebugString {
    String value() default "";
}

public @interface SyncEvent {
    DebugString options() default @DebugString;
    boolean allowPreempting();
}

public @interface AsyncEvent {
    DebugString options() default @DebugString;
    boolean joinAfterCompletion();
}

@AsyncEvent(options=@DebugString("myEventMethod called"),
            joinAfterCompletion=false))
public void myEventMethod() { … }

In other words, we’re not going after inheritance as a form of reuse, but as a logical design mechanism–which was sort of what it was intended to do in the first place (modulo the experience of Smalltalk, at least). What’s more, I get a little tired of people constantly trotting out annotations and using examples like "debug strings" or "tracing"–annotations are not aspects, were never intended to be aspects, and will never become aspects! This sort of example needs to be discussed in an AOP forum, not one on annotations.

    You may come up with your own favorite workaround, but the only elegant solution is to support inheritance of annotation types.

No, the only elegant solution is to use the right tool for the job; in this particular case, since he wants to introduce an annotation that somehow introduces behavior into the method, I suggest he look at AspectJ instead.

    Default Values. Thankfully, J2SE 5.0 offers some amount of support for providing defaults, but it is presently rather cumbersome and inadequate. Because null is not allowed as a valid default value, a special "uninitialized" value needs to be reserved for each type. If this is not possible, it will be hard to know, when reading the annotation, whether the default value was actually specified or whether the value was obtained because it was the default (that is, it was not specified). There is often a semantic difference between these types of values. It gets worse with nested annotations, because the default must be an actual annotation. The unpleasant workaround is to create a bogus annotation member that is used simply to distinguish whether the annotation was specified or was provided by default.

This was a case where Neal and Josh simply stated that the Java compiler would have problems consuming "null" as a value; I won’t pretend to understand the details, but let’s be honest–you can’t specify null as a default value in a lot of places inside the Java environment, such as:

public class Counter
{
  private int i = null; // Error!
}

and somehow we manage to get along just fine. Would it have been nicer if null could be specified as an annotation member default value? Sure. Is it a requirement? Nope.

    The resulting example above is arguably no worse than the XML case, in which a default value may be specified only for a simple type. A more useful feature that does not currently exist is when the default is actually dependent on some program element. Default values should be parameterizable and refer to other elements within the same program scope. For example, what if you wanted to make the default refer to a static variable that could be modified by an admin tool? Or if an annotation value were able to be defaulted to the name of the element it is annotating? The ability for default values to be parameterizable would be really helpful for the processor, but in practice this would be rather difficult to implement, because the annotation type does not have any real scope at definition time. Reserved words, such as this, would probably be needed to make default values parameterizable — an achievable goal.

NO, NO, NO. Mike, annotation values have to be known at compile-time, and allowing annotations to depend on other program elements would only work if those program elements could be known at compile-time. If we could allow the default to refer to a static variable, then what, exactly, do we put into the compiled .class file? Remember, annotations have to be stored there, for processing by apt-lets, and the apt-let may not have said static variable handy for dereferencing. It might be convenient for an annotation to be able to reference the name of the element it is annotating, but this easily turns into a slippery slope; first you want the name, then you want the type (for a field) or the parameters (for a method), or the accessibility flag (public/private/whatever), and suddenly you’re looking at having to introduce a "thisJoinPoint"-like modifier just for annotations to reference. It becomes a huge can of worms that keeps the javac authors busy for years.

    An allowance for single-value annotation types means that if the name of a member is the magic word "value," the name can be skipped (defaulted to value). Although this is somewhat helpful, I believe that it is a rather arbitrary and unnecessary choice. If a single-member annotation is being used, it is not obvious why it can’t be treated as a "value" member in any case, without actually calling it "value." Because the name is guaranteed to be useful only within a single-member annotation, it doesn’t seem to serve any real purpose, and the ability to assign more-descriptive names to my single-member annotations would be useful.

Actually, if I’m not mistaken, it doesn’t have to read "value"; it can be of any name. The name "value" was intended entirely as a documentation hint. And, in fact, I recommend not using it, for maintenance purposes–what happens when you need to add another member to an annotation later? Now you have one member named "value", and it’s not obvious what it’s supposed to be.

    Validation. The allowable set of member value types is fairly restricted and syntactical, and simple value type validation is basically free, just as XML parsers check the type definitions for the base XML schema types. Furthermore, an @Target annotation provides a convenient complimentary check by the VM to ensure that annotation types do not annotate inappropriate program elements or elements that were not meant to be annotated by that annotation. It is more difficult, however, to restrict the annotations that may coannotate an element without doing a great deal of code checking and/or annotation rework. It may not make sense for a method to be both an AsyncEvent and a SyncEvent, but what is to stop someone from annotating it that way? The processor would have to look for both annotations and error-check for the unusual case of both existing on a method. Annotations could be reworked to use enumerated types and thereby limit what could be specified. Inheritance could go some of the way toward solving this problem, because you could at least introduce a parent element that included a single Event member, thus constraining the element to only a single event. What would really help, though, is an additional built-in annotation that would allow an annotation definition to specify a set of annotations that are unacceptable peers annotating the same programming element.

But that presumes that the annotation definition in question knows all other possible set of annotations that could be used to annotate a target, and what happens when an annotation type it doesn’t know about gets added? Should we be strict or liberal in the interpretation of said annotation? Strict means you’d never be able to be annotated alongside third-party library annotations you have no problems with, liberal means you’re basically back to where we started. I don’t really see this as a problem in the future, but should this become a problem, it would essentially be a simple thing to create another javac-enforced meta-annotation like what Mike describes. The hard part would be to decide what the semantics of this "Restricted" annotation would be, strict or liberal.

    Namespace. A concern in the annotations world is that multiple layers of annotations will start colliding within the same global annotations namespace. For example, some of the common annotation type names, such as @Transaction, are likely to be used by multiple layers to signify slightly different semantics unique to each layer. I agree that this is a valid concern, because one of the strengths of annotations is the potential for terseness (potential because they are not necessarily terse; they must be designed to be so) and making names longer to lessen the possibility of collisions could reduce this. … In the end, the annotations namespace problem is analogous to the class namespace problem, and we may be forced to fully qualify the annotations as a last resort.

Which is exactly what Java does currently for any other class name clash. What Java really needs is yet another language enhancement along the lines of the "using" capability in C#:

import java.awt.List = Listbox; // make "Listbox" an alias
import java.util.List = List; // make "List" an alias, just to be clear

public class App
{
  List l = new List(); // no ambiguity: java.util.List
  Listbox lb = new Listbox(); // clearly a java.awt.List
}

which would then render the fully-qualified hideousness problem a thing of the past. JDK 1.6, anyone?

In the end, I don’t necessarily disagree with some of Mike’s points; in fact, I argued for us to allow annotations to inherit just like any other class element (and, in fact, I also wanted us to define a scheme to define annotation elements at the .jar level, so as to integrate Manifest directly into the language, since we’re almost there anyway), but as with any committee-driven effort, others disagreed with me, and the general discussion led to a majority opinion that it wasn’t a good idea. That’s part of what being a citizen in the community means: you win some, you lose some.

In a lot of ways, I wish I could publish the collective email messages that went back and forth among the 175 members, because I hear a lot of criticism that "you never considered …", when in fact, we did consider it, we discussed it, and we rejected it for various reasons. There were some very bright people on that JSR, all of whom I respect deeply (even if I didn’t agree with them), and to suggest that we just casually dismissed an idea or concept essentially insults them all, which I won’t allow to happen without a fight. 🙂

There’s nothing wrong with disagreeing with the decisions we made, so long as you’re willing to offer up something in its place that (a) meets all the criteria of the scope of the JSR, and (b) fits in with the needs of every JSR member on the 175 committee. That’s what we had to do, and in some cases we had to engage in that essential element of every democracy–compromise.

 

“>