This Bugzilla instance is a read-only archive of historic NetBeans bug reports. To report a bug in NetBeans please follow the project's instructions for reporting issues.

Bug 150447 - @ServiceProvider annotation and processor
Summary: @ServiceProvider annotation and processor
Status: RESOLVED FIXED
Alias: None
Product: platform
Classification: Unclassified
Component: Lookup (show other bugs)
Version: 6.x
Hardware: All All
: P1 blocker (vote)
Assignee: Jesse Glick
URL:
Keywords: API
: 141678 (view as bug list)
Depends on: 147393 154958
Blocks: 150804
  Show dependency tree
 
Reported: 2008-10-16 22:28 UTC by Jesse Glick
Modified: 2009-02-19 22:53 UTC (History)
5 users (show)

See Also:
Issue Type: ENHANCEMENT
Exception Reporter:


Attachments
Example registration for openide.filesystems (3.35 KB, patch)
2008-10-17 18:02 UTC, Jesse Glick
Details | Diff
Proposed implementation (15.84 KB, patch)
2008-10-17 18:07 UTC, Jesse Glick
Details | Diff
Script to update registrations (3.53 KB, text/plain)
2008-11-03 18:04 UTC, Jesse Glick
Details
Updated script with several fixes (4.32 KB, text/plain)
2008-11-03 21:56 UTC, Jesse Glick
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Jesse Glick 2008-10-16 22:28:15 UTC
I would like to create an annotation and processor to handle META-INF/services registration in the format recognized by
our MetaInfServicesLookup and Lookups.forPath.
Comment 1 Jesse Glick 2008-10-16 22:46:30 UTC
Proposed signature:

@Target(ElementType.TYPE)
public @interface Service {
  Class[] value();
  int position() default Integer.MAX_VALUE;
  String[] supersedes() default {};
  String path() default "";
}
Comment 2 Jesse Glick 2008-10-17 18:02:43 UTC
Created attachment 72144 [details]
Example registration for openide.filesystems
Comment 3 Jesse Glick 2008-10-17 18:07:44 UTC
Created attachment 72145 [details]
Proposed implementation
Comment 4 Jaroslav Tulach 2008-10-18 06:18:05 UTC
Y01 I'd rather see the annotation next to Lookup then in module system. Maybe as org.openide.util.lookups.Service. It 
seems to me it is more lookup related than module system related.

Otherwise perfect, this is how our future should look like.
Comment 5 rmichalsky 2008-10-20 13:10:33 UTC
Looking forward to it as well. Couple of questions:

R01 When does annotation processor get called? During build?
R02 What about UI? I suppose we want to keep service nodes in Project, but adding a service via context menu should be
changed to add an annotation, right? Plus there should be context menu item "Register as service" on eligible classes. 
R03 (related to R01) Is it sufficient for Service Node to listen to META-INF/services as it currently does? In other
words when someone adds the annotation to a class, will META-INF/services be updated anytime soon?
Comment 6 Jesse Glick 2008-10-20 21:11:38 UTC
Y01 - reasonable, will do.

R01 - yes, during the build. If using JDK 6+ to build, this would be automatic, but to support building on JDK 5 for now
we need to do some tricks to run the JDK 6 javac; see issue #147393.

R02 / R03 - as part of the change I am deleting the META-INF/services GUI entirely. It was never very stable or fast,
and is now superfluous when you can declare a service directly from source code with no special metadata files.
Comment 7 rmichalsky 2008-10-21 11:00:49 UTC
Re deleting UI: not sure about 'superfluous', following the same reasoning Navigator window is also superfluous as you
can't add a method there :).
Service UI has never been especially useful for adding a service, but IMHO its primary purpose is to show what services
are exported from the module and what services will be present in the whole application (you see, I am a fan of those
"overall" views :-)). With annotations this kind of information is even more scattered over the sources.
Comment 8 Jesse Glick 2008-10-21 23:04:31 UTC
Showing services exported from a single module is I think not very useful. (Note that they are already displayed as
informational notes during the build.) Showing all services present in an application from a suite logical view might be
useful, though I am not sure of the exact use case. Remember you can find all registrations of a particular interface in
open module projects just using Find Usages; apisupport.refactoring could if desired add a provider which additionally
reports M-I/s/* registrations present in the binary platform corresponding to open module(s). And of course you can Find
Usages on @Service itself if you really wanted to.

In any case the previous GUI code is no longer worth much: the parts concerned with edit operations are obsolete, and
the display aspect would no longer work anyway because it would not see services defined in module projects. The same
considerations will also affect the XML layer node eventually, once most layer entries are generated from annotations.

There is also the general problem with overview nodes that it is very hard to make them both efficient and correct. For
correctness you need to listen for changes on a lot of different data sources, and this can quickly lead to leaks and
"event storms". Overview functionality which is produced as a static report upon request (e.g. Find Usages) does not
suffer from these problems - it takes however long it takes (and may be cancellable), and is accurate at the time it was
created.
Comment 9 brucechapman 2008-10-22 09:43:36 UTC
The proposed implementation is not safe in the context of incremental compiles.   For how to do that see
https://hickory.dev.java.net/nonav/apidocs/index.html?net/java/dev/hickory/incremental/package-summary.html. (Note that
the javadoc for the (*sole public) class in that package provides a sample implementation of @ServiceProvider.) There
are also a couple of other things about that example which are worth heeding - don't generate the output till processing
is complete (think what would happen if a different processor in the first round generated a class annotated with this
proposed annotation) and don't do anything if there are errors raised.

@Service is probably not the correct name. (see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6354623 for a related
earlier problem). According to http://java.sun.com/javase/6/docs/api/index.html?java/util/ServiceLoader.html this
proposed annotation identifies the "ServiceProvider". The "Service" is the abstract class or interface that is being
implemented so the member which specifies that should be called "service" (rather than value). It should rather be like this

@interface ServiceProvider {
    Class<?> service();
}

(note using "value" as a member name is probably a bad idea unless it is the only member)

from here on I'll use my suggested naming.

As an alternative to specifying the service with an annotation member each of the "Services" in neatbeans could be
annotated with a new marker annotation @Service then the processor for @ServiceProvider could look at the supertypes of
the target and determine the Service(s) automatically which saves the programmer from specifying them explicitly since
they are usually pretty obvious from inspection of the declared supertypes.

I wonder whether the META-INF/services portion is of such general value that it would be more valuable as part of the
JDK rather than hidden away inside NB? 

Are there any existing examples where a single class is a Provider for more than one Service? Can the complexity of
having multiple services specified (Class[] value() ) be justified? And if so, what if the position or supersedes 
values are different for the multiple services? Its actually all a bit ugly. The convention for dealing with possible
plurality is to introduce a collection annotation, for example

@interface ServiceProviders {
    ServiceProvider[] value();
}

would provide a cleaner mechanism if a class is a ServiceProvider for more than one service allowing each to have a
different position etc.

The 'String path() default "";' member is unrelated to META-INF/services and I can't see how 'String[] supersedes()
default {};' can be implemented for META-INF/services. Maybe these are two different things and maybe there should be
two different annotations, one for {@link Lookups#forPath}and one for java.util/ServiceLoader / {@link
Lookup#getDefault} to separate the concerns. Mixing them is somewhat confusing.

The javadoc for 'int position() default Integer.MAX_VALUE;' (at least for the META-INF/services case) should be a bit
more explicit about the ordering being only relative to other service providers for the same service in the same module.
(that is it only controls the ordering in one META-INF/services file. Nothing in the META-INF/services mechanism
provides for specifying any absolute ordering between different jar files, they are searched in classpath order. 
Comment 10 Jesse Glick 2008-10-22 17:00:32 UTC
Bruce, thank you for the detailed review! I will try to address your points in turn.


First of all, yes it would be great for an annotation like this to be defined in the Java platform, but we cannot wait
for years for a new JDK incorporating such an API to be released (and then years more for NB to be able to assume that
all its users are using at least that version of the Java platform). Anyway NB's Lookups.metaInfServices defines
extensions to the ServiceLoader behavior (e.g. positions) which would not be supported by a Java platform annotation.


I agree that @ServiceProvider is likely a better name. Regarding the use of the special attribute name "value", you may
be right that this should not be used in case there are other optional attributes (as indeed there are), though I will
point out that at least javax.annotation.Generated violates this convention already. "service" is a reasonable name for
the attribute.


I like your suggestion of @ServiceProviders({@ServiceProvider(...), @ServiceProvider(...)}) for dealing with multiple
registrations from a single class. To answer your question, yes there are cases where this would be used, e.g.
org.netbeans.modules.java.project.UnitTestForSourceQueryImpl is registered as both a
org.netbeans.spi.java.queries.MultipleRootsUnitTestForSourceQueryImplementation and a (deprecated)
org.netbeans.spi.java.queries.UnitTestForSourceQueryImplementation.


The suggestion of marking services with @Service and then inferring the service name in a @ServiceProvider by inspection
of its supertypes has come up before. For now I think this is not a good idea, for these reasons:

1. It would interact poorly with @ServiceProviders, especially if you wanted separate positions for each supertype.

2. It would gratuitously prevent you from registering a provider for a service which has long been defined but which has
not yet been marked with @Service.

3. It would violate the general principle that the behavior of a class should not be modified merely by implementing a
new interface.

4. To someone reading source code (a much more important audience than the person writing it!) "@ServiceProvider" does
not say what is really happening (you need to think for a moment about what interface is involved), whereas
"@ServiceProvider(service=Something.class)" says it very clearly. This is especially important when the provider does
not directly declare that it implements the interface but goes through an intermediate base class.

5. Having an explicit declaration of the service in the source code makes it easier for tools (other than the AP) to
generate summaries of service providers, and lets Find Usages jump to all providers of a given service.

#1-#3 could be mitigated by retaining a "service" attribute but making it optional when exactly one supertype is a
declared @Service. I consider #4 the most relevant point: too much "magic" makes code harder to follow, and in this case
it is not making the code significantly shorter anyway.


To the question of incremental compilation:

The proposed implementation already does handle incremental compilation, though not for 269-generated sources (which we
anyway do not use in NB yet): it deals with multiple types being registered under one service within the same job, and
also with a M-I/s/* file already present in the class output area from a previous job (which will be appended to,
similar entries being replaced). I learned to do this kind of trick independently in sezpoz.dev.java.net but I am glad
to see there is a generic infrastructure for it now available (though for reasons of dependency minimization we probably
cannot use it as such in NB). I will try to modify the processor to abort writing if there is an error condition, and to
defer writing until the last round in order to accommodate annotated generated sources.

See issue #149136 for further discussion on the topic of processor lifecycle. I find the 269 API to be maddening in that
it is close to impossible for multiple processors to cooperate on writing the same class output file (thus the
WeakHashMap<ProcessingEnvironment,...> hack in that implementation). In the case of @ServiceProvider this is not such a
big deal because it can be expected (hoped?) that there is one and only one processor generating M-I/s/* files.

Another technical question relates to incremental compilation: 269 does not state clearly whether it is legal for a
processor to overwrite a class output resource file which existed before the job started (e.g. from a previous run). It
is specific that the AP cannot write to the file twice in a job, but also says that inputs are considered to have been
generated by a primordial round. In fact JDK 6+ javac permits this whereas APT does not. The processor in this issue (as
well as the processor base class in #149136) absolutely requires this to work, as otherwise it would be impossible to
support routine incremental compilation. (Note that if you delete an annotated source file and do an incremental build,
the annotation will remain - but then again, so will the class file. As is usual, after deleting things you need to
remember to do a clean build of the module. I cannot think of any way to fix this.)


The "path" attribute just modifies the registration to be in META-INF/namedservices/$path/ rather than
META-INF/services/ and can be used with our Lookups.forPath. Other than the existence of the path, the behavior of the
service provider registration is identical, so I think using the same annotation is appropriate.

The "supersedes" attribute is orthogonal; again this implements a NB-specific extension to the service provider
mechanism. One module can hide another's provider.

The "position" attribute does indeed specify a global ordering across modules. Again this relies on a NB-specific extension.
Comment 11 Jesse Glick 2008-10-30 21:51:41 UTC
*** Issue 141678 has been marked as a duplicate of this issue. ***
Comment 12 Jesse Glick 2008-11-01 16:51:30 UTC
Merged in core-main #78103b644dfc.
Comment 13 Jesse Glick 2008-11-01 21:22:37 UTC
I am in the process of updating all main & contrib M-I/s/* registrations to use @ServiceProvider. This is not trivial as
there are a lot of mistakes and special cases in our source base, hidden in the middle of hundreds of normal correct
registrations.
Comment 14 Jesse Glick 2008-11-03 18:04:17 UTC
Created attachment 73147 [details]
Script to update registrations
Comment 15 Jesse Glick 2008-11-03 21:56:51 UTC
Created attachment 73160 [details]
Updated script with several fixes
Comment 16 Jesse Glick 2008-11-04 01:09:54 UTC
Using annotation in core-main #78da8804b135.
Comment 17 Jesse Glick 2008-11-04 01:34:46 UTC
Also in contrib #0b9b6af17703.
Comment 18 Quality Engineering 2008-11-04 16:22:59 UTC
Integrated into 'main-golden', will be available in build *200811041401* on http://bits.netbeans.org/dev/nightly/ (upload may still be in progress)
Changeset: http://hg.netbeans.org/main/rev/d822d0d40c42
User: Jesse Glick <jglick@netbeans.org>
Log: #150447: @Service annotation.
Comment 19 arittner 2008-11-05 10:52:09 UTC
What is about performance? 

The manifest-files an the layer.xml files are well known files in the JARs.

For annotations you need to scan all the classes to detect the ServiceProvider-annotation. The boot performance of
modules is a very critical section. With annotations we need(?) a additional register cache or we can wait a long time
for booting netbeans.

Hibernate lives with the same problem. Many JARs results in a very poor boot startup. In Hibernate you can exclude jars
from scanning JPA annotations. 

josh.
Comment 20 brucechapman 2008-11-05 20:08:40 UTC
Josh,

No runtime preformance difference at all.  The annotations are processed at COMPILE TIME by an annotation processor
which generates the META-INF/services/service.xxx file.


Bruce
Comment 21 arittner 2008-11-05 21:24:35 UTC
Bruce, thank you for the clarification. josh.