While processing an annotation, I want to get all the subclass/subtypes of an element.
The element will be a Movie
so I would like to get Elements or TypeMirrors of TerrorMovie
and ComedyMovie
.
interface Movie {}
class TerrorMovie implements Movie {}
class ComedyMovie implements Movie {}
Getting the superType is easy as I can use the Types.directSupertypes()
but how can I get the subtypes?
Thanks
CodePudding user response:
You can't. You can work around it though. You need to understand the significant limitations fundamental to the question 'We are compiling class X; can I get a list of all subtypes of this class?', because otherwise you're going to shoot yourself in the foot. Then I'll get to the workarounds.
The issue is, a superclass is fine. After all, this:
class Bob extends Alice {}
is not even going to compile if Alice
is not available either as source file or as compiled class file when the compiler attempts to compile Bob
. Therefore, asking the annotation processing (AP) tools to give you a TypeMirror
object representing Alice
is fine. It'll be there.
However, given that somewhere else in the codebase, class Carol extends Bob {}
exists, is irrelevant, and if neither that source file nor that class file is around, Bob.java
still compiles just fine. That's why the AP tools API doesn't give you a .getSubtypes()
method. Because the docs would have to say something like this:
/**
* Finds all subtypes that are mirrorable in the current compilation context
* <b>but this does not do what you think it does and will miss a ton of classes,
* please read a page worth of caveats or prepare to shoot your foot off</b>:
*
* <ul><li>loooong list of caveats here</li></ul>
*/
Whereas the same list of caveats aren't necessary for getSupertypes()
, because the cases where supertypes cannot be found are rare, are going to stop compilation anyway, and can thus don't apply (APs don't run when compilation errors occur, or if they do, it's not likely to be important that they can't do the job right).
You have 2 crucial issues:
In any case, 'search the universe is impossible'
I can write a class that extends AbstractList. I can do it right now: class ReiniersList extends AbstractList<String> {}
. That was easy. When Team OpenJDK compiles AbstractList.java
, of course ReiniersList
cannot be returned when they run an AP that calls a hypothetical .getSubtypes()
.
So, you need to figure out what kind of subtype do you want. "Anywhere in this package?" "Anywhere in the source and classpath?" That last one is impossible - the classpath abstract simply does not support a 'list' directive. And yet that's probably what you intended to do here. The best option, in the sense that it really is the only thing that the AP API can meaningfully give you, is 'please give me all subclasses of this class that are part of the current compilation run, and only in source file form'. In other words, if you run javac *.java
in a package and this hits AbstractFoo.java
, FooImpl1.java
, and FooImpl2.java
, then asking 'getSubtypes()of the TypeMirror representing
AbstractFoo, you would be able to get
FooImpl1and
FooImpl2` typemirrors. That's possible.
The problem is, programming teams split projects, and other code that doesn't even exist yet could extend AbstractFoo
5 years from now and you can't get those in the list. You're going to have to make crystal clear what you actually 'mean' when you ask for subtypes, and then check if that definition is something that the AP tools can even provide for you.
Inners
method-local classes can extend a type but aren't part of the type mirror infrastructure. They cannot have an impact on type resolution. This is a rarely used java feature, perhaps you don't know what I'm talking about. It's this:
class Example {
void foo() {
class MethodLocalClass extends AbstractList<String> {}
}
}
is legal java, actually. Yes, I stuck a class def in the middle of a method declaration. Outside of the method decl this class is utterly invisible. You can't write Example.MethodLocalClass
anywhere, except inside foo()
. Because of this, this class does not exist at annotation processing time, effectively. You can't ask for it. It has no accessible fully qualified name. Nevertheless, it could be a subclass. Point is, getSubtypes()
cannot return it, ever. If that is a problem, then you can't do this at all. Hopefully, you're okay with this.
Incremental compilation
That problem of 'search the universe is impossible' takes on an acute role in making what you want completely impossible, at least as far as the AP tools are concerned, when you factor in that most compilation is done incrementally: Only the source files that need recompilation (because they were modified) are recompiled. That means if you have AbstractFoo
, FooImpl1
and FooImpl2
all in the same package, and you edit AbstractFoo and FooImpl1, but don't touch FooImpl2, and then save and recompile, your compiler infra (be it an IDE, or maven, or gradle, or some other build tool) is rather likely to compile only AbstractFoo
and FooImpl2
which means it is not possible for the infra to give you FooImpl1.
Even though FooImpl1
is in the same project (in the same package, even!). And there is no way to ask: It's simply not in the 'source set' of the compiler, and the classpath does not support a 'list' directive (where FooImpl2.class
lives - after all, that's why it's not part of the source set, it already exists and does not need to be recreated).
Solutions
The main solution is to do a lot of bookkeeping yourself:
First, understand that you cannot ever get a list of stuff that lives outside your project. Only files that, on a complete clean recompilation run, would be touched, can be found. If it's in a different project built by the same team, you can find it, but stuff others write, or stuff that will be written in the future, you can never find that. Ensure you don't need these, or we're done.
Bookkeep every class you touch. Make a text file and write this (using the Filer, you can write non-java files too) into the output directory, listing all types you 'touched'. Then, on init, read this file, and for each class in it, ask the type tools for this type. This 'solves' the "classpaths cannot list" problem: Now you're not 'list all types and find me every type that extends X', now you are simply asking: "Please give me a TypeMirror representing class X, get it from the source path or the class path, and tell me what types that type extends". Crucially, that second thing is something the AP infra can do. You then do a quick 'cache clear' - whatever classes do not exist anywhere, you now know - I guess someone deleted that file. For everything that does exist, then that is one of the subtypes, though they might not be part of this compilation 'source set'.
You read the file during init, then you go through the rounds, extending this Set<String>
(representing fully qualified types that are subtypes, automatically getting rid of duplicate names), and then on the last round (when roundEnv.isLastRound()
is true), you check each type if it still exists and is a subtype of what you intended to be. And now you have your list of subtypes. Write this pruned list back for future runs, and do whatever you want to do with 'you now have a list of type names that extend the type you are interested in'.
I'm not aware of any libraries that can help you with this. It's not too hard to write it yourself.
This principle of 'write a file that contains, per line, the fully qualified type of some relevant type' smacks a lot of the SPI system, but crucially this one runs during compilation whereas SPI is designed as a runtime discovery system.