Anotacijos Java kalboje    

Taip pat siūlome susipažinti: Lambda išraiškos – Java į naują lygį  
Java 8: Optional prieš null  

Anotacijos yra labai svarbi Java dalis, įtraukta Java 5 versijoje.
Jei jau programavote Java kalba, neabejotinai esate susipažinęs su anotacijomis. Tačiau, galbūt, jums neteko kurti savų anotacijų. Šis straipsnelis aprašo tai, bei parodo, kaip iš programos nuskaityti anotacijas. Tačiau pradžioje vis tiek pateikiama bendra informacija apie anotacijas – galite praleisti pradinę skiltį, jei ji pasirodys žinoma – iškart pereidami prie skilties „Savo anotacijų kūrimas“.

Pastaba: straipsnelyje paminimos ir kai kurios Java 8 galimybės darbui su anotacijomis. Ta2iau šis straipsnelis nėra išsamus ir pilnas anotacijų aprašymas: pagrindinis jo tikslas – pristatyti anotacijas ir pademonstruoti darbą su jomis programų vykdymo metu.


Anotacijos yra tam tikra metaduomenų rūšis, suteikinačios informaciją apie programą, tačiau pačios nesančios programos dalimi, tad jos neturi tiesioginio poveikio kodui, kurį apibūdina (anotuoja). Jas galima panaudoti įvairiai:

  • Pateikti informaciją kompiliatoriui;
  • Kompiliavimo ar diegimo (deployment) metu;
  • Vykdant programą.
  • Pavyzdžiui, programos formavimo (building) metu kompiliuojamas kodas, kuriami XML failai (pvz., diegimo deskriptoriai), sukompiliuotas kodas ir failai pakuojami į JAR failą ir kt. Formavimas paprastai atliekamas automatinių įrankių (pvz., Apache Ant arba Apache Maven). Tie įrankiai gali peržiūrėti Java kodą, ieškodami tam tikrų anotacijų ir pagal jas formuoti reikiamą versiją.

    Anotacijos tekste pradedamos „eta“ ženklu (@), nurodančiu, kad po jo seka anotacija. Pavyzdžiui, kode dažnai matoma anotacija yra

    @Override
    public void toksMetodas() { … }

    Anotacijos gali turėti vidinius elementus – su priskirtais vardais arba anoniminius, pvz.

    @Autorius (asmuo="Jonas Jonaitis",data="05/31/2016")
    public class AnotuotaKlase () { … }

    Jei elementas anotacijoje tėra vienas, jo vardą galima praleisti, pvz.,

    @SuppressWarnings("unchecked")
    void manoMetodas() { ... }

    Pastaba: tam pačiam aprašui galima naudoti kelias anotacijas (net ir to paties tipo, tačiau tokios leistinos tin nuo Java 8), pvz.,

    @Override
    @Autorius (asmuo="Petras Petraitis",data="05/31/2016")
    @Autorius (asmuo="Antanas Antanaitis",data="06/30/2016")
    public void toksMetodas() { … }

    Anotacijas galima naudoti visiems aprašams: klasių, laukų, metodų ir kitų programos elementų.

    Java 8 anotacijas papildomai galima naudoti:

    • kuriant naują objekto kopiją (pvz., new @Interned manoObjektas;)
    • atliekant tipų suderinamumą (pvz., eilute = (@NonNull String)str;)
    • Realizavimo sąlygoje (pvz., class ReadonlyList<T> implements @Readonly List<@Readonly T> { ... } )
    • Išimčių pateikimo sąlygoje (pvz., void monitorTemperature() throws @Critical TemperatureException { ... } )

    Galima susikurti savas anotacijas. Jų aprašas panašus į sąsajos (interface) aprašą – tik prasideda „@” ženklu, pvz.,

    @Documented
    @interface Autorius {
        String asmuo();
        String data();
        Int revizija default 1;
    }

    Čia papildoma anotacija „@Documented” nurodo, kad anotacijos „Autorius” informacija turi atsirasti Javadoc generuotoje dokumentacijoje.

    Standartinės anotacijos

    Kažkiek anotacijų yra iš anksto apibrėžtos Java API; vienas jų naudoja kompiliatorius, o kai kurios taikomos kitoms anotacijoms.

    java.lang apibrėžia @Deprecated, @Override ir @SuppressWarnings

    @Deprecated nurodo, kad elementas yra smerktinas ir neturėtų būti daugiau naudojamas. Kompiliatorius pateikia perspėjimą, jei jis panaudojamas.

    @Override nurodo, kad elementas perdengia elementą, aptašytą superklasėje.

    @SuppressWarnings nurodo, kad nereikia pateikti perspėjimų (nurodyto tipo, deprecation arba unchecked).

    Kitoms anotacijoms taikomos anotacijos yra metaanotacijos ir kažkiek jų aprašyta java.lang.annotation.

    @Retention nurodo, kaip yra saugoma anotacija:

    • RetentionPolicy.SOURCE – tėra tik pradinio teksto lygyje ir praleidžiama kompiliatoriaus;
    • RetentionPolicy.CLASS – išsaugoma kompiliatoriaus, tačiau praleidžiama JVM;
    • RetentionPolicy.RUNTIME – išlaikoma iki programos vykdymo ir gali būti panaudota programos vykdymo metu
      Reflection API turi kelis metodus, kuriuos galima panaudoti anotacijų pasiėmimui. Po vieną anotaciją gražina tokie metodai kaip AnnotatedElement.getAnnotationByType(Class<T>). Jei vieno tipo yra kelios anotacijos, tada jas galima išlukštenti pasiimant konteinerį. Java 8 yra įtraukusi naujų metodų. Darbą su anotacijomis aptarsime atskiroje skiltyje.

    @Documented nurodo, kad elementai bus dokumentuojami su Javadoc.

    @Inherited nurodo, kad anotacija yra paveldėta iš superklasės. Tokios anotacijos taikomos tik klasėms.

    @Repeatable - Java 8 įvesta anotacija, nurodanti, kad anotacija gali būti pasikartojanti (tam pačiam elementui).

    @Target papildomai apriboja anotacijos naudojimą nurodydama, kokiems elementams ji taikytina. Galimos reikšmės: ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE

    Anotacijų naudojimas

    Paimkime paprastą pavyzdį, - tam tikrai klasei realizuokime metodą:

    @Override
    public String toString() {
    	return "Objekto atvaizdavimas: " + super.toString();
    }

    Jame mes perdengiame toString() metodą savu objekto atvaizdavimu. Jei nebūtų nurodyta @Override anotacija, kodas irgi veiktų. Tad koks anotacijos privalumas? @Override nurodo kompiliatoriui, kad metodas yra perdengtas ir jei jokio tokio metodo nėra tėvinėje klasėje, būtų pateikiama kompiliavimo klaida. Tarkim, kad mano pirštas nuslydo, ir vietoje public String toString() { ... } aš kode surinkau public String tooString() { ... }. Jei nebūtų @Override, kompiliatorius man tos klaidos nepraneštų ir programa gali veikti ne taip, kaip aš tikėjausi.

    Kodėl buvo įtrauktos anotacijos?

    Iki anotacijų metaduomenų pateikimui buvo plačiai naudojamas XML formatas. Tačiau jis labai prastai „lipo“ prie kodo. XML buvo įtrauktas tam, kad atskirtų kongigūraciją nuo kodo – kas truputį kelia įtarimą, nes tai dvi to paties ciklo dalys.

    Tarkim, kad norite nustatyti visai programai bendras konstantas (parametrus). Tada XML yra geresnis pasirinkimas, nes tai nesusiję su jokia konkrečia kodo dalimi. Tačiau jei norite atskirą metodą pateikti kaip servisą, geriau rinktis anotaciją, nes ji glaudžiau susieta su to metodo realizacija ir apie tai žino to metodo programuotojas.

    Kitas svarbus veiksnys buvo tas, kad anotacijos numato labai standartinį būdą metaduomenų aprašymui kode. Iki tol programuotojai tam naudojo įvairius savus būdus. Šiais laikais dažniausiai derinama XML ir anotacijų naudojimas, išnaudojant teigiamas abiejų būdų puses.

    Anotacijos yra galinga priemonė, kurią plačiai naudoja paketai (kaip spring ir Hibernate – ypač loginimui ir validacijai). servlet 3.0 specifikacija įtraukė daug naujų anotacijų, ypač su saugumu susijusiems dalykams.

    Savo anotacijų kūrimas    

    Jei programavote Java kalba, neabejotinai esate susipažinęs su anotacijomis. Tačiau, galbūt, jums neteko kurti savų anotacijų.

    Tam tereikia susikurti naują tipą naudojant @interface, nurodant visus metaduomenis. Tolimesniuose pavyzdžiuose naudosime tokį savo anotacijos Attr aprašą:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @interface Attr {
    	boolean bPublic () default false;
    	String Description();
    }

    Pirmiausia matome, kad mūsų anotacija Attr pati turi dvi anotacijas:
    @Target(ElementType.TYPE) – nurodo, kokiems elementams gali būti taikoma anotacija. ElementType.TYPE reiškia ji bus taikoma visiems aprašams;
    @Retention(RetentionPolicy.RUNTIME) – nurodo anotacijos išlaikymo sąlygą.
    RetentionPolicy.RUNTIME reiškia, kad anotacija bus išsaugoma iki programos vykdymo ir jos duomenis bus galima pasiekti iš programos.

    Anotacijas galima priskirti klasėms, metodams, parametrams ir laukams. Sukurkime demonstracinę klasę, kurioje visose situacijose panaudosime prieš tai aprašytą anotaciją Attr:

    	
    @Attr(bPublic=true, Description="Priskirta anotacija")
    public static class myAnnotadedClass {
    	
    	@NonNull
    	@Attr(Description="prisistatymo tekstas")
    	String sPrisistatymas = "Tai mano anotuotos klases pavyzdys"; 
    	    
    	static int nInstance = 0; // neanotuotas kintamasis
    	
    	public myAnnotadedClass() {
    	     nInstance++;
    	}
    	
    	@Override
    	@Attr(bPublic=true, Description="prisistatymo tekstas")
    	public String toString() {
    	     return "Objekto atvaizdavimas. Kopijos: " + nInstance + "\n" + sPrisistatymas + ". " + super.toString();
    	}
    
    	// Metodas be anotacijos, bet anotuotas parametras
    	public void pasisveikink (@Attr(Description="Vardo parametras") String pVardas) { 
    	     System.out.println ("Labas, " + pVardas + ". Esu anotuota myAnnotadedClass!");
    	}
    	
    	public void emptyMethod () {} // jokios anotacijos
    }

    Šios klasės veikimo patikrinimui tinka toks kodo pavyzdys:

    myAnnotadedClass clKopija = new myAnnotadedClass();		
    System.out.println (clKopija.toString());		
    clKopija.pasisveikink("pone");

    Taigi, kol kas nieko ypatinga, kitame skirsnelyje apžvelgsime, kaip galima paimti anotacijas vykdymo metu.

    Anotacijų pasiekimas iš kodo

    Java Reflection API leidžia pasiekti klases, sąsajas, laukus ir metodus vykdymo metu, kaip jų vardai nėra žinomi kompiliavimo metu. Jo priemonėmis taip pat galima kurti naujas objektų kopijas, iškviesti metodus bei gauti ar priskirti laukų reikšmes. Tai priemonė ir darbui su anotacijomis programos vykdymo metu.

    Nesigilinant į detales, priede straipsnelio pabaigoje pateikiamas pilnas darbo su anotacijomis pavyzdžio tekstas, kuriame rasite visų prieš tai aprašytų anotacijų tipo paėmimo iliustraciją.

    Anotacijos pakeitimas vykdymo metu

    Jei anotacija išsaugojama iki programos vykdymo (t.y. aprašyta kaip @Retention(RetentionPolicy.RUNTIME) ), jos aprašą galima pakeisti vykdymo metu. Pateiksime vieną variantų, iliustruojančių tą veiksmą aukščiau aprašytos klasės myAnnotadedClass anotacijos pakeitimui:

    private static void changeAnnotationDescription(Class pClass, Class pAnnotClass, final String pElem, final String pDescr) {      
          @SuppressWarnings("unchecked")
          Annotation anot = pClass.getAnnotation(pAnnotClass);
            
            Object handler = Proxy.getInvocationHandler(anot);
              Field laukas;
              try {
                  laukas = handler.getClass().getDeclaredField("memberValues");
              } catch (NoSuchFieldException | SecurityException e) {
                  throw new IllegalStateException(e);
              }
              laukas.setAccessible(true);
            
              Map nariai;
              
              try {
                 nariai = (Map<String, Object>) laukas.get(handler);
                 System.out.println(nariai.toString());
                 
                 Object oldValue = nariai.get(pElem);             
                 nariai.replace(pElem, pDescr);   
                 
                 System.out.println("Pakeista " + pElem + ": " + oldValue.toString() + " -> " + pDescr);
              } catch (IllegalArgumentException | IllegalAccessException e) {
                  throw new IllegalStateException(e);
              }               
       }

    Šį metodą galima panaudoti taip:

     
    changeAnnotationDescription(myAnnotadedClass.class, Attr.class, "Description", "Pakeista myAnnotadedClass anotacija");
    Attr newAnnot = clKopija.getClass().getAnnotation(Attr.class);
            
    System.out.println("\nNaujai priskirta: " + newAnnot.Description());

    Priedas

    Pateikiame pilną pradinį pavyzdžio, iliustruojančio straipsnyje aprašytus klausimus, tekstą. Jo pateikiami rezultatai apačioje.

    Pastaba: Jo vykdymui naudokite Java 8 aplinką, o į projektą įtraukite org.eclipse.jdt.annotation.NonNull paketą, jei naudojatės Eclipse IDE! Kiti gali j5 pakeisti kitu analogišku paketu.

    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.annotation.Annotation;
    import java.util.Map;
    
    import org.eclipse.jdt.annotation.NonNull;
    
    
    @SuppressWarnings("rawtypes")
    public class LabasAnotacija {
    
       @Retention(RetentionPolicy.RUNTIME)   
       @interface Attr {
          boolean bPublic () default false;
          String Description();
       }
          
       
       public static void main(String[] args) {
    
          System.out.println ("Labas, anotacija");
                
          myAnnotadedClass clKopija = new myAnnotadedClass();      
          System.out.println (clKopija.toString());      
          clKopija.pasisveikink("pone");
                
          boolean isNullAnnot, isOverAnnot, isAttrAnnotaded = clKopija.getClass().isAnnotationPresent(Attr.class);
          
          System.out.println ("Ar anuotuotos? LabasAnotacija: " + LabasAnotacija.class.isAnnotationPresent(Attr.class) + 
                "; myAnnotadedClass: " + isAttrAnnotaded);
          
          if (isAttrAnnotaded) {         
               System.out.println ("myAnnotadedClass: public: " + clKopija.getClass().getAnnotation(Attr.class).bPublic() +
                        ", kas: " + clKopija.getClass().getAnnotation(Attr.class).Description()) ;
          }
                
          System.out.println("\nAnotuoti metodai:");
          Method[] metodai = clKopija.getClass().getMethods();
    
          for (Method m : metodai) {
             System.out.println ("Metodas " + m.getName() + ". ");
              isOverAnnot = m.isAnnotationPresent(Override.class);
              isAttrAnnotaded = m.isAnnotationPresent(Attr.class);         
              System.out.print ("anotuotas kaip Override: " + isOverAnnot + ", su Attr anotacija: " + isAttrAnnotaded);
              if (isAttrAnnotaded) {
                 Attr annot = m.getAnnotation(Attr.class);
                 System.out.print ( "; anotacija: " + annot.Description());
              }
              System.out.println();
           }
          
          System.out.println("\nAnotuotas parametras:");
            Method m; 
            try {
                  m = clKopija.getClass().getMethod("pasisveikink", String.class);
                  Annotation[][] parameterAnnotations = m.getParameterAnnotations();
                  Class[] parameterTypes = m.getParameterTypes();
    
                  System.out.println ("Metodo " + m.getName() + " parametro anotacija:");
                  int k = 0;
                  for (Annotation[] annotations : parameterAnnotations) {                 
                Class parameterType = parameterTypes[k++];
    
                      for (Annotation a : annotations) {
                          if (a instanceof Attr) {
                            Attr myAnnotation = (Attr) a;
                             System.out.print("parametro tipas: " + parameterType.getName());                         
                             System.out.println(", anotacija " + myAnnotation.getClass().getName() + ": " + myAnnotation.Description());
                           }
                       }
                  }
           
            } catch (NoSuchMethodException e) {
             e.printStackTrace();
            }      
                
          System.out.println("\nAnotuoti laukai:");
          Field laukai [] = clKopija.getClass().getFields();
            for (Field f : laukai) {
               System.out.print("Laukas: " + f.getName() + "; tipas: " + f.getType() + ". ");
              isAttrAnnotaded = f.isAnnotationPresent(Attr.class);
              isNullAnnot = f.isAnnotationPresent(NonNull.class);
              System.out.print ("anotuotas kaip NonNull - " + isNullAnnot + ", su Attr anotacija: " + isAttrAnnotaded);
                 if (isAttrAnnotaded) {
                    System.out.print ( "; anotacija: " + f.getAnnotation(Attr.class).Description());
                 }   
                 System.out.println();          
            }
                    
            changeAnnotationDescription(myAnnotadedClass.class, Attr.class, "Description", "Pakeista myAnnotadedClass anotacija");
            Attr newAnnot = clKopija.getClass().getAnnotation(Attr.class);
            
            System.out.println("\nNaujai priskirta: " + newAnnot.Description());
            
          System.out.println ("\nSudie, anotacija");   
       }   
       
       // -------------------------------------------------
       @SuppressWarnings("unchecked")
       private static void changeAnnotationDescription(Class pClass, Class pAnnotClass, final String pElem, final String pDescr) {      
          Annotation anot = pClass.getAnnotation(pAnnotClass);
            
            Object handler = Proxy.getInvocationHandler(anot);
              Field laukas;
              try {
                  laukas = handler.getClass().getDeclaredField("memberValues");
              } catch (NoSuchFieldException | SecurityException e) {
                  throw new IllegalStateException(e);
              }
              laukas.setAccessible(true);
            
              Map<String, Object> nariai;
              
              try {
                 nariai = (Map<String, Object>) laukas.get(handler);
                 System.out.println(nariai.toString());
                 
                 Object oldValue = nariai.get(pElem);             
                 nariai.replace(pElem, pDescr);   
                 
                 System.out.println("Pakeista " + pElem + ": " + oldValue.toString() + " -> " + pDescr);
              } catch (IllegalArgumentException | IllegalAccessException e) {
                  throw new IllegalStateException(e);
              }               
       }
       
       // --------------------------------------------------------   
       @Attr(Description="Priskirta anotacija")
        static class myAnnotadedClass {
          
          @NonNull
          @Attr(bPublic=true, Description="prisistatymo tekstas")
          public String sPrisistatymas = "Tai mano anotuotos klases pavyzdys"; 
              
          static int nInstance = 0; // neanotuotas kintamasis
          
          public myAnnotadedClass() {
             nInstance++;
          }
          
          @Override
          @Attr(bPublic=true, Description="prisistatymo tekstas")
          public String toString() {
             return "Objekto atvaizdavimas. Kopijos: " + nInstance + "\n" + sPrisistatymas + ". " + super.toString();
          }
    
          // Metodas be anotacijos, bet anotuotas parametras
          public void pasisveikink (@Attr(Description="Vardo parametras") String pVardas) { 
             System.out.println ("Labas, " + pVardas + ". Esu anotuota myAnnotadedClass!");
          }
          
          public void emptyMethod () {} // jokios anotacijos      
       }   
    }

    Rezultatai

    Pateiktas pavyzdys pateikia tokius rezultatus:

    Labas, anotacija
    Objekto atvaizdavimas. Kopijos: 1
    Tai mano anotuotos klases pavyzdys. LabasAnotacija$myAnnotadedClass@c03315b5
    Labas, pone. Esu anotuota myAnnotadedClass!
    Ar anuotuotos? LabasAnotacija: false; myAnnotadedClass: true
    myAnnotadedClass: public: false, kas: Priskirta anotacija
    
    Anotuoti metodai:
    Metodas equals. 
    anotuotas kaip Override: false, su Attr anotacija: false
    Metodas hashCode. 
    anotuotas kaip Override: false, su Attr anotacija: false
    Metodas toString. 
    anotuotas kaip Override: false, su Attr anotacija: true; anotacija: prisistatymo tekstas
    Metodas pasisveikink. 
    anotuotas kaip Override: false, su Attr anotacija: false
    Metodas emptyMethod. 
    anotuotas kaip Override: false, su Attr anotacija: false
    Metodas getClass. 
    anotuotas kaip Override: false, su Attr anotacija: false
    Metodas notify. 
    anotuotas kaip Override: false, su Attr anotacija: false
    Metodas notifyAll. 
    anotuotas kaip Override: false, su Attr anotacija: false
    Metodas wait. 
    anotuotas kaip Override: false, su Attr anotacija: false
    Metodas wait. 
    anotuotas kaip Override: false, su Attr anotacija: false
    Metodas wait. 
    anotuotas kaip Override: false, su Attr anotacija: false
    
    Anotuotas parametras:
    Metodo pasisveikink parametro anotacija:
    parametro tipas: java.lang.String, anotacija $Proxy1: Vardo parametras
    
    Anotuoti laukai:
    Laukas: sPrisistatymas; tipas: class java.lang.String. anotuotas kaip NonNull - false, su Attr 
    anotacija: true; anotacija: prisistatymo tekstas
    {bPublic=false, Description=Priskirta anotacija}
    Pakeista Description: Priskirta anotacija -> Pakeista myAnnotadedClass anotacija
    
    Naujai priskirta: Pakeista myAnnotadedClass anotacija
    
    Sudie, anotacija
    

    (c) 2016, Vartiklis. Visos teisės saugomos. Leidžiama naudoti tik asmeninės savišvietos tikslais. Bet koks platinimas bet kokiomis priemonemis, viso teksto arba atskiros jo dalies, draudžiamas!

    Ka-ka-rie-kūūūū...
    Tiesiog - Java
    'Java' ir ne tik ji!
    Skriptai - ateities kalbos?
    JavaScript pradžiamokslis
    Java 8: Optional prieš null
    Programavimas Unix aplinkoje
    Programavimo kalbų klegesys
    Pitonas, kandantis sau uodegą!
    AWK kalba - sena ir nuolat aktuali
    Lambda išraiškos – Java į naują lygį
    CGI.pm biblioteka: sausainiai
    Vaizdi rašysena - VB Script
    Viešojo rakto kriptografija
    Programavimo kalbų istorija
    Dygios JavaScript eilutės
    Iš kur Javos tas lėtumas?
    Unix komandinė eilutė
    Ruby on Rails
    AdvancedHTML
    Vartiklis