blob: 99f3099c23200d82aa364cd348862faa64f316ef [file] [log] [blame]
<html>
<head>
<title>Using JAM to Improve Developer Experience with JSR175</title>
<link href="jam.css" rel="stylesheet" type="text/css" />
</style>
</head>
<body>
<h2>Using JAM to Improve Developer Experience with JSR175</h2>
<h3>Overview</h3>
<p>
<a href='http://www.jcp.org/en/jsr/detail?id=175'>JSR175</a> is
a new metadata specification being rolled out in JDK1.5. It promises
to formalize the current ad-hoc javadoc tag-based systems by introducing
new language constructs and relfection APIs for viewing metadata. On
the whole, this is going to be a very good thing for the java
community.
</p>
<p>
However, the transition to JSR175 is going to be painful for some
developers, especially those who have already developed tools which process
javadoc tags. This document provides a detailed description of what
some of those difficulties are and how the JAM API can help.
</p>
<h3>The Problem</h3>
<p>
JSR175 brings us a world in which Java has language constructs
to support the declaration of metadata, as well as strongly-typed APIs
for accessing it at runtime. Gone will be the days of parsing
nastily-encoded javadoc tags, replaced by the convenience of
compile-time checks on annotations and simple bean-like interfaces for
accessing them. There is no doubt that these are huge and long-overdue
improvements to Java.
</p>
<p>
However, there are a few practical problems which java developers
face when looking at JSR175. The most obvious is that it only works in
JDK1.5. Java developers who wish to utilize 175 must convince their users
to upgrade to a new JRE. Historically, this has often been a very
difficult thing to
do. Developers who are unable to convince their users to upgrade to 1.5
but wish to still take advantage of JSR175 are faced with the daunting
task of wedding older javadoc tag-based annotations with the newer 175
structures. The problem is compounded by the fact that Sun has
failed to articulate any migration strategy for such developers and users.
</p>
<p>
However, aside from JRE versioning problems, there is another, less-obvious
fly in the 175 ointment. This other problem will not evaporate as
soon as JRE 1.4.2 has gone the way of 1.0.2 - it is a problem fundamentally
woven into the JSR175 specification. It was the result of a conscious design
decision by the spec's authors, and it was made with good reasons
and the best of intentions. It is not a problem that will affect all
java developers, and perhaps not even very many of them. Nonetheless, for a
subset of java programmers trying to perform certain kinds of tasks, it
will remain a problem.
</p>
<p>
The root of this problem is that these new, wonderful
strongly-typed annotation structures are interfaces, as opposed
concrete classes.
</p>
<h3>Divergent APIs</h3>
<p>
Why is that a problem? To answer that, a quick perusal of the 1.5
JDK documentation is informative. Notice <code>java.lang.Class</code>,
you can get instances of <code>java.lang.annotation.Annotation</code>.
However, if you look at the 1.5 Javadoc Doclet API, you will see that
there is no way to do this; rather, it provides only untyped, 'by-name'
access to the annotation data.
</p>
<p>
Why is this? Remember that those Annotation types are really interfaces.
They don't have a constructor and you cannot instantiate them directly.
The 1.5 JVM has some magic to synthesize an implementation of the
annotation type on the fly at runtime and return an instance of it for you
to use. Unfortunately, the JVM doesn't share that magic with Javadoc,
or with anyone else, for that matter. As a result, there is no way to get
an instance of those annotation interfaces unless you are running in a VM
that has classloaded compiled versions of the annotated classes.
</p>
<p>
Now one may well ask: so what? Aren't you always running in a VM
that has classloaded compiled version of the annotated classes? Well,
this is true most of the time for most developers. However, there are some
developers who want to pre-process java souce before it is compiled.
Often this is because they want access to information that is lost
during java compilation, such as comments. This source analysis is
typically used in driving some kind code generation - xdoclet is a good
example of this. But more generally, this will be a problem for anyone
who is implementing or extending any kind of compilation technology which
directly reads JSR175-annotated java sources.
</p>
<p>
Sun's response (implicit in the 1.5 doclet API) seems to be that if you
are processing java sources in this way, not only are you stuck using a
completely different API for modelling java types, that API in turn forces
you to use an untyped access model for metadata on those types.
<p>
<p>
This is somewhat inconvenient. Strong typing in java programming is
almost always a Good Thing, and again, one of the great promises of 175
is to bring strong typing to the world of metadata. The failure to fully
deliver on this promise to our cadre of compiler and compiler extension
authors may cause them to wonder what was so great about JSR175 in the
first place.
</p>
<p>
<h3>So what can JAM do about this?</h3>
<p>JAM's solution to this problem is simple: return control of the
metadata access model to the compiler authors. This is achieved by
allowing authors to create a bean which acts as a proxy to the JSR 175
annotation data; these beans are called <i>AnnotationProxies</i>.</p>
<p>
The annotation data being proxied may in fact reside on an instance of
<code>java.lang.annotation.Annotation</code>, in the case where we are
looking at an annotation in a 1.5 VM. However, it may also simply
be an 175 annotation declaration in a java source file (remember,
again, that there we have no way to instantiate the 175 interface for
the annotation be declared). JAM does all the work to plug the
data into the AnnotationProxy. The compiler author in turn gets a
strongly-typed, consistent API metadata without having to worry about
where it's coming from.
</p>
<p>
Here is a simple example of what this looks like. Say we have defined
a 175 annotation type, like the following:
</p>
<pre>
package com.special;
public @interface SpecialId {
public int id() default 13;
}
</pre>
<p>
Our compiler author would then define a proxy to represent the SpecialId
annotation which would probably look something like this:
</p>
<p>
(you can ignore the verbose javadocs unless you're looking for more detail):
</p>
<pre>
package com.special;
/**
* This class is defined by the author to act as a proxy to instances
* ot the SpecialId annotation. Though the author is largely free
* to do whatever they want in this class here, it is probably advisable
* to make it conform to the conventions and constraints
* associated with JSR 175 annotation types as much as possible, i.e.
* as if it actually implemented the 175 annotation interface.
* This means there are limits on the types of the bean properties
* and also that accessor methods are not prefixed with 'get'. A
* proxy should resemble the real thing as much as possible.
*
* The only really firm requirement is that the class be public,
* non-abstract, and have a public default constructor. It also is
* required that you provide setter methods that correspond to the type
* and names of the annotation properties so that JAM can use reflection
* to populate the AnnotationProxy. (Actually, even this is not a really
* firm requirement if you're willing to do a little extra work - see
* the TypedAnnotationProxyBase javadocs for details).
*/
public class SpecialIdProxy extends TypedAnnotationProxyBase {
public SpecialIdProxy() {} // a public no-arg constructor is required
public int id() { return mId;} // just like the 175 annotation method
public void setId(int id) { mId = id; } // this gets called by JAM
private int mId = 13; // note that this is same as default
}
</pre>
<p>
We have chosen to make the SpecialIdProxy expose the same public face
as our JSR175 interface, <code>SpecialId</code> (they both have an
<code>id()</code> property accessor). However, it is worth noting that
SpecialIdProxy does <b>not</b> implement <code>SpecialId</code>.
There are two reasons for this:
</p><p>
<ul>
<li>
Doing so would immediately lock our code into JDK 1.5. This
is beacuse SpecialId will extend from
<code>java.lang.annotation-Annotation</code>, which is 1.5-specific.
</li>
<li>
As of this writing, it's not entirely clear that writing your own
implementation of a JSR175 annotation interface is a valid thing to do
</li>
</ul>
</p>
<p>
So, now we have an JSR175 annotation interface, and we've written a
proxy class that looks just like it (but does not implement it).
So far, so redundant, right?
</p>
<p>
To see why we have done this, let's write a very simple compiler-like
tool that inspects a single java source file or java class, prints out
it's comments (if it has any) and outputs the value of the SpecialId
annotation. That program would look something like this:
</p>
<pre>
package com.special;
public class SpecialIdViewer {
public static void main(String[] args)
{
// Verify the args are correct
if (args.length != 1) {
throw new Exception("must pass one java class name or "+
"relative source file path");
}
// Bootstrap into JAM. We need to create a JamService that has either
// a class- or source- based representation of a class, depending
// on what they specified.
JamServiceFactory factory = JamServiceFactory.getInstance();
JamServiceParams params = factory.createServiceParams();
// Establish a mapping between the 175 annotation type and our proxy
params.register175AnnotationProxy(SpecialIdProxy.class,"com.special.SpecialId");
// Decide whether they want to look at a source file or a class
if (args[0].endsWith(".java")) {
params.includeSourceFiles(new File("."),args[0]);
} else {
params.includeClass(args[0]);
}
// Ok, create our entry point into JAM
JamService service = factory.createService(params);
// Whew! Ok, enough set up, let's get some real work done
// First, we get a JClass from the service that represents the
// class they specified. In this particular case, we know
// there can be exactly one, so just get it.
JClass clazz = service.getClasses[0];
// Now get the class doc and print it out if it's available
JComment comment = clazz.getComment();
System.out.println((comment != null) ? comment.getText():
"[comments are not available from classfiles]");
// Ok, get (a proxy to) the annotation
SpecialIdProxy annotation =
(SpecialIdProxy)c.getAnnotation(SpecialIdProxy.class);
// Note that at this point we have a typed structure for our metadata
// which we can use exactly like a real 175 annotation.
int specialId = annotation.id();
System.out.println("The special id is: "+specialId);
}
}
</pre>
<p>
We can now compile our SpecialIdViewer along with our SpecialId 175
annotation type and our SpecialIdProxy proxy. With that done, we might
now jar these things up and then send them off into the world so that
people can annotate their classes with SpecialIds and view them with our
SpecialIdViewer.
</p>
<p>Let's take a look at how that would work. Say we have a JRE 1.5 user
who has just downloaded our jar. They add our jar to their classpath
and then compile the following java source file they have written:
<pre>
package com.random.user;
import com.special.SpecialId;
/**
* This is some random user code compiled with 1.5 javac.
*/
@SpecialId(id = 42)
protected abstract class RandomUserCode {}
</pre>
<p>Now they run our SpecialIdViewer tool and see the following:</p>
<pre>
> java com.special.SpecialIdViewer com.random.user.RandomUserCode
[comments are not available from classfiles]
The special id is: 42
</pre>
<p>
Nothing too surprising there, but what exactly happened? When we asked
for the class' annotation of type SpecialIdProxy, JAM looked up the
SpecialId on the user class (because that's what we mapped
SpecialIdProxy to in the call to <code>mapAnnotationProxy()</code>),
created a new instance of SpecialIdProxy, copied the SpecialId data into
that proxy, and returned it to us so that we could print it out. It also
realized that it was unable to print out the class comments because it
only had the class, not the source.
</p>
<p>
At this point, you are probably still wondering why we went to all
of this trouble with this proxy business. Why didn't we just use the
175 SpecialId annotation type directly? To illustrate the advantage,
consider the same example again, except that this time the user
does not run <code>javac</code> on RandomUserCode.java and instead
provides our tool with only raw, uncompiled <code>.java</code> source code.
</p>
<pre>
> java com.special.SpecialIdViewer RandomUserCode.java
This is some random user code.
The special id is: 42
</pre>
<p>
Note JAM is able to give us the comment now, since this time we
have the source code. But otherwise, things basically worked just
the same as before. Nothing to surprising here, either.
</p>
<p>
But wait, isn't that itself surprising? Think for a minute about what you
would have to do in order to make this work without JAM (i.e. with
reflection and javadoc or something similar like xjavadoc or qdox.
At the point in our SpecialIdViewer where you decide whether you are
looking at a source file or a class file, you would have to fork your
codepath - on the one path, you would use the Reflection APIs, get your
175 SpecialId instance directly, and decide not to print out a comment.
On the other path, you would have to use javadoc's untyped API to get the
SpecialId value, and in this case you would be able to print out a comment.
In effect, you would have written almost all of your tool twice over.
</p>
<p>
In case you're still not surprised, consider what happens if our user
suddently decides to switch back to JRE 1.4. If they repeat the
previous example (using the source file), it still works just the
same. How is that possible, you ask? Well, first off, note that
SpecialIdViewer itself will still load just fine under 1.4 because
SpecialIdProxy has isolated it from any 1.5 dependencies. And because
we have the source file (RandomUserCode.java), JAM's java source parser is
able to parse the 1.5 annotations and populate the SpecialIdProxy without
requiring anything from 1.5.
</p>
<p>
That may be a neat party trick, but is it really useful? After all,
RandomUserCode is never going to compile or classload under 1.4.
Why someone want to do this? In truth, they probably won't. However,
they might want to do something similar but slightly different.
</p>
<h2>Processing Javadoc Tags</h2>
<p>
Take the case of a user who has not yet upgraded to JDK 1.5. They
simply refuse to upgrade, for the usual reasons users refuse to upgrade.
They still want to markup their classes with our SpecialIds, but because
they won't have JRE 1.5, they won't be able to use our JSR175 annotation
(SpecialId) to do it.</p>
<p>
The obvious choice in this case to let them declare their special ids
with javadoc tags, which have been the de facto standard for declaring
java metadata prior to JDK 1.5. Accordingly, our stubborn 1.4 user might
expect to be able to run SpecialIdViewer on the following 1.4 java
source file:
</p>
<pre>
package com.stubborn.user;
/**
* This is code from someone who simply will not upgrade to 1.5.
*
* @special id=79
*/
protected abstract class StubbornUserCode {}
</pre>
<p>
This seems reasonable enough. Unfortunately, though, that means we're going
to have to write a lot more code in our tool. Now we have to check for
javadoc tags, parse them if they are there, and then somehow glue
everything together in our code so that the behavior is the same. Right?
</p>
<p>
Wrong, fortunately. JAM can be a big help in this case as well. To support
this javadoc tag, we only need to add one new line (in bold) to our
SpecialIdViewer tool:
</p>
<pre>
package com.special;
public class SpecialIdViewer {
public static void main(String[] args)
{
...
// Establish a mapping between the 175 annotation type and our proxy
params.register175AnnotationProxy(SpecialIdProxy.class, "com.special.SpecialId");
// Also establish a mapping between the '@special' javadoc tag and our proxy
<b>params.registerJavadocTagProxymapJavadocTagProxy(SpecialIdProxy.class, "special");</b>
...
}
</pre>
<p>
Now our stubborn 1.4 user runs our tool, and the desired output appears:
</p>
<pre>
> java com.special.SpecialIdViewer StubbornUserCode.java
This is code from someone who simply will not upgrade to 1.4.
The special id is: 79
</pre>
<p>
This works by exactly the same principle as our earlier example using
JRE 1.5. We have created a SpecialIdProxy through which we want to
view metadata. We now have told JAM that we want that class to be a
proxy for the 'special' javadoc tag (as well as the SpecialId 175
annotation type). When JAM is processes the java source, it therefore
knows that when it sees a 'special' tag, it should parse out it's values
and populate an instance of our SpecialIdProxy for us to use in our code.
</p>
<p>
So, here again we see how useful the annotation proxy is. Because we
have isolated our processing logic from the details of parsing metadata,
the same code works in all of the different scenarios we have seen: with
source files, with class files, with 175-style annotations, or with javadoc
annotations. You don't have to waste time worrying about how your
metadata and type information is going to get represented or parsed.
JAM lets you hide all of this away so you can focus on the real problems
your your application is trying to solve.
</p>
<p>
<i>Still have questions? <a href='faq.html'>Try looking in FAQ.</a>
</p>
</body>
</html>