[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