Liigu peamise sisu juurde

Ise annotatsioonide loomine

Sissejuhatus

Sisseehitatud annotatsioonid on kasulikud, kuid annotatsioonide tegelik jõud seisneb selles, et neid on võimalik ka ise luua. Kohandatud annotatsioon on märgis, mille mõtled ise välja ning mida loetakse seejärel käitusajal selleks, et kood saaks sellele reageerida. Raamistikud defineerivad pidevalt oma annotatsioone, mõndade nendega oleme juba tutvunud (JUniti @Test näiteks). Selles peatükis tutvume annotatsioonide loomisega, järgmises peatükis tutvume, kuidas neid lugeda ja nende põhjal käitumust programmile juurde lisada.

Annotatsiooni loomine

Annotatsioone deklareeritakse @interface märksõnaga:

public @interface Audited {
}

Vaatamata märksõnale ei ole tegemist tavapärases mõttes liidesega. Seda ei saa teostada, sellele saab ainult elemente külge lisada:

@Audited
public void deleteBook(int id) {
...
}

Iseenesest ei tee @Audited veel midagi. Selleks, et sellega midagi teha peame esmalt kompilaatorile ütlema kahte asja:

  • Millistele elementidele seda saab lisada
  • Kui kaua see peab pärast lähtekoodi kompileerimist eksisteerima

@Target ehk millistele elementidele saab annotatsiooni lisada

@Target piirab, millist tüüpi elementidele võib annotatsiooni juurde lisada. Ilma selleta võib annotatsiooni lisada peaaegu kõikjale, mille tulemusena kompilaator ei anna veateadet olukordades, kus seda kasutatakse valedes kohtades.

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
public @interface Audited {
}

Sellega sai @Audited piiratud ainult meetoditel kasutamiseks. Selle lisamine klassile või väljale põhjustaks kompileerimisvea.

ElementType sisaldab endas võimalike sihtkohti, millele annotatsioone saab lisada, nendeks on:

VäärtusKus annotatsioon võib esineda
TYPEKlassid, liidesed, enumid
FIELDVäljad
METHODMeetodid
PARAMETERMeetodi parameetrid
CONSTRUCTORKonstruktorid
LOCAL_VARIABLEKohalikud muutujad

@Target ei ole piiratud ainult ühe sihiga - neid saab kombineerida järgnevalt:

@Target({ ElementType.METHOD, ElementType.FIELD })
public @interface Audited {
}

Lisaks meetoditele võib nüüd @Audited annotatsiooni lisada ka väljadele.

@Retention ehk annotatsiooni eluiga

@Retention määrab annotatsiooni eluea. Võimalik on valida kolme valiku vahel, millest igaüks käitub väga erinevalt:

ReegelEluigaKasutusjuht
SOURCEEemaldatakse koodist kompileerimise ajalTööriistad, mis loevad lähtekoodi
CLASS (vaikimisi)Salvestatakse .class failidesse, kuid ei laeta programmi käitusajalBaitkoodi analüsaatorid
RUNTIMESalvestatakse .class failidesse ja on reflektsiooni kaudu nähtavRaamistikud nagu JUnit, Spring

Kui eesmärk on annotatsioone kasutada loogika lisamiseks, peab valik olema RUNTIME.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
}

Ilma @Retention(RetentionPolicy.RUNTIME)-ita ei ilmu annotatsioon välja läbi getAnnotations() meetodi käitusajal.

Annotatsiooni parameetrid

Annotatsioonid võivad defineerida parameetreid, mida saab kasutada koodis info edastamiseks. Need näevad @interface sees välja nagu tavalised meetodid:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
String reason();
String level() default "info";
}

Kasutuskohas kirjutatakse parameetrid sulgudesse:

@Audited(reason = "GDPR", level = "critical")
public void deleteUser(int id) {
...
}

Parameetritel võivad olla vaikeväärtused, kuid need ei ole kohustuslikud. Vaikeväärtustega parameetreid ei pea annotatsiooni kasutamisel eraldi välja kirjutama, ilma nendeta peab.

value parameeter

Kui annotatsioonil on ainult üks parameeter ning selle nimi on täpselt value, siis võib parameetri nime kasutuskohas ära jätta:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonField {
String value();
}
@JsonField("book_title")
private String title;

See on võrdväärne @JsonField(value = "book_title")-le.

Lubatud tüübid parameetritel

Annotatsiooni parameetrid ei ole suvalised Java väärtused. Lubatud tüübid on:

  • Primitiivtüübid (int, long, boolean, ...)
  • String
  • Class<?> (kasutatakse sageli teistele tüüpidele viitamiseks, näiteks expected = IllegalArgumentException.class)
  • Enum-tüübid
  • Teised annotatsioonitüübid
  • Massiivid ükskõik millisest eelnevalt loetletud tüübist

Tavalist objektiviidet, List-i ega Map-i kasutada ei saa. Sellel on kindel põhjus: annotatsioonid peavad olema klassifailis esitatavad staatilise metaandmestikuna.

Massiivid parameetrina

Massiiviparameetrid võtavad looksulgude sees komadega eraldatud väärtuste loendi:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Tag {
String[] names();
}
@Tag(names = {"slow", "integration"})
public void loadFromDatabase() { ... }

Kui massiivis on ainult üks element, võib looksulud ära jätta: @Tag(names = "slow").

Class parameeter

Class<?> parameeter on loomulik viis teisele tüübi viitamiseks:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Handles {
Class<? extends Throwable> exception();
}
@Handles(exception = IOException.class)
public void onIoFailure(IOException e) { ... }

Käitusajal saab seda annotatsiooni lugev raamistik kätte tegeliku Class<?> objekti ning saab seda kasutada - näiteks koos isAssignableFrom meetodiga - otsustamaks, kuidas meetodit käsitleda.

Praktiline näide

Pannes kõik eelnevalt õpitu kokku, saame tulemuseks midagi sellist:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MaxLength {
int value();
String message() default "Value is too long";
}

Ning seda saab kasutada järgnevalt:

public class BookForm {

@MaxLength(value = 200, message = "Title cannot exceed 200 characters")
private String title;

@MaxLength(100)
private String author;

}

Järgmine samm oleks seda annotatsiooni lugeda ja sellele loogikat juurde lisada, kasutades reflektsiooni - teema, mida käsitleme järgmises peatükis.