[JSR308] Suggestion (and example) for annotations that take any annotation or for annotation inheritance
Joseph D. Darcy
Joe.Darcy at Sun.COM
Mon Oct 6 19:24:30 EDT 2008
Ciera Jaspan wrote:
> Hi everyone,
>
> I, and several others, have a need for either annotations that can
> take any annotation or (preferably) for annotation inheritance. While
> annotation inheritance is preferred, I understand there may be good
> reasons this was left out of JSR-175 initially. If annotation
> inheritance is not possible, having annotations which can take any
> annotation would at least help. I've provided examples that motivate
> each of these suggestions below, from a static analysis tool we are
> building. Please let me know if you have any questions about the
> examples given, or need further examples for motivation.
Hello.
I have some general reaction to proposals to change annotations, and
other Java language features, today as well as some specific comments on
this proposal.
JSR 175 (http://www.jcp.org/en/jsr/detail?id=175), which added
annotations to the platform ,was primarily trying to add a language
feature to meet the needs of "Enterprise Edition" Java developers rather
than needs of developers on the base "standard edition" platform.
Indeed, in JDK 5 itself there are few annotation types in the platform
specification and direct use of annotations in the JDK code base is
mostly limited to some @Inherited and @Deprecated tags. Having the
annotations facility be somewhat usable for other purposes, such as
moving toward pluggable type systems, is certainly a happy outcome, but
was not an explicit goal of JSR 175. The design and feature set of JSR
175 was judged as adequate for the enterprise needs and within our
resource envelope to implement. For example, adding support for
repeating annotations was explicitly considered at the time, but in the
end not included in JSR 175 because it would add complexity and there
was the straightforward workaround of using a wrapper annotation with an
array value. Likewise, various other tweaks that added complexity
without clear need or tweaks with unfavorable"thrust to weight ratio,"
as Josh would say, were not necessarily seriously considered even if
they added some expressiveness.
Changing the Java programming language is tricky and a lot of work;
trickier and more work than one would expect! These difficulties stem
from the compatibility constraints on the platform and the number of
ways a language change can be exposed in the platform. I've previously
written about the surprising amount of work needed to add the JDK 5
language features:
"So you want to change the Java Programming Language..."
http://blogs.sun.com/darcy/entry/so_you_want_to_change
I encourage all those seeking to change the language to read through
this to get some sense of the totality of what might be involved and
work through the different facets of their proposal; it can be much more
than just hacking on javac and possibly the JVM. Especially given some
of the experiences adding wildcards to the language, Sun and the JCP
will be conservative in evolving the language now and in the future.
Since the source code for the large majority of the JDK is now available
as open source (http://openjdk.java.net), language experimenters have
access to nearly all the core JDK code that might need to be updated for
a language change, allowing many interactions to be found and addressed
during initial design. Besides the regression tests that come with the
code, OpenJDK developers can also get access to the JCK tests used for
compliance
(http://openjdk.java.net/groups/conformance/JckAccess/index.html).
Compelling, widely applicable use cases and proof-of-concept
implementations of language changes add confidence, but those steps
should be seen only as necessary rather than sufficient conditions. A
serious proposal will take some steps to demonstrate its practicality
via implementation. Experimental work can be quite worthwhile even if
only a modified subset of changes makes it back to the platform, such as
the Pizza proposal (among many others) informing GJ before the
subsequent addition of generics to Java.
I summarize the lesson Dick Gabriel's "Worse is Better" essay
(http://dreamsongs.com/WIB.html) in economic terms: the net present
value of an adequate language feature today (or during the JDK 5 design
cycle) is higher than a perfect language feature six months or a year
(or 4+ years after JDK 5's design) later. If the perfect proposal was
known as the time, it would certainly be worth using. But given an
existing adequate feature, upgrading to the perfect feature later, even
if it is available, may not be worth the cost.
For the annotation proposal in hand, one implication of being about to
return an arbitrary annotation is that the binary encoding of the
annotation data in the class files would need to change. The actual
annotation objects returned at runtime are dynamic proxies implementing
the annotation as an interface. For example, in something like
@Constraint(
trigger={"Child(ddl, this)", "Type(ddl, DropDownListCtrl.class)",
"Equals(select, true)"},
post={"!Selected(ddl)"}
)
the fact that an array of strings needs to be returned for "trigger" is
gleaned from class definition of the Constraint annotation type. If
that reasoning path is changed, this type information needs to be stored
somewhere else. Such a change to the annotation encoding implies
downstream tools that consume classfiles and manipulate annotations need
to be updated too, etc. So this change would actually be much more
pervasive throughout the Java ecosystem than, say, a repeating
annotation proposal which just desugars to a wrapper annotation with an
array-valued member, as has been proposed to the list before. (As an
aside, changing the API to basically require instanceof checks is by
default a bit suspect.)
Now a proposal to change the encoding could be put forward too, but
these can be tricky to get right. For example, fairly late in JDK 5 a
design bug in the annotation encoding was found; storing the Class
objects for primitive types (int.class, etc.) in class-valued annotation
members didn't work. Oops! Mea culpa as well; I had looked over the
design and didn't see the problem and multiple groups of testers and
users didn't find the bug for many months. This year's JavaOne bof
"Upcoming Java Programing Language Features" discussed another
narrowly-avoided problem caught while evolving the language in JDK 5:
using a new interface in java.lang to support the for-each loop nearly
caused a migration compatibility flaw.
This encoding issue is one complication that I see after a bit of
inspection. There may be other complications too; I haven't sat down to
think through all of them and I won't do so if a full evaluation of the
proposal isn't developed by the proposer. I don't want to deprive
others of such an educational experience ;-) Bruce Schneier's lessons
on developing cryptosystems are germane to language evolution too.
While Schneier's Law is "Any person can invent a security system so
clever that she or he can't think of how to break it," doing such
analysis yourself is quite useful and instructive before seeking the
review of others. Schneier discusses review cycles where he is
contracted to try to break a new cryptosytem for a certain number of
hours, say 5. Typically a bug is found within those 5 hours, and the
author of the cryptosytem might then fix the bug,and come back for
another 10 hours of analysis. If no problem is found in those 10
hours, the conclusion must *not* be that the system is secure, rather
only that it can withstand 10 hours of analysis. An 11 th hour might
break it. Likewise, an 11 th or 12 th or 20 th hour of analysis and use
might be needed to find a fatal problem with a Java language change.
We've been surprised all too often before :-(
Certain changes to annotations are within the agreed upon charter of JSR
308. A desugaring of repeating annotation to a wrapper annotation with
an array type isn't very invasive. Revisiting more fundamental design
decisions of JSR 175, such as annotation inheritance and arbitrary
annotation return values, is not attractive because the costs are likely
to exceed the expected benefits to the broader Java developer
community. While the reported use cases are genuinely awkward with the
annotations as they are today, the use cases are very much niche
problems in the vast landscape of all Java programs.
Some of the workarounds being used, such as interpreting string values,
sound sensible given the current annotation design. I'd encourage
exploration of other API-based approaches. For example, the
annotation processing and language model in JDK 6
(javax.annotation.processing.* and javax.lang.model.*) is designed to be
extended and should be easy to proxy an extension based on what the
platform provides. Additionally, the constructing dynamic proxies can
be done for any interface type, not just annotations, so one can place
arbitrary consistency constraints on the construction of such objects.
Likewise, compile-time annotation processing (javac -processor ...
usable in eclipse too) can also enforce arbitrary extra-lingual
consistency constraints.
-Joe
>
>
> 1) Annotations that can take ANY annotation as a property (as opposed
> to a specific annotation type)
>
> Example:
> We have some annotations which signify the relationships between
> different objects at runtime. For example, the user-defined annotation:
>
> @Child({"this", "ret"})
> Object getItem(int ndx);
>
> signifies that the object returned from getItem() is a child of 'this'.
>
> We later use these annotations to define pre and post conditions for
> other methods. This is done with the annotation @Constraint. Shown
> below is a constraint which is enabled only when the three
> relationships in the trigger array are true, and the post condition is
> a single relationship set to false, as shown.
>
>
> @Constraint(
> trigger={"Child(ddl, this)", "Type(ddl, DropDownListCtrl.class)",
> "Equals(select, true)"},
> post={"!Selected(ddl)"}
> )
>
> This is somewhat awkward though. We have annotations in some places in
> the code (such as the annotation on getItem), but in the constraints,
> we use them as a string rather than as an annotation. What we really
> want to is continue to treat these as annotations. For example, we
> could turn our @Constraint annotation above into:
>
>
> @Constraint(
> trigger={@Child({"ddl", "this"}), at Type({"ddl",
> DropDownListCtrl.class}), @Equals({"select", "true"})},
> post={@Selected({"ddl", REMOVE})}
> )
>
> This provides several benefits. Of course, in this case, there is
> consistency for the user. More importantly, with more structure, this
> is more checkable using existing Java tooling. With the string
> representation, a tool must parse the strings itself and report any
> errors. The annotations, however, provide a structure that is already
> checked by the Java parser.
>
> To do this, we would need, at minimum, for annotations to take any
> annotation as a parameter (as opposed to one of a particular type).
> This would then allow the above situation, where we have an array of
> annotations of different types.
>
> Alternately, annotation inheritance would also have sufficed in this
> example, if Child, Type, and Equals all had a common base annotation
> (see below).
>
>
> 2) Annotation inheritance
> Annotation inheritance was explicitly left off in JSR-175, for the
> reason of promoting composition over inheritance. However, this means
> that a) the above scenario is not possible and b) the syntax is unweildy.
>
> As implied earlier, most of our annotations, such as Child, have a
> common base class. These user-defined annotations must have the
> following properties:
>
> String[] value;
> Effect effect; //Effect is an enum
> String test;
>
> Currently, we signal that an annotation has these three properties by
> using the meta-annotation @Relation. Anything using this
> meta-annotation is assumed to have these properties. Therefore, Child
> looks like this:
>
> @Relation
> public @interface Child {
> public String[] value();
> public Effect effect() default Effect.ADD;
> public String test() default "";
> }
>
> As shown before, we use it like this:
> @Child({"this", "ret"})
> Object getItem(int ndx);
>
> Since Child is a user-defined property, we are depending on the
> programmer (not the developer of this tool) to define this properly.
>
> If we were supposed to use composition, then this would imply that
> Relation should have had these properties, and the definition of Child
> then looks like:
>
> public @interface Child {
> public Relation value();
> }
>
> Of course, that means that to use Child, we have to do:
> @Child(@Relation({"this", "ret"}))
> Object getItem(int ndx);
>
> which is rather confusing.
>
> Instead, we'd rather show the inheritance here, which was really what
> we were doing anyway:
>
> public @interface Relation {
> public String[] value();
> public Effect effect() default Effect.ADD;
> public String test() default "";
> }
>
> public @interface Child extends Relation {
> }
>
> and we would use it like this:
>
> @Child({"this", "ret"})
> Object getItem(int ndx);
>
> This keeps the syntax simple, and it makes it checkable. Notice also
> that if we had inheritance for annotations, problem 1 is no longer an
> issue since logically, we are just wanting inheritance in the first place.
>
> Thanks,
> Ciera
>
> --
> Ciera N.C. Jaspan
> Institute for Software Research
> Carnegie Mellon University
> www.cs.cmu.edu/~cchristo <http://www.cs.cmu.edu/%7Ecchristo>
> ------------------------------------------------------------------------
>
> _______________________________________________
> JSR308 mailing list
> JSR308 at lists.csail.mit.edu
> https://lists.csail.mit.edu/mailman/listinfo/jsr308
>
More information about the JSR308
mailing list