Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AnnotationProcessor using multiple source-files to create one file

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();
like image 879
Poidi Avatar asked Nov 15 '12 12:11

Poidi


2 Answers

-- 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.

like image 68
visola Avatar answered Nov 16 '22 09:11

visola


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.

like image 41
Jaroslav Tulach Avatar answered Nov 16 '22 09:11

Jaroslav Tulach