Using an annotation processor to enforce Java-like access rules

Sometimes you need to expose a method from one class to another class, but do not wish to make this method public for anyone to call (because you are not prepared to maintain it compatibly forever). If they happen to be in the same package, you can just use package-private access. But this does not work if they are not in the same package.

Some module systems - NetBeans, OSGi - let you declare that some packages or classes are not to be accessed outside of the declaring module, even if marked public. The FriendPackages pattern can be useful in case you want to expose your method to a class in another package so long as that other package is private and is in the same module. But there are many cases where you want to make a method accessible to a particular caller in another module.

Ideally you could just use something like Java's public keyword - i.e., an annotation. But what would pay attention to that annotation? Well, an annotation processor. (At least at compile time; runtime access checks are another matter, but anyone using reflection probably knows how to call setAccessible anyway, so runtime checks would only be useful in security sandboxes.)

If you are compiling using JDK 7, the new AbstractTypeProcessor class in the Tree API lets you write annotation processors which not only can examine the full source tree, but get access to type information - such as what the receiver type of a method call is. This is critical.

I have started to prototype an annotation @Public which can be placed on a nominally public member to declare that it should be visible only to the enumerated classes; and an accompanying annotation processor: PublicProcessor By placing the JAR containing the annotation and its processor in your classpath, and compiling with JDK 7+ javac, you can have a new access modifier intermediate between package-private and public!

Comments:

Can you please expand on those last two paragraphs? Does this technique somehow protect your code from third-parties who download your "protected" library (e.g. an API jar)? In other words, how do you enforce that another person will install your PublicProcessor when they compile their code which references your library? (Or am I horribly confused about your intended purpose? ;-)

Posted by Chris on February 17, 2010 at 04:40 PM EST #

Please, give a code example.

Posted by Alexander Volkov on February 17, 2010 at 07:58 PM EST #

The context Jesse is referring to is the one of the NetBeans platform / OSGi, where there's a container which properly manages the classloaders and provides the augmented access control. Both platforms rely on specific information in the MANIFEST.MF for handling "friend packages"; I suppose that the annotation processor Jesse is working on is just a generator of the proper information in the MANIFEST.MF, which is usually a bit annoying to be manually written.

Too bad I'll have to wait for 1.5 years before using it :-)

Posted by Fabrizio Giudici on February 17, 2010 at 09:19 PM EST #

@Chris: you cannot force a third party to run the AP. But if they run javac, it will be run by default, since it is registered in META-INF/services: they would have to pass e.g. -proc:none to disable it. If they are using Eclipse, APs in the classpath are not run by default <https://bugs.eclipse.org/bugs/show_bug.cgi?id=280542> but this AP would not work in Eclipse anyway (since it relies on javac-specific APIs).

@Fabrizio: no, NetBeans "friend packages" (not sure if Equinox has something similar; official OSGi does not) are strictly weaker. That facility lets you declare that a whole package is visible to only certain other modules. The @Public annotation lets you control access to an individual class or member (e.g. method) and export it only to a particular caller (or perhaps wildcarded set of callers etc.). It is independent of friend packages and implemented inside the compiler, not by an external module system.

For example, I might have an API which I expect to be widely used and very stable, such as <http://bits.netbeans.org/dev/javadoc/org-openide-util/org/openide/util/NbPreferences.html>. For reasons of code separation I wish to put its implementation in a separate module which will communicate using an SPI such as <http://bits.netbeans.org/dev/javadoc/org-openide-util/org/openide/util/NbPreferences.Provider.html>. But for the moment, there is only one known implementation of that SPI, and I would rather not yet commit to making only compatible changes to it forever after. So I would like to mark it @Public("the.expected.NbPrefsProviderImpl") so only that class can implement it until further notice. Without language support, the best you can do is put a warning in Javadoc, or use cumbersome tricks like throwing an IllegalArgumentException from its constructor if getClass().getName() has an unexpected value.

You can of course debate whether it is wise to ever make such quasi-public APIs, and OSGi intentionally does not offer such a facility. (You can put mandatory constraints on package exports, which makes callers do something special to import your API, but you still have no clue who might be using it.) But I have found that the need seems to come up pretty frequently in real code evolution, as you incrementally refactor your system. In the long run you would want to decide that one design is right, define an official API, expose it to any caller, and abstain from making potentially incompatible changes. @Public can be used before you are sure what design is right.

If you run the 'test' target of the prototype project on BitBucket using JDK 7, you can see the compiler reporting an error for one forbidden method call ("p/User2.java:2: Cannot access p.PartAPI.m2"). Currently the processor only understands basic method calls and the like; it needs to be improved to handle constructors and probably other special cases.

An open question is whether it is possible to run such a processor in JDK 6 javac. Probably AbstractTypeProcessor could be backported somehow (it does not seem to work unmodified in JDK 6). Ideally the processor would load successfully using only 269 APIs, then check for the required javac APIs and conditionally continue - this would let it at least display a polite warning when run in Eclipse. (Writing an Eclipse-compatible port using JDT is left as an exercise to the reader.)

By the way, JSR 294 (in JDK 7?) is expected to define an access modifier 'module' which would let you make a member accessible only inside a module (typically one source tree). While this access mode would certainly be the preferred approach for this case when it is available (official language support implemented by all compilers and double-checked by the runtime), it would not be applicable when you want to export the member selectively to a class in another module.

Posted by Jesse Glick on February 18, 2010 at 12:59 AM EST #

Thanks Fabrizio, I wasn't aware of META-INF/services. I'm looking forward to investigating that feature.

Posted by Chris on February 18, 2010 at 01:27 AM EST #

Post a Comment:
  • HTML Syntax: NOT allowed
About

jglick

Search

Categories
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