Úvod
Zatiaľ čo reflexia umožňuje pracovať s procesom — bežiacim programom, na prácu s jeho zdrojovým kódom potrebujeme využiť odlišné techniky.
Syntaktická analýza
Zdrojový kód programov má väčšinou podobu textu, preto na prácu s nim potrebujeme využiť techniky syntakticnej analýzy a spracovávania formálnych jazykov. Tie umožňujú zistiť štruktúru textu na základe gramatiky jazyka.
Ako už viete z iných predmetov, syntaktický analyzátor môže byť implementovaný manuálne, na základe jedného z mnohých existujúcich algoritmov.
Môžete tiež použiť generátor syntaktických analyzátorov, ktorý na základe difinície gramatiky, automaticky vygeneruje syntaktický analyzátor. Príkladmi takýchto generátorov sú YACC, Bison, ANTLR a mnohé ďalšie. Tieto nástroje sú univerzálne a dokážu spracovať rôzne jazyky, ak ich vieme opsísať pomocou bezkontextovej gramatiky.
V prípade mnohých jazykov už môžu existovať hotové implementácie syntaktického analyzýtora, ktoré môžeme tiež využiť. Môže ísť o analyzátor, ktorý používa priamo prekladač alebo interpretátor daného jazyka. Môže tiež ísť o samostatný nástroj, ako je JavaParser pre jazyk Java, ktorý sa špecializuje nielen na analýzu, ale umožňuje aj modifikovanie kódu.
Anotačný procesor
Jednou z techník analýzy kódu, ktorá je podporovaná štandardnými nástrojmi jazyka Java je anotačný procesor.
Anotačný procesor je možné vnímať ako plug-in do prekladača Javy, ktorý je možné integrovať do procesu prekladu. Anotačný procesor má možnosť analyzovať štruktúru prekladaných tried a na základe toho vykonávať nejaké akcie. Nie je pritom možné modifikovať existujúci kód, anotačný procesor však môže generovať zdrojové kódy nových tried. Tieto triedy sú tiež preložené a stanú sa súčasťou výsledného programu.
Keďže výsledkom spustenia procesorov môžu byť nové zdrojové súbory a tie môžu tiež obsahovať anotácie, spúšťanie procesorov prebieha v niekoľkých kolách (rounds). Preto je pri implementácii procesora potrebné rátať s tým, že bude spustený viac krát.
Implementácia
Pre implementáciu vlastného anotačného procesora je potrebné vytvoriť triedu implementujúcu roznranie Processor. Najjednoduchšie je oddediť od triedy AbstractProcessor. Potom potrebujete implementovať hlavne tieto metódy:
void init(ProcessingEnvironment processingEnv)
boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
Metóda init
je zavolaná raz po vytvorení procesora a umožňuje inicializovať procesor. Metóda process
je volaná v každom kole spracovania a dostáva ako argument množinu anotácií, ktoré sú podporované procesorom a vyskytli sa v kóde. Na označenie toho, ktoré anotačné typy naš procesor podporuje môžeme buď implementovať metódu Set<String> getSupportedAnnotationTypes()
alebo použiť anotáciu SupportedAnnotationTypes.
Model jazyka
V anotačnom procesore máme prístup k štruktúre prekladaných tried pomocou špeciálneho API definovaného v module java.compiler. Toto API je založené na princípoch zrkadiel (mirrors), o ktorých sme hovorili skôr. Nevýhodou je to, že sa výrazne odlišuje od API reflexie. Dôvodom je nielen to, že musí byť prispôsobené fungovaniu počas prekladu, ale aj lepšie prispôsobenie novšim verziám jazyka Java.
Základom API je rozhranie Element a jeho podtypy. Štruktúra kódu je reprezentovaná stromom elementov implementujúcich dané rozhranie. Typ elementu je možné zistiť pomocou metódy getKind() a následne pretypovať na špecifickejší podtyp, ktorý poskytuje viac metód pre daný druh elementov. Napríklad
- TypeElement pre triedy a rozhrania,
- ExecutableElement pre metódy
- VariableElement pre členské premenné.
Ďalšie zaujímavé rozhrania sú TypeMirror reprezentujúce typové definície v programe, a tiež AnnotationMirror reprezentujúci anotácie.
Detaily implementácie pocesora
Anotačný procesor môže využívať pri svojej práci dva objekty prostredia: ProcessingEnvironment a RoundEnvironment.
ProcessingEnvironment je argumentom metódy init a v prípade, že dedíte od AbstractProcessora, máte ho uložené v členskej premennej processingEnv. ProcessingEnvironment poskytuje pomocou svojich metód viacero užitočných nástrojov
Messager getMessager()
— objekt pre vypisovanie hlásení počas prekladu (napríklad na implementáciu vlastných varovaní alebo chýb prekladu);Filer getFiler()
— objekt pre vytváranie súborov (napríklad pre generovanie kódu);Elements getElementUtils()
— objekt s pomocnými metódami pre manipuláciu s elementmi jazyka.
Druhým prostredím je RoundEnvironment, ktoré je argumentom metódy process. Jeho najzaujímavejšou metódou je getElementsAnnotatedWith, ktorý vráti všetky elementy so zadanou anotáciou.
Poznámka
Pri vývoji aplikácie sa štandardne prekladajú iba tie triedy, ktoré sa zmenili od posledného prekladu. Takže pokiaľ budete vyvíjať váš anotačný procesor, nezabudnite vymazať výsledky predchádzajúceho prekladu pred jeho testovaním. Napríklad pomocou takéhoto Maven príkazu:
mvn clean package
Štandardný model jazyka používaný v anotačnom procesore neposkytuje prístup k telám metód. Na prístup k zdrojovému kódu metód sa dá využiť samostatné Compiler Tree API definované v module jdk.compiler. Toto API poskytuje aj triedu Trees prepájajúcu obidve API.
Registrácia
Na to, aby ste zapli použitie vášho anotačného procesora pri preklade, je potrebné ho registrovať. Registrácia spočíva v uvedení úplného názvu triedy procesora v špeciálnom súbore:
META-INF/services/javax.annotation.processing.Processor
Aby bol súbor umiestnený na takejto ceste vo výslednom JAR archíve, je potrebné ho v rámci Maven projektu umiestniť do src/main/resources
. Celá cesta súboru v projekte teda bude
src/main/resources/META-INF/services/javax.annotation.processing.Processor
Poznámka
Pozor na to, že IntelliJ IDEA zobrazuje časť cesty META-INF/services
s bodkou ako oddeľovačom adresárov — META-INF.services
. Je to len vec zobrazenia v strome projektu a v skutočnosti je adresár services
vnorený v adresári META-INF
.
ServiceLoader
Registrácia anotačného procesora je len špeciálnym prípadom použitia ServiceLoader API. To slúži na vyhľadanie v CLASSPATH implementácií zadaného rozhrania. Toto API je veľmi užitočné pre systémy, ktoré je možné dynamicky rozširovať, napríklad pomocou pluginov. Systém definuje rozhranie pre pluginy a potom pomocou ServiceLoaderu vyhľadá všetky registrované implementácie tohto rozhrania.
Literatúra
- JavaDoc: Interface Processor
- H. Dorfmann. Annotation Processing 101
- JavaDoc: ServiceLoader