16

I have two classes with methods and i want to combine the methods of the two classes to one class.

@Service("ITestService")
public interface ITest1
{
   @Export
   void method1();
}

@Service("ITestService")
public interface ITest2
{
   @Export
   void method2();
}

Result should be:

public interface ITestService extends Remote
{
  void method1();
  void method2();
}

The first run of my AnnotationProcessor generates the correct output (because the RoundEnvironment contains both classes).

But if I edit one of the classes (for example adding a new method), the RoundEnviroment contains only the edited class and so the result is follwing (adding newMethod() to interface ITest1)

public interface ITestService extends Remote
{
  void method1();
  void newMethod();
}

Now method2 is missing. I don't know how to fix my problem. Is there a way (Enviroment), to access all classes in the project? Or is there another way to solve this?

The code to generate the class is pretty long, so here a short description how i generate the class. I iterate through the Elements with env.getElementsAnnotatedWith(Service.class) and extract the methods and write them into the new file with:

FileObject file = null;
file = filer.createSourceFile("com/test/" + serviceName);
file.openWriter().append(serviceContent).close();
Poidi
  • 161
  • 1
  • 5
  • Are you running this annotation processor in Eclipse? – John Ericksen Feb 14 '13 at 04:13
  • 2
    @johncarl's point is important and must be true. The standard Java compiler does not allow incremental compilation. There is no way that RoundEnvironment could contain only a single file. The Eclipse compiler is incremental and only compiles files that have changed. It seems this logic does not work for you and you will need someway to indicate to Eclipse that a given file must always be recompiled. There are probably some things we can try there but first, to avoid wasted effort, we should be certain that this only affects Eclipse compilation. – Pace Dec 17 '13 at 17:10
  • 1
    @Pace: Is it specified somewhere that javac does never uses incremental compilation? I think ant and maven also have modes for incremental compilation, so I guess they also would not work correctly with such an annotation processor. – Jörn Horstmann Dec 18 '13 at 09:41
  • 1
    javac [does not support incremental compilation](http://stackoverflow.com/questions/2590579/can-standard-sun-javac-do-incremental-compiling). One can implement incremental compilation on top of javac, which is what Ant does and maybe Maven. Eclipse doesn't actually use javac, it uses ecj, the Eclipse Java Compiler. Each of these is going to implement the rules to handle incremental compilation differently. One could create an alias to javac or a custom CompilerAdapter to fool Ant, but that wouldn't fool Eclipse. Eclipse may allow control over incremental builds, but that doesn't fool Ant. – Pace Dec 18 '13 at 13:42

2 Answers2

9

-- Option 1 - Manual compilation from command line ---

I tried to do what you want, which is access all the classes from a processor, and as people commented, javac is always compiling all classes and from RoundEnvironment I do have access to all classes that are being compiled, everytime (even when no files changed), with one small detail: as long as all classes show on the list of classes to be compiled.

I've done a few tests with two interfaces where one (A) depends on the (B) other (extends) and I have the following scenarios:

  1. If I ask the compiler to explicitly compile only the interface that has the dependency (A), passing the full path to the java file into the command line, and adding the output folder to the classpath, only the interface I passed into the command line gets processed.
  2. If I explicitly compile only (A) and don't add the output folder to the classpath, the compiler still only processes interface (A). But it also gives me the warning: Implicitly compiled files were not subject to annotation processing.
  3. If I use * or pass both classes to the compiler into the command line, then I get the expected result, both interfaces gets processed.

If you set the compiler to be verbose, you'll get an explicity message showing you what classes will be processed in each round. This is what I got when I explicitly passed interface (A):

Round 1:
input files: {com.bearprogrammer.test.TestInterface}
annotations: [com.bearprogrammer.annotation.Service]
last round: false

And this is what I've got when I added both classes:

Round 1:
input files: {com.bearprogrammer.test.AnotherInterface, com.bearprogrammer.test.TestInterface}
annotations: [com.bearprogrammer.annotation.Service]
last round: false

In both cases I see that the compiler parses both classes, but in a different order. For the first case (only one interface added):

[parsing started RegularFileObject[src\main\java\com\bearprogrammer\test\TestInterface.java]]
[parsing completed 15ms]
[search path for source files: src\main\java]
[search path for class files: ...]
[loading ZipFileIndexFileObject[lib\processor.jar(com/bearprogrammer/annotation/Service.class)]]
[loading RegularFileObject[src\main\java\com\bearprogrammer\test\AnotherInterface.java]]
[parsing started RegularFileObject[src\main\java\com\bearprogrammer\test\AnotherInterface.java]]

For the second case (all interfaces added):

[parsing started RegularFileObject[src\main\java\com\bearprogrammer\test\AnotherInterface.java]]
...
[parsing started RegularFileObject[src\main\java\com\bearprogrammer\test\TestInterface.java]]
[search path for source files: src\main\java]
[search path for class files: ...]
...

The important detail here is that the compiler is loading the dependency as an implicit object for the compilation in the first case. In the second case it will load it as part of the to-be-compiled-objects (you can see this because it starts searching other paths for files after the provided classes are parsed). And it seems that implicit objects aren't included in the annotation processing list.

For more details over the compilation process, check this Compilation Overview. Which is not explicitly saying what files are picked up for processing.

The solution in this case would be to always add all classes into the command for the compiler.

--- Option 2 - Compiling from Eclipse ---

If you are compiling from Eclipse, incremental build will make your processor fail (haven't tested it). But I would think you can go around that asking for a clean build (Project > Clean..., also haven't tested it) or writing an Ant build that always clean the classes directory and setting up an Ant Builder from Eclipse.

--- Option 3 - Using build tools ---

If you are using some other build tool like Ant, Maven or Gradle, the best solution would be to have the source generation in a separate step than your compilation. You would also need to have your processor compiled in a separated previous step (or a separated subproject if using multiprojects build in Maven/Gradle). This would be the best scenario because:

  1. For the processing step you can always do a full clean "compilation" without actually compiling the code (using the option -proc:only from javac to only process the files)
  2. With the generated source code in place, if you were using Gradle, it would be smart enough to not recompile the generated source files if they didn't change. Ant and Maven would only recompile the needed files (the generated ones and that their dependencies).

For this third option you could also setup an Ant build script to generate those files from Eclipse as a builder that runs before your Java builder. Generate the source files in some special folder and add that to your classpath/buildpath in Eclipse.

visola
  • 7,493
  • 1
  • 17
  • 21
  • Nice answer, if you could find some official link where it says javac does no incremental compilation you've earned the bounty. – Jörn Horstmann Dec 20 '13 at 08:18
  • I don't think the spec requires the compiler to be incremental or not. It seems to be a decision for the compiler implementor. In the Filer javadoc (http://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/Filer.html) there's even a citation about incremental or non-incremental, not making it explicit but giving a hint that it's really up to the implementation. "This information may be used in an incremental environment to determine the need to rerun processors or remove generated files. Non-incremental environments may ignore the originating element information." – visola Dec 20 '13 at 21:35
  • @JörnHorstmann After reading a lot about it, my conclusion is that Java is just about the language and the JVM. There's no spec on the compiler. The spec only explains the format of the bytecode. If you want to generate the bytecode by hand and process the annotations using a notebook and a pen, it's fine! As long as you respect the API for annotation processing and generate the correct bytecode format at the end. So it's really up to the implementation to be incremental or not. A simple example is the Eclipse compiler, which also generate standard bytecode but is incremental. – visola Dec 21 '13 at 14:52
  • It's probably also worth pointing out that incremental compilation is usually not the compiler's responsibility. The other major compilers out there (G++/gcc/etc.) also don't support incremental compilation themselves, that is supported by tools like make. – Pace Dec 22 '13 at 21:23
3

NetBeans @Messages annotation generates single Bundle.java file per all classes in the same package. It works correctly with incremental compilation thanks to following trick in the annotation processor:

Set<Element> toProcess = new HashSet<Element>();
for (Element e : roundEnv.getElementsAnnotatedWith(Messages.class)) {
  PackageElement pkg = findPkg(e);
  for (Element elem : pkg.getEnclosingElements()) {
    if (elem.getAnnotation(Message.class) != null) {
      toProcess.add(elem);
    }
  }
}
// now process all package elements in toProcess 
// rather just those provided by the roundEnv

PackageElement findPkg(Element e) {
  for (;;) {
    if (e instanceof PackageElement) {
      return (PackageElement)e;
    }
    e = e.getEnclosingElement();
  }
}

By doing this one can be sure all (top level) elements in a package are processed together even if the compilation has only been invoked on a single source file in the package.

In case you know where to look for your annotation (top level elements in a package or even any element in a package) you should be able to always get list of all such elements.

Jaroslav Tulach
  • 519
  • 4
  • 7
  • This does not work for me, the first loop pkg.getEnclosedElements() returns the elements but there the annotation is only returned by the element being compiled, the other getAnnotation return null though they have it defined on the class. (I checked the annotation processor in the link above and have added if (roundEnv.processingOver()) return false;) edit: I run this via javac – RookieGuy Oct 21 '15 at 15:07