Analýza a transformace kódu psaného v Javě s využitím knihovny Spoon

29. 9. 2016
Doba čtení: 19 minut

Sdílet

Knihovnu Spoon je možné použít v případě, že je zapotřebí analyzovat zdrojové kódy psané v Javě, hledat v nich typické příznaky chyb či kódy dokonce programově modifikovat.

Obsah

1. Analýza a transformace kódu psaného v Javě s využitím knihovny Spoon

2. Lokální instalace a základní otestování knihovny Spoon

3. Grafické zobrazení abstraktního syntaktického stromu zdrojových kódů

4. Vytvoření a překlad velmi jednoduchého procesoru zdrojového kódu

5. Otestování procesoru zdrojového kódu

6. Procesor zjišťující pozice elementů ve zdrojovém kódu

7. Otestování druhého procesoru

8. Procesor zjišťující všechny deklarované třídy

9. Otestování třetího procesoru

10. Procesor detekující zápisy do proměnných

11. Otestování čtvrtého procesoru

12. Zdrojové kódy všech procesorů i testovaných příkladů

13. Použité třídy a rozhraní knihovny Spoon

14. Obsah druhé části článku

15. Odkazy na Internetu

1. Analýza a transformace kódu psaného v Javě s využitím knihovny Spoon

V seriálu o programovacím jazyku Java i o virtuálním stroji Javy jsme se již zabývali několika nástroji, které jsou používány pro analýzu programů a aplikací naprogramovaných v Javě, popř. pro nějakou transformaci kódů (například pro vložení volání loggeru na začátek vybraných metod atd.). Jednalo se ovšem prozatím převážně o knihovny a nástroje, které jsou založeny na zpracování a modifikaci bajtkódu. Zmínili jsme se například o knihovně ASM, dále pak o knihovně BCEL (Byte Code Engineering Library) a nesmíme zapomenout ani na knihovnu Javassist a utilitu Byteman. Nad těmito knihovnami jsou postaveny složitější a možná i známější nástroje typu FindBugs. Ovšem již při častějším používání FindBugs začne být patrné, že tento nástroj nemá (a ani nemůže mít) k dispozici všechny důležité informace o zpracovávaných kódech, protože jeho princip je založen na analýze bajtkódu, ke kterému se pouze přidávají další metainformace typu číslo řádku a jméno zdrojového souboru.

I některé další aplikace vyžadují poněkud odlišný přístup než „pouhou“ analýzu bajtkódu. Pro příklad samozřejmě nemusíme chodit daleko, protože takovou aplikací jsou všechna moderní integrovaná vývojová prostředí, která „rozumí“ zapisovanému zdrojovému kódu – nezpracovávají ho tedy jako pouhou sekvenci textových řádků, kde každý řádek obsahuje proměnlivý počet znaků, ale udržují si určitý sofistikovanější model zdrojového kódu, který umožňuje, aby IDE mohlo provádět kontextové doplňování identifikátorů (metod, atributů, tříd, …), refaktoring apod. Onen sofistikovaný model je ve většině případů založen na AST (Abstract Syntax Tree). A právě na programovou manipulaci s AST reprezentujícím zdrojové kódy Javy je zaměřena knihovna Spoon, jejíž základní vlastnosti si dnes přiblížíme. V nejjednodušším případě Spoon pracuje jako „kolona“ známá všem administrátorům unixových systémů:

zdrojový kód → AST → processor1 → processor2 → modifikovaný AST → výsledný zdrojový kód

Na vstupu i na výstupu tedy máme zdrojový kód, ovšem uprostřed „kolony“ se neprovádí nějaké textové substituce či aplikace textových maker (jak to známe z preprocesoru céčka), ale přímo manipulace s AST. V některých případech nás dokonce ani nezajímá výsledný zdrojový kód, ale pouze průběh analýzy; to využijeme například při hledání chyb, porovnávání dvou kódů (jeho sémantiky) atd. Ve skutečnosti ovšem může být manipulace s AST poměrně sofistikovaná, od přidání nových metod až po záměnu programových smyček, „expanzi“ anotace na skutečný programový kód (například při označení třídy jako jedináčka se může automaticky skrýt konstruktor) apod.

2. Lokální instalace a základní otestování knihovny Spoon

Knihovnu Spoon je možné nainstalovat a použít několika způsoby. Pokud při správě projektů používáte Maven, stačí do konfiguračního souboru projektu (.pom) přidat následující řádky odkazující se na Spoon verze 5.3.0:

<dependency>
    <groupId>fr.inria.gforge.spoon</groupId>
    <artifactId>spoon-core</artifactId>
    <version>5.3.0</version>
</dependency>

Ve skutečnosti je však samozřejmě možné knihovnu Spoon nainstalovat a použít i bez Mavenu. Postačuje si pouze do pracovního adresáře stáhnout již připravený Java archiv (JAR) obsahující již přeloženou knihovnu a současně i všechny další potřebné balíčky. Takto vytvořený Java archiv vyžaduje ke své činnosti pouze JVM (navíc ještě budeme potřebovat alespoň překladač pro překlad tzv. procesorů). Pro naše účely se tedy celá instalace zmenší na jediný příkaz pro stažení Java archivu s knihovnou verze 5.3.0:

wget https://gforge.inria.fr/frs/download.php/file/36179/spoon-core-5.3.0-jar-with-dependencies.jar

Právě stažený Java archiv pro jistotu otestujeme. Po zadání tohoto příkazu:

java -jar spoon-core-5.3.0-jar-with-dependencies.jar

by se na standardní výstup měla vypsat nápověda:

Spoon version 5.3.0
Usage: java <launcher name> [option(s)]
 
Options :
 
  [-h|--help]
 
...
...
...

Poznámka: všechny příklady byly otestovány na OpenJDK 1.7.0 a budou funkční i pro 1.8.0. Pokud stále ještě používáte Javu 5 či 6, bude pravděpodobně nutné si Spoon přeložit ze zdrojových kódů.

3. Grafické zobrazení abstraktního syntaktického stromu zdrojových kódů

Ještě než si popíšeme základní způsoby zpracování AST, můžeme se podívat na to, jak je vlastně takový AST v knihovně Spoon reprezentován. Vytvořme si pro tento účel tu nejjednodušší možnou třídu:

public class EmptyClass {
}

Poznámka: třída je public z toho důvodu, aby bylo zřejmé, jak se jmenuje její soubor.

Tuto třídu nemusíme překládat, protože Spoon skutečně zpracovává zdrojový kód. Snadno se o tom můžeme přesvědčit po zadání následujícího příkazu v pracovním adresáři:

java -jar spoon-core-5.3.0-jar-with-dependencies.jar -g -i EmptyClass.java

V tomto příkazu specifikujeme jméno zpracovávané třídy parametrem -i a parametr -g zařídí, že Spoon zobrazí GUI okno se strukturou AST zobrazenou klasickým Swingovým „stromečkem“:

Obrázek 1: AST třídy EmptyClass. Každý uzel stromu má svůj typ (CtClass, CtBlock).

Zkusme ještě něco složitějšího; třídu Operators (k té se ještě dnes později vrátíme):

public class Operators {
    public static void main(String[] args) {
        int x, y, z;
        x = 10;
        y = 2 * x;
        z = y + x / 3 * (x + y);
    }
}
java -jar spoon-core-5.3.0-jar-with-dependencies.jar -g -i Operators.java

Obrázek 2: AST třídy Operators.

Poznámka: uzly stromu jsou implicitně zabaleny, ovšem z kontextového menu (pravé tlačítko myši) lze zvolit příkaz Expand All na zobrazení celého stromu.

4. Vytvoření a překlad velmi jednoduchého procesoru zdrojového kódu

Okno se zobrazeným AST je vhodné jen pro první seznámení s tím, jakým způsobem je zdrojový kód v knihovně Spoon reprezentován, ovšem veškeré případné manipulace musí být provedeny programově. Pro tento účel se používají takzvané procesory, což jsou třídy rozšiřující abstraktní třídu spoon.processing.AbstractProcessor či některého jejího potomka. Konkrétní (neabstraktní) třídy odvozené od AbstractProcessor obsahují metodu process(), která je postupně volána pro každý element AST, jenž je zvoleného typu. Pro začátek se nebudeme zabývat typy elementů a ukážeme si implementaci procesoru, jehož metoda process() postupně získá a zpracuje všechny elementy (každý element je reprezentován instancí třídy implementující rozhraní CtElement, kde „CT“ značí „Compile Time“). Náš úplně první procesor je velmi jednoduchý:

import spoon.processing.AbstractProcessor;
import spoon.reflect.declaration.CtElement;
 
public class FirstProcessor extends AbstractProcessor {
    public void process(CtElement element) {
        System.out.println(element);
    }
}

Pro překlad je pochopitelně nutné specifikovat, kde se nachází importovaná třída AbstractProcessor a rozhraní CtElement. Překlad se tedy provede následovně:

javac -cp spoon-core-5.3.0-jar-with-dependencies.jar FirstProcessor.java

5. Otestování procesoru zdrojového kódu

Otestujme si nyní právě přeložený procesor. Pro spuštění knihovny Spoon s procesorem se spouští metoda main třídy spoon.Launcher, samotný procesor se specifikuje přepínačem -p:

java -cp .:spoon-core-5.3.0-jar-with-dependencies.jar spoon.Launcher -p FirstProcessor -i zdrojový_kód

Zkusme si procesor otestovat na již představené třídě EmptyClass:

public class EmptyClass {
}
java -cp .:spoon-core-5.3.0-jar-with-dependencies.jar spoon.Launcher -p FirstProcessor -i EmptyClass.java
 
java.lang
java.lang.Object
java.lang
java.lang.Object
java.lang.Object#Object()
super()
{
}
public EmptyClass() {
}
public class EmptyClass {}

Metoda process() byla postupně volána pro všechny elementy AST a vypsala jejich textovou podobu CtElement.toString() na standardní výstup (ve skutečnosti není výsledek příliš čitelný, protože implementace metody CtElement.toString() jsou pojaty dosti minimalisticky).

Můžeme si ještě vyzkoušet nepatrně složitější příklad – třídu HelloWorld:

public class HelloWorld {
    private static String HELLO = "Hello world!";
 
    public static void main(String[] args) {
        System.out.println(HELLO);
    }
}
java -cp .:spoon-core-5.3.0-jar-with-dependencies.jar spoon.Launcher -p FirstProcessor -i HelloWorld.java
 
java.lang
java.lang.String
java.lang
java.lang.String
"Hello world!"
private static java.lang.String HELLO = "Hello world!";
java.lang
java.lang.Object
java.lang
java.lang.Object
java.lang.Object#Object()
super()
{
}
public HelloWorld() {
}
void
java.lang
java.lang.String
java.lang.String[]
java.lang.String[] args
java.lang
java.lang.System
java.lang.System
java.lang
java.lang.System
java.io
java.io.PrintStream
java.lang.System.out
java.lang.System.out
java.io
java.io.PrintStream
void
java.lang
java.lang.String
java.io.PrintStream#println(java.lang.String)

HelloWorld
HelloWorld

HelloWorld
java.lang
java.lang.String
HelloWorld.HELLO
HelloWorld.HELLO
java.lang.System.out.println(HelloWorld.HELLO)
{
    java.lang.System.out.println(HelloWorld.HELLO);
}
public static void main(java.lang.String[] args) {
    java.lang.System.out.println(HelloWorld.HELLO);
}
public class HelloWorld {
    private static java.lang.String HELLO = "Hello world!";

    public static void main(java.lang.String[] args) {
        java.lang.System.out.println(HelloWorld.HELLO);
    }
}
0 packages

Poznámka: povšimněte si, že při spuštění procesoru vznikl adresář spooned, do kterého jsou uloženy transformované třídy. Ve skutečnosti se vlastně žádná skutečná transformace (modifikace) neprováděla, takže třídy jsou vlastně sémanticky totožné.

6. Procesor zjišťující pozice elementů ve zdrojovém kódu

První implementace procesoru byla velmi primitivní (jediný programový řádek v metodě process), takže se ani není čemu divit, že generovaný výstup je prakticky nečitelný. Zkusme si nyní vytvořit nepatrně složitější procesor, který u každého elementu vypíše, na kterém místě ve zdrojovém kódu se tento element nachází, dále pak typ elementu (resp. přesněji řečeno jméno třídy implementující rozhraní CtElement) a nakonec lepší textovou reprezentaci elementu zjišťovanou metodou CtElement.getShortRepresentation. Vylepšený procesor vypadá následovně:

import spoon.processing.AbstractProcessor;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.cu.SourcePosition;
 
public class ListSourcePositionProcessor extends AbstractProcessor {
    public void process(CtElement element) {
        SourcePosition sp = element.getPosition();
        String position = sp == null ? "unknown" : sp.toString();
        System.out.println(position + "\t" + element.getClass().getName() + "\t" + element.getShortRepresentation());
    }
}

Povšimněte si, že nejprve zjišťujeme pozici elementu ve zpracovávaném zdrojovém kódu, která však nemusí být u všech elementů známá. Pokud nelze pozici elementu určit, vrátí se null, který je nutné korektně zpracovat.

Překlad vylepšeného procesoru:

javac -cp spoon-core-5.3.0-jar-with-dependencies.jar ListSourcePositionProcessor.java

7. Otestování druhého procesoru

Druhý procesor zjišťující pozice elementů a jejich typ, si otestujeme jak na třídě EmptyClass, tak i na třídě HelloWorld:

java -cp .:spoon-core-5.3.0-jar-with-dependencies.jar spoon.Launcher -p ListSourcePositionProcessor -i EmptyClass.java
 
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.lang
unknown spoon.support.reflect.reference.CtTypeReferenceImpl     java.lang.Object
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.lang
unknown spoon.support.reflect.reference.CtTypeReferenceImpl     java.lang.Object
unknown spoon.support.reflect.reference.CtExecutableReferenceImpl       java.lang.Object#Object()
(/home/tester/spoon/EmptyClass.java:1)       spoon.support.reflect.code.CtInvocationImpl     (java.lang.Object#Object()())
(/home/tester/spoon/EmptyClass.java:1)       spoon.support.reflect.code.CtBlockImpl  {
(java.lang.Object#Object()());
}
(/home/tester/spoon/EmptyClass.java:1)       spoon.support.reflect.declaration.CtConstructorImpl     EmptyClass()
(/home/tester/spoon/EmptyClass.java:1)       spoon.support.reflect.declaration.CtClassImpl   class EmptyClass
unknown spoon.reflect.CtModelImpl$CtRootPackage

Z výpisu, který je vhodné porovnat s prvním obrázkem ve třetí kapitole, je patrné, proč se u některých elementů nedá určit jejich vazba na konkrétní místo ve zdrojovém kódu. Taktéž si při porovnání s obrázkem můžeme uvědomit, že se metoda process volá pro všechny uzly AST, ovšem prohledávání je prováděno do hloubky, takže se kořenový uzel zpracuje jako poslední, což má svůj význam například při manipulaci s bloky či s výrazy.

java -cp .:spoon-core-5.3.0-jar-with-dependencies.jar spoon.Launcher -p ListSourcePositionProcessor -i HelloWorld.java
 
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.lang
(/home/tester/spoon/HelloWorld.java:2)  spoon.support.reflect.reference.CtTypeReferenceImpl java.lang.String
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.lang
unknown spoon.support.reflect.reference.CtTypeReferenceImpl java.lang.String
(/home/tester/spoon/HelloWorld.java:2)  spoon.support.reflect.code.CtLiteralImpl    "Hello world!"
(/home/tester/spoon/HelloWorld.java:2)  spoon.support.reflect.declaration.CtFieldImpl   java.lang.String HELLO
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.lang
unknown spoon.support.reflect.reference.CtTypeReferenceImpl java.lang.Object
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.lang
unknown spoon.support.reflect.reference.CtTypeReferenceImpl java.lang.Object
unknown spoon.support.reflect.reference.CtExecutableReferenceImpl   java.lang.Object#Object()
(/home/tester/spoon/HelloWorld.java:1)  spoon.support.reflect.code.CtInvocationImpl (java.lang.Object#Object()())
(/home/tester/spoon/HelloWorld.java:1)  spoon.support.reflect.code.CtBlockImpl  {
(java.lang.Object#Object()());
}
(/home/tester/spoon/HelloWorld.java:1)  spoon.support.reflect.declaration.CtConstructorImpl HelloWorld()
(/home/tester/spoon/HelloWorld.java:4)  spoon.support.reflect.reference.CtTypeReferenceImpl void
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.lang
unknown spoon.support.reflect.reference.CtTypeReferenceImpl java.lang.String
(/home/tester/spoon/HelloWorld.java:4)  spoon.support.reflect.reference.CtArrayTypeReferenceImpl    java.lang.String[]
(/home/tester/spoon/HelloWorld.java:4)  spoon.support.reflect.declaration.CtParameterImpl   args
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.lang
unknown spoon.support.reflect.reference.CtTypeReferenceImpl java.lang.System
(/home/tester/spoon/HelloWorld.java:5)  spoon.support.reflect.code.CtTypeAccessImpl java.lang.System
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.lang
unknown spoon.support.reflect.reference.CtTypeReferenceImpl java.lang.System
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.io
unknown spoon.support.reflect.reference.CtTypeReferenceImpl java.io.PrintStream
(/home/tester/spoon/HelloWorld.java:5)  spoon.support.reflect.reference.CtFieldReferenceImpl    java.io.PrintStream java.lang.System#out
(/home/tester/spoon/HelloWorld.java:5)  spoon.support.reflect.code.CtFieldReadImpl  java.io.PrintStream java.lang.System#out
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.io
unknown spoon.support.reflect.reference.CtTypeReferenceImpl java.io.PrintStream
unknown spoon.support.reflect.reference.CtTypeReferenceImpl void
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.lang
unknown spoon.support.reflect.reference.CtTypeReferenceImpl java.lang.String
unknown spoon.support.reflect.reference.CtExecutableReferenceImpl   java.io.PrintStream#println(java.lang.String)
unknown spoon.support.reflect.reference.CtPackageReferenceImpl
unknown spoon.support.reflect.reference.CtTypeReferenceImpl HelloWorld
unknown spoon.support.reflect.code.CtTypeAccessImpl HelloWorld
unknown spoon.support.reflect.reference.CtPackageReferenceImpl
unknown spoon.support.reflect.reference.CtTypeReferenceImpl HelloWorld
unknown spoon.support.reflect.reference.CtPackageReferenceImpl  java.lang
unknown spoon.support.reflect.reference.CtTypeReferenceImpl java.lang.String
unknown spoon.support.reflect.reference.CtFieldReferenceImpl    java.lang.String HelloWorld#HELLO
(/home/tester/spoon/HelloWorld.java:5)  spoon.support.reflect.code.CtFieldReadImpl  java.lang.String HelloWorld#HELLO
(/home/tester/spoon/HelloWorld.java:5)  spoon.support.reflect.code.CtInvocationImpl (java.io.PrintStream#println(java.lang.String)(java.lang.String HelloWorld#HELLO))
(/home/tester/spoon/HelloWorld.java:4)  spoon.support.reflect.code.CtBlockImpl  {
(java.io.PrintStream#println(java.lang.String)(java.lang.String HelloWorld#HELLO));
}
(/home/tester/spoon/HelloWorld.java:4)  spoon.support.reflect.declaration.CtMethodImpl  void main(java.lang.String[])
(/home/tester/spoon/HelloWorld.java:1)  spoon.support.reflect.declaration.CtClassImpl   class HelloWorld
unknown spoon.reflect.CtModelImpl$CtRootPackage

8. Procesor zjišťující všechny deklarované třídy

Procesory ve skutečnosti nemusí zpracovávat všechny typy elementů, ale je možné provádět různé formy filtrace. Nejjednodušší filtrace je zajištěna explicitním určením typů elementů – namísto obecného rozhraní CtElement lze zvolit konkrétní typ elementu či množinu elementů z následujícího seznamu rozhraní: CtAbstractInvocation, CtAnnotation, CtAnnotationFieldAccess, CtAnnotationMethod, CtAnnotationType, CtAnonymousExecutable, CtArrayAccess, CtArrayRead, CtArrayTypeReference, CtArrayWrite, CtAssert, CtAssignment, CtBinaryOperator, CtBlock, CtBreak, CtCase, CtCatch, CtCatchVariable, CtCatchVariableReference, CtCFlowBreak, CtClass, CtCodeElement, CtCodeSnippetExpression, CtCodeSnippetStatement, CtComment, CtConditional, CtConstructor, CtConstructorCall, CtContinue, CtDo, CtEnum, CtEnumValue, CtExecutable, CtExecutableReference, CtExecutableReferenceExpression, CtExpression, CtField, CtFieldAccess, CtFieldRead, CtFieldReference, CtFieldWrite, CtFor, CtForEach, CtFormalTypeDeclarer, CtIf, CtInterface, CtIntersectionTypeReference, CtInvocation, CtLambda, CtLiteral, CtLocalVariable, CtLocalVariableReference, CtLoop, CtMethod, CtModifiable, CtMultiTypedElement, CtNamedElement, CtNewArray, CtNewClass, CtOperatorAssignment, CtPackage, CtPackageReference, CtParameter, CtParameterReference, CtReference, CtReturn, CtStatement, CtStatementList, CtSuperAccess, CtSwitch, CtSynchronized, CtTargetedExpression, CtThisAccess, CtThrow, CtTry, CtTryWithResource, CtType, CtTypeAccess, CtTypedElement, CtTypeMember, CtTypeParameter, CtTypeParameterReference, CtTypeReference, CtUnaryOperator, CtUnboundVariableReference, CtVariable, CtVariableAccess, CtVariableRead, CtVariableReference, CtVariableWrite, CtWhile, CtWildcardReference.

Rozhraní tvoří hierarchii, například CtElement → CtLoop → CtDo, CtFor, CtForEach, CtWhile atd.

Podívejme se nyní na praktický příklad. Bude se jednat o procesor, který vypíše všechny deklarované třídy nalezené ve zpracovávaném zdrojovém kódu. V procesoru jsou provedeny dvě změny. Nejprve uvedeme typ zpracovávaných elementů v deklaraci třídy (namísto extends AbstractProcessor zapíšeme konkrétně extends AbstractProcessor<CtClass> a metoda process je upravena takovým způsobem, že zpracovává elementy typu CtClass a nikoli CtElement, takže se vyhneme zbytečnému přetypování:

import spoon.processing.AbstractProcessor;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.cu.SourcePosition;
 
public class ListClassesProcessor extends AbstractProcessor<CtClass> {
    public void process(CtClass element) {
        SourcePosition sp = element.getPosition();
        String position = sp == null ? "unknown" : sp.toString();
        System.out.println(position + "\t" + element.getSimpleName());
    }
}

Překlad procesoru provedeme nám již známým příkazem:

javac -cp spoon-core-5.3.0-jar-with-dependencies.jar ListClassesProcessor.java

9. Otestování třetího procesoru

Nejprve nový procesor otestujeme na naší známé třídě EmptyClass:

java -cp .:spoon-core-5.3.0-jar-with-dependencies.jar spoon.Launcher -p ListClassesProcessor -i EmptyClass.java
 
(/home/tester/spoon/HelloWorld.java:1)       HelloWorld

Nic překvapivého se nevypsalo, ale můžeme zde vidět, že správnou filtrací lze dosáhnout mnohem uspokojivějších výsledků.

Zkusme tedy něco složitějšího, a to konkrétně následující zdrojový kód s několika top-level třídami a vnořenými třídami:

public class ManyClasses {
    class InnerClass {
    }
    class OtherInnerClass {
        class NestedClass {
        }
    }
}
 
class OtherClass {
}
 
class YetAnotherClass {
}

Spusťme náš nový procesor:

java -cp .:spoon-core-5.3.0-jar-with-dependencies.jar spoon.Launcher -p ListClassesProcessor -i ManyClasses.java
 
(/home/tester/spoon/ManyClasses.java:2)      InnerClass
(/home/tester/spoon/ManyClasses.java:5)      NestedClass
(/home/tester/spoon/ManyClasses.java:4)      OtherInnerClass
(/home/tester/spoon/ManyClasses.java:1)      ManyClasses
(/home/tester/spoon/ManyClasses.java:10)     OtherClass
(/home/tester/spoon/ManyClasses.java:13)     YetAnotherClass

Povšimněte si způsobu výpisu – první třída obsahující dvě vnitřní třídy je vypsána až na čtvrtém místě, tj. ve chvíli, kdy má procesor šanci správně zpracovat všechny vnitřní třídy.

10. Procesor detekující zápisy do proměnných

Všechny tři předchozí procesory vlastně ukazovaly pouze nepatrnou část možností knihovny Spoon. Podívejme se na složitější a možná již praktičtější příklad. Budeme v něm hledat všechna místa v programovém kódu, v nichž se zapisuje do proměnných. Pro každou operaci zápisu, kterou nalezneme, vypíšeme jak pozici ve zdrojovém kódu, tak i jméno proměnné. Náš procesor nyní bude filtrovat jen elementy typu CtVariableWrite a nikoli všechny elementy CtElement. Dále pro každý nalezený element v AST přečteme, o jakou se jedná proměnnou. Objekt představující referenci na proměnnou v AST má typ CtVariableReference, nás ovšem v tuto chvíli zajímá pouze jeho metoda getSimpleName(). Výsledná implementace procesoru může vypadat takto:

import spoon.processing.AbstractProcessor;
import spoon.reflect.code.CtVariableWrite;
import spoon.reflect.reference.CtVariableReference;
import spoon.reflect.cu.SourcePosition;
 
public class VariableWriteProcessor extends AbstractProcessor<CtVariableWrite> {
    public void process(CtVariableWrite element) {
        SourcePosition sp = element.getPosition();
        CtVariableReference variable = element.getVariable();
        String variableName = variable.getSimpleName();
        String position = sp == null ? "unknown" : sp.toString();
        System.out.println("write into variable '" + variableName + "' at " + position);
    }
}

Příkaz pro překlad procesoru již známe:

javac -cp spoon-core-5.3.0-jar-with-dependencies.jar VariableWriteProcessor.java

11. Otestování čtvrtého procesoru

Pro otestování čtvrtého procesoru použijeme třídu Operators, s níž jsme se již seznámili ve třetí kapitole (v ní je zobrazena část AST této třídy):

public class Operators {
    public static void main(String[] args) {
        int x, y, z;
        x = 10;
        y = 2 * x;
        z = y + x / 3 * (x + y);
    }
}

Zkusme nyní nalézt všechny zápisy do proměnných ve třídě Operators:

java -cp .:spoon-core-5.3.0-jar-with-dependencies.jar spoon.Launcher -p VariableWriteProcessor -i Operators.java

Zde je výsledek, který je již na první pohled korektní:

bitcoin_skoleni

write into variable 'x' at (/home/tester/spoon/Operators.java:4)
write into variable 'y' at (/home/tester/spoon/Operators.java:5)
write into variable 'z' at (/home/tester/spoon/Operators.java:6)

Poznámka: tento procesor nedokáže (a ani nemá dokázat) zobrazit deklaraci proměnné s přiřazením.

12. Zdrojové kódy všech procesorů i testovaných příkladů

Zdrojový kód Adresa
FirstProcessor.java https://github.com/tisnik/pre­sentations/blob/master/spo­on/FirstProcessor.java
compile_FirstProcessor.sh https://github.com/tisnik/pre­sentations/blob/master/spo­on/compile_FirstProcessor­.sh
run_FirstProcessor.sh https://github.com/tisnik/pre­sentations/blob/master/spo­on/run_FirstProcessor.sh
   
ListSourcePositionProcessor.java https://github.com/tisnik/pre­sentations/blob/master/spo­on/ListSourcePositionProces­sor.java
compile_ListSourcePositionProcessor.sh https://github.com/tisnik/pre­sentations/blob/master/spo­on/compile_ListSourcePosi­tionProcessor.sh
run_ListSourcePositionProcessor.sh https://github.com/tisnik/pre­sentations/blob/master/spo­on/run_ListSourcePosition­Processor.sh
   
ListClassesProcessor.java https://github.com/tisnik/pre­sentations/blob/master/spo­on/ListClassesProcessor.ja­va
compile_ListClassesProcessor.sh https://github.com/tisnik/pre­sentations/blob/master/spo­on/compile_ListClassesPro­cessor.sh
run_ListClassesProcessor.sh https://github.com/tisnik/pre­sentations/blob/master/spo­on/run_ListClassesProcessor­.sh
   
VariableWriteProcessor.java https://github.com/tisnik/pre­sentations/blob/master/spo­on/VariableWriteProcessor­.java
compile_VariableWriteProcessor.sh https://github.com/tisnik/pre­sentations/blob/master/spo­on/compile_VariableWritePro­cessor.sh
run_VariableWriteProcessor.sh https://github.com/tisnik/pre­sentations/blob/master/spo­on/run_VariableWriteProces­sor.sh
   
EmptyClass.java https://github.com/tisnik/pre­sentations/blob/master/spo­on/EmptyClass.java
HelloWorld.java https://github.com/tisnik/pre­sentations/blob/master/spo­on/HelloWorld.java
ManyClasses.java https://github.com/tisnik/pre­sentations/blob/master/spo­on/ManyClasses.java
Operators.java https://github.com/tisnik/pre­sentations/blob/master/spo­on/Operators.java

13. Použité třídy a rozhraní knihovny Spoon

  1. spoon.processing.AbstractProcessor
    http://spoon.gforge.inria­.fr/mvnsites/spoon-core/apidocs/spoon/proces­sing/AbstractProcessor.html
  2. spoon.reflect.declaration.CtElement
    http://spoon.gforge.inria­.fr/mvnsites/spoon-core/apidocs/spoon/reflec­t/declaration/CtElement.html
  3. spoon.reflect.declaration.CtClass
    http://spoon.gforge.inria­.fr/mvnsites/spoon-core/apidocs/spoon/reflec­t/declaration/CtClass.html
  4. spoon.reflect.code.CtVariableWrite
    http://spoon.gforge.inria­.fr/mvnsites/spoon-core/apidocs/spoon/reflec­t/code/CtVariableWrite.html
  5. spoon.reflect.cu.SourcePosition
    http://spoon.gforge.inria­.fr/mvnsites/spoon-core/apidocs/spoon/reflec­t/cu/SourcePosition.html
  6. spoon.reflect.reference.CtVa­riableReference
    http://spoon.gforge.inria­.fr/mvnsites/spoon-core/apidocs/spoon/reflec­t/reference/CtVariableRefe­rence.html

14. Obsah druhé části článku

Dnes jsme si ukázali pouze naprosté základy nabízené knihovnou Spoon. Příště se seznámíme se složitějšími procesory a taktéž s takzvanými šablonami, které lze využít pro automatizaci různých transformací zdrojového kódu.

15. Odkazy na Internetu

  1. Spoon – Source Code Analysis and Transformation for Jav
    http://spoon.gforge.inria.fr/
  2. Project Filelist for Spoon
    https://gforge.inria.fr/frs/?grou­p_id=73
  3. Spoon na GitHubu
    https://github.com/INRIA/spoon
  4. Instance Jenkinsu, v níž se překládá Spoon
    https://ci.inria.fr/sos/
  5. JavaParser: Java Parser and Abstract Syntax Tree
    http://javaparser.org/
  6. Javaparser na GitHubu
    https://github.com/javapar­ser/javaparser
  7. FindBugs Fact Sheet
    http://findbugs.sourcefor­ge.net/factSheet.html
  8. Open Source ByteCode Libraries in Java
    http://java-source.net/open-source/bytecode-libraries
  9. ASM Home page
    http://asm.ow2.org/
  10. Seznam nástrojů využívajících projekt ASM
    http://asm.ow2.org/users.html
  11. ObjectWeb ASM (Wikipedia)
    http://en.wikipedia.org/wi­ki/ObjectWeb_ASM
  12. Java Bytecode BCEL vs ASM
    http://james.onegoodcooki­e.com/2005/10/26/java-bytecode-bcel-vs-asm/
  13. BCEL Home page
    http://commons.apache.org/bcel/
  14. Byte Code Engineering Library (před verzí 5.0)
    http://bcel.sourceforge.net/
  15. Byte Code Engineering Library (verze >= 5.0)
    http://commons.apache.org/pro­per/commons-bcel/
  16. BCEL Manual
    http://commons.apache.org/bcel/ma­nual.html
  17. Byte Code Engineering Library (Wikipedia)
    http://en.wikipedia.org/wiki/BCEL
  18. BCEL Tutorial
    http://www.smfsupport.com/sup­port/java/bcel-tutorial!/
  19. Bytecode Engineering
    http://book.chinaunix.net/spe­cial/ebook/Core_Java2_Volu­me2AF/0131118269/ch13lev1sec6­.html
  20. Bytecode Outline plugin for Eclipse (screenshoty + info)
    http://asm.ow2.org/eclipse/index.html
  21. Javassist
    http://www.jboss.org/javassist/
  22. Byteman
    http://www.jboss.org/byteman
  23. Java programming dynamics, Part 7: Bytecode engineering with BCEL
    http://www.ibm.com/develo­perworks/java/library/j-dyn0414/
  24. Using javap
    http://www.idevelopment.in­fo/data/Programming/java/mis­cellaneous_java/Using_javap­.html
  25. Examine class files with the javap command
    http://www.techrepublic.com/ar­ticle/examine-class-files-with-the-javap-command/5815354
  26. aspectj (Eclipse)
    http://www.eclipse.org/aspectj/
  27. Aspect-oriented programming (Wikipedia)
    http://en.wikipedia.org/wi­ki/Aspect_oriented_program­ming
  28. AspectJ (Wikipedia)
    http://en.wikipedia.org/wiki/AspectJ
  29. EMMA: a free Java code coverage tool
    http://emma.sourceforge.net/
  30. Cobertura
    http://cobertura.sourceforge.net/
  31. jclasslib bytecode viewer
    http://www.ej-technologies.com/products/jclas­slib/overview.html
  32. Abstract syntax tree
    https://en.wikipedia.org/wi­ki/Abstract_syntax_tree

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.