Properties via Annotation Processing

The annotation processing APIs provided by apt in JDK 5 and javac in JDK 6 both present a read-only view of source code; by design directly modifying the input sources is not supported through either annotation processing API. Recently, Project Lombok has used the hook of being able to run an annotation processor inside javac to start an agent which rewrites javac internal classes to change the compiler's behavior; yikes! Such extreme measures are not needed to get much of the effect of modifying the input sources. As I've outlined in the annotation processing forum over the years, just using standard annotation processing to generate a subclass or superclass of a type being processed is a very powerful technique for controlling the ultimate semantics and behavior of the original class. For example, I've hacked up a proof-of-concept annotation type and matching processor to generate property-style getter and setter methods based on annotations on fields.

Concretely, the programmer writes something like

public class TestProperty extends TestPropertyParent {
    protected TestProperty() {}

    @ProofOfConceptProperty
    protected int property1;

    @ProofOfConceptProperty(readOnly = false) // Generate a setter too.
    protected long property2;

    @ProofOfConceptProperty
    protected double property3;
    
    public static TestProperty newInstance(int property1,
                                           long property2,
                                           double property3) {
        return new TestPropertyChild(property1, property2, property3);
    }
}

and, after suitable annotation processing, using the TestProperty type as in

public class Main {
    public static void main(String... args) {
        TestProperty testProperty = TestProperty.newInstance(1, 2, 3);
        output(testProperty);
        testProperty.setproperty2(42);
        output(testProperty);
    }

    private static void output(TestProperty testProperty) {
        System.out.format("%d, %d, %g%n",
                          testProperty.getproperty1(),
                          testProperty.getproperty2(),
                          testProperty.getproperty3());
    }
}

produces the expected output:

prompt$ java Main 
1, 2, 3.00000
1, 42, 3.00000

This approach does have limitations; primarily the annotated class like TestProperty needs to be written to allow its superclass and subclass(es) to be generated. Since it runs as part of the build, the annotation processor needs to be built separately beforehand. The javac command to run the annotation processor looks like:

javac -s ../gen_src/ -d ../bin -processor foo.PocProcessor -cp ../lib TestProperty.java Main.java 

Good practice sets an output location for generated source code, TestPropertyParent and TestPropertyChild in this case, separate from the output location for class files. When compiling this, javac emits an error before the superclass and subclass are generated, but the entire compilation process completes fine correctly generating the source files and compiling all the files together. Java IDEs have varying levels of support for annotation processing; check your IDEs' documentation for details.

The annotation type and processor is only a proof of concept; many possible refinements are left as "exercises for the reader" including:

  • Developing a second annotation type to mark a class separate from the annotation to configure how each field should be treated.

  • Additional structural checks on the annotated code, proper modifiers on fields and constructors, etc.

  • Generation of equals and hashCode methods.

While using an annotation processor to approximate properties is awkward compared to built-in language support, annotation processors can be used today as part of some toolchains and they are configurable by the user. The code provided should be enough of a starting part for others to experiment with using annotation processors in this fashion; have fun.

Comments:

Hello!
Good concept, except one thing: readOnly = false, is too verbose.
A differently-named annotation would be better.

@Property // default for non-final
@FinalProperty

Or the other way round

@Property // default for final
@ReadWriteProperty

Posted by Ivan on September 03, 2009 at 01:34 AM PDT #

Or, wait, a better proposal:

@Read // default is @Read(PUBLIC)
@Write(PROTECTED)
int writeMeOnlyIfYouKnowMe;

@Read
@Write
int writeMeAlawys;

Posted by Ivan on September 03, 2009 at 02:04 AM PDT #

combine these annotations with the ones from JPA to get a feel for what your code looks like. It ain't pretty! Add jsr-308 annotations to that and you have one heck of a mess. I agree with Joe that these are better served as language changes. Encouraging behaviour like this will get us into annotation hell. Does anybody remember doing xml pushups all day? Lets not repeat the same mistake with annotations.

Posted by joeJava on September 03, 2009 at 02:53 AM PDT #

Of course, lombok, being an agent, can (though doesn't yet) change grammar rules and add e.g. closures, if you want.

The support for apt is pretty bad in IDEs; eclipse is actually the best at it. In netbeans, nothing happens until you do a full build, and the same applies to IntelliJ. This means you need to do a full build after making any changes at all to annotated items, or any of the annotations themselves.

Posted by Reinier Zwitserloot on September 03, 2009 at 09:58 AM PDT #

The misuse of annotations is happening because the language itself does not offer a solution. There's precedence to this in the form of JPQL, where a DSL is buried in a type-unsafe string offering no syntax/formatting guidance with the underlying problem being unable to represent a type-safe expression tree in Java.

So while I could fear annotation hell, I think what Lombok is doing is no more a misuse than say JPQL. Java development already is heavily dominated by what IDE's are able to do.

Posted by Casper Bang on September 03, 2009 at 09:14 PM PDT #

@Reinier,

You are your Lombok compatriots deserve credit for a clever hack and some interesting experimentation!

However, there are a few orders of magnitude difference in scope between, say, rewriting a couple javac methods to always return "false" in order to get rid of checked exceptions and adding fundamentally new features to the language like closures, which would presumably require new data structures, etc. to be supported in a reasonably natural way.

It is certainly technically \*feasible\* to use an agent to adopt, say, the BGGA prototype's code or other large change into javac. However, for such pervasive changes I think direct implementation in a compiler is a more tractable technical approach.

Posted by Joe Darcy on September 04, 2009 at 03:42 AM PDT #

Creating a javac fork would certainly be easier, but, the sheer power of adding any and all new features one could possibly dream of simply by going:

javac -cp javaButBetter.jar MySourceUsingNewFeatures.java

is pretty convincing, and meshes well with ant, maven, and other build tools, which pretty much all make it trivial to add libraries to the classpath during compilation and make it relatively hard to use a different compiler executable.

For eclipse and other IDEs, the main draw is in not running either a hacked executable (as in, a completely separate install, which is hard to keep updated), or a module that redoes/fork most of the Java-Toolkit bits, causing a lot of incompatibilities between java.next and java code.

Thus, the old rock and a hard place situation.

Posted by guest on September 08, 2009 at 06:02 AM PDT #

Creating a javac fork would certainly be easier, but, the sheer power of adding any and all new features one could possibly dream of simply by going:

javac -cp javaButBetter.jar MySourceUsingNewFeatures.java

is pretty convincing, and meshes well with ant, maven, and other build tools, which pretty much all make it trivial to add libraries to the classpath during compilation and make it relatively hard to use a different compiler executable.

For eclipse and other IDEs, the main draw is in not running either a hacked executable (as in, a completely separate install, which is hard to keep updated), or a module that redoes/fork most of the Java-Toolkit bits, causing a lot of incompatibilities between java.next and java code.

Thus, the old rock and a hard place situation.

Posted by Reinier Zwitserloot on September 08, 2009 at 06:05 AM PDT #

Post a Comment:
Comments are closed for this entry.
About

darcy

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today
News

No bookmarks in folder

Blogroll