<

3. Metadane i adnotacje


Zajmiemy się teraz jednym z najważniejszych nowych elementów Javy - adnotacjami.
Okaże się, że ten prosty w idei mechanizm prowadzi do znaczącego zmniejszenia pracochłonności przy tworzeniu i wdrażaniu aplikacji - szczególnie tych z klasy "enterprise". O niezwykle ważnej roli adnotacji można dobitnie przekonać się przy lekturze wykładu 14 (stanowią one ważny składnik specyfikacji EJB 3.0 i Java Persistance API). Teraz pora na poznanie adnotacji od podstaw.

1. Wprowadzenie


Metadane to dane opisujące dane. W przypadku programowania chodzi o takie metadane, które opisują i uzupełniają kod źródłowy w znaczeniu semantycznym


Coraz częściej metadane są obecne w językach programowania.
W Javie częściowo występowały już wcześniej - w samym środowisku (dokumentacyjne np. @author), a częściowo jako zewnętrzne uzupełnienia (np. XDoclet, JBoss AOP).
W C# nazywają się (nieco nieszczęśliwie) atrybutami.

Java 5 rozszerza i standaryzuje zastosowanie metadanych na platformie Javy poprzez mechanizm adnotacji.

Zastosowania (bardzo różnorodne), m.in.
Mechanizm adnotacji jest najważniejszym praktycznie uzupełnieniem Javy w wersji 5, choćby dlatego, że już teraz kolejne wersje dużych platform, takich jak J2EE, a także pakiety narzędziowe , takie jak JDBC 4.0, intensywnie używają adnotacji.


Zalety adnotacji:
Wady:

2. Rodzaje adnotacji

Można wyróżnić następujące rodzaje adnotacji:

Adnotacje mogą byś stosowane wobec:

Można wyraźnie zaznaczyć do czego odnosi się nowodefiniowana adnotacja za pomocą metaadnotacji @Target (wtedy inne jej zastosowanie będzie wykryte jako błąd w fazie kompilacji).

Możliwe znaczenia (wartości) metaadnotacji @Target

ANNOTATION_TYPE - dana adnotacja adnotuje inną,
PACKAGE - dotyczy pakietu,
TYPE - klas i interfejsów
METHOD - metod
CONSTRUCTOR - konstruktorów
FIELD - pól
PARAMETER - parametrów
LOCAL_VARIABLE - zmiennych lokalnych


Przykład (fragment) definicji adnotacji o nazwie Test, która może dotyczyć tylko metod:

@Target(ElementType.METHOD)
public @interface Test

Uwagi:
  1. adnotacja nie oznaczona znacznikiem @Target ma zastosowanie wszędzie.
  2. aby określić kilka możliwych zastosowań piszemy @Target({ a, b, c } ), gdzie a, b, c to elementy w postaci ElementType.rodzaj, a rodzaj to jeden z PACKAGE, METHOD, FIELD itp.

Siła adnotacji polega na tym, że mogą one być przetwarzane:

Odpowiadają temu trzy polityki utrzymywania adnotacji, specyfikowane przez metaadnotację @Retention.


Polityki utrzymywania adnotacji

RetentionPolicy.SOURCE - tylko w żródle,
RetentionPolicy.CLASS - w klasie skompilowanej, ale niedostępne w fazie wykonania,
RetentionPolicy.RUNTIME - dostępne w fazie wykonania.


Przykład:
@Retention(RetentionPolicy.SOURCE)   // utrzymanie tylko w kodzie adnotacji AdapterFor
public interface @AdapterFor               // zostanie przetworzona przez narzędzia w fazie
                                                        // kompilacji

Wśrod metaadnotacji dostępne są jeszcze:

@Documented  - mowi o tym, że dokumentacja działania adnotacji ma być włączona do dokumentacji wszystkich oznaczanych przez nią elementów

@Inherited - mówi o tym, że oznaczana przez nią adnotacja (zaznaczająca klasy) ma być dziedziczona przez podklasy zaznaczonych klas.


Zestaw regularnych, wbudowanych w Javę adnotacji jest na razie bardzo niewielki i obejmuje:

@Deprecated - zaznacza dowolny element jako spadkowy ("przestarzały"),

@SupressWarnings - blokuje ostrzeżenia (podanego typu) ze strony kompilatora,

@Override - stosowana  wobec metod, oznacza intencję programisty przedefiniowania metody z nadklasy, dzięki czemu jest możliwość sprawdzenia w fazie kompilacji czy programista nie popełnił błędu,

Ta ostatnia adnotacja jest b. użyteczna i należy ją stosować.
Dzięki temu unikniemy błędów niewykrywalnych nie tylko w fazie kompilacji, ale również czasem w fazie wykonania.

Np.

class Push extends JButton {

    public Dimension getPrefferedSize() { ... }

}

Tutaj nastąpiła pomyłka w nazwie metody - wobec czego pojawia się nowa metoda (nigdy nie wołana), a właściwa (getPreferredSize()) nie jest przedefiniowana. Błędu nie ma  ani w kompilacji ani w fazie wykonania (oprócz - być może nie zawsze, nie od razu, nie w każdych okolicznościach - widocznych niewłaściwych rozmiarów przycisku).

Gdy napiszemy:

class Push extends JButton {

  @Override  public Dimension getPrefferedSize() { ... }



to kompilator wykryje błąd i powiadomi nas o tym.


Jak widać, składnia zastosowania adnotacji jest bardzo prosta.
Adnotacje zaczynają sie znakiem @.
Adnotacje poprzedzają inne kwalifikatory elementów (klas, metod, pól).




3. Definiowanie adnotacji

Adnotacje są definiowane jako swego rodzaju interfejsy, za pomocą słowa @interface
Wewnątrz takiego interfejsu dostarcza się deklaracji danych, które adnotacja może zawierać.


[ew. kwalifikacja dostępu]  @interface NazwaAdnotacji {
    deklaracja1
    deklaracja2
     . . .
    deklaracjaN
}


Każda deklaracja ma postać:

 typ nazwaDanej();


albo

typ nazwaDanej() default wartość_domyślna;



Przy zastosowaniu adnotacji możemy podać konkretne dane:

@NazwaAdnotacji(nazwaDanej1=wartość1, nazwaDanej2=wartość2, . . .)

Typy danych w adnotacji mogą być następujące:


Przykład:
public @interface Opis {
    String text() default "Brak opisu";
    int version() default 1;
}


// i zastosowanie np. do opisu klasy:

@Opis(text="Klasa warzyw", version=2)
public class Warzywa { ... }




Można też tak:

@Opis(version=5, text="Klasa warzyw")

albo:

@Opis(text="Klasa warzyw")

tu pominięte dane przyjmą wartości domyślne,
w szczególności:


@Opis
oznacza to samo co

@Opis()
i co

@Opis(text="Brak opisu", version=1)


Mamy też szczególny przypadek, kiedy można pominąć nazwę danych i znak =. Mianowicie:

public @interface JakaśAdnotacja {
   jakiśTyp value()
}


Wtedy można pisać np. tak (jeśli jakiśTyp to int):

@JakaśAdnotacja(111)

nadając danej oznaczanej przez value wartość 111.


Dane konkretnych adnotacji mogą być uzyskiwane od nich (w fazie wykonania lub kompilacji) poprzez odwołania do "metod" interfejsu definiującego adnotację
(np. jeśli annot - jest uzyskaną metodami refleksji lub przez narzędzia przetwarzania w fazie kompilacji adnotacją @Opis, to możemy wołać:

String op = annot.txt();
int v = annot.version();


4. Przetwarzanie adnotacji w fazie wykonania

Aby przetwarzać - i odpowiednio stosować - adnotacje w fazie wykonania, należy:

Dla każdego elementu programu (uzyskanego dynamicznie): Class, Method, Field itp. możemy użyć metod, które zwracają informację o adnotacjach zastosowanych wobec tego elementu.

<T extends Annotation>
T
getAnnotation(Class<T> annotationType)
          Zwraca adnotację podanego typu, albu null, jeśli element nie jest oznaczony adnotacją.
 Annotation[] getAnnotations()
          Zwraca wszystkie adnotacje dla tego elementu
 Annotation[] getDeclaredAnnotations()
          Zwraca wszystkie adnotacje bezpośrednio zastosowane (z pominieciem dziedziczonych)
 boolean isAnnotationPresent(Class<? extends Annotation> annotationType)
          Zwraca true, jeśli wobec elementu zastosowano adnotację podanego typu.

Co to jest typ adnotacji?
Nic innego jak nazwa interfejsu który ją definiuje. Np. dla adnotacji, zdefiniowanej jako:

public @interface Opis { ... }

typem jest:

Opis.class
albo
Class.forName("Opis");

Uwagi:

Przykład.

Stworzymy i zastosujemy adnotację, dzięki której w prosty sposób w kodzie źródłowym będziemy ustalać do jakich kontenerów mają być wkładane wybrane komponenty GUI.
Adnotację nazwiemy Loc (od locate).

import java.lang.annotation.*;
import java.awt.*;

@Target(ElementType.FIELD)             // do oznaczania pól
@Retention(RetentionPolicy.RUNTIME)    // faza wykonania

public @interface Loc {
   String to();
}

Adnotacja ma jedną "daną", swoisty atrybut, o nazwie to, który będzie reprezentował za każdym razem nazwę zmiennej oznaczającej kontener do którego dany komponent (oznaczony tą adnotacją)  ma być dodany.

A oto jej zastosowanie:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.lang.reflect.*;

public class Annot0 extends JFrame {

  JComponent cp = (JComponent) getContentPane();

  @Loc(to="cp") JPanel p1 = new JPanel();
  @Loc(to="cp") JPanel p2 = new JPanel();

  @Loc(to="p1") JButton b1 = new JButton("Przycisk 1");
  @Loc(to="p1") JButton b2 = new JButton("Przycisk 2");
  @Loc(to="p2") JButton b3 = new JButton("Przycisk 3");
  @Loc(to="p2") JButton b4 = new JButton("Przycisk 4");
  @Loc(to="p2") JButton b5 = new JButton("Przycisk 5");

  public Annot0() {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
    try {
      locateComponents();   // metoda ta zajmie się wkładaniem do kontenerow
    } catch(Exception exc) {
        exc.printStackTrace();
    }
    pack();
    show();
  }
//....
}


Metoda locateComponents() nie jest trudna do napisania, jeśli tylko choć trochę władamy metodami refleksji:

  private void locateComponents() throws Exception {
    Class klasa =  getClass();
    for (Field f : klasa.getDeclaredFields()) {   // po polach klasy
      Loc annot = f.getAnnotation(Loc.class);     // dla f uzyskać anotację Loc
      if (annot == null) continue;                // nie ma - następne pole
      System.out.println(annot);                  // zobaczmy jak wygląda
      String contName = annot.to();               // od adnotacji: nazwa kontenera
      Field contField = klasa.getDeclaredField(contName);  // pole, ktore go deklaruje
      Object container = contField.get(this);  // sam obiekt-kontener
      Method m = container.getClass().getMethod("add", Component.class); // metoda add
      m.invoke(container, f.get(this)); // i jej wywołanie - dodajemy komponent
    }
}


Zobacz demo programu

Można powiedzieć, że ten sposób programowania, szczególnie w dużych projektach może być bardzo użyteczny, bowiem łatwo pozwala zmieniać ułożenie komponentów.
Sama metoda locateComponents() może być nieco zmodyfikowana, tak by mogła znaleźć się w jakiejś klasie narzędziowej i być zastosowana wobec dowolnych klas o podobnej j.w. konstrukcji.

5. Przetwarzanie adnotacji w fazie kompilacji

Niewątpliwie możliwość przetwarzania adnotacji  w fazie kompilacji  jest najbardziej ekscytująca.
Umożliwia np. generowanie dodatkowych klas czy niezbędnych plików zewnętrznych.

Do takiego przetwarzania powołane są odpowiednie dodatkowe narzędzia.

Należy do nich
apt
czyli "Annotation Processing Tool", dostępny w pakiecie Javy.

Skąd apt ma wiedzieć, jak należy przetwarzać nasze adnotacje?
Otoż musimy mu to sami powiedzieć, dostarczając tzw. procesora adnotacji.


Procesor adnotacji definiujemy implementując we własnej klasie interfejs AnnotationProcessor i dostarczając definicji jedynej jego metody public void process()

Apt uzyskuje dostęp do naszego procesora za pomocą metody getProcessorFor(...) z klasy, którą też musimy zdefiniować i która stanowi fabrykę procesorów - implementację interfejsu AnnotationProcessorFactory.

Podczas wywołania tej metodzie przekazywane jest środowisko działania dla procesora - AnnotationProcessorEnvironment. Z tego środowiska nasz procesor może odczytać wszystkie niezbędne informacje o strukturze kodu źródłowego oraz sposobach tworzenia i generowania nowych plików, a także raportowania błędów i ostrzeżeń.

Apt używa naszego procesora, który np. produkuje dodatkowe pliki, po czym wykonuje wszystkie niezbędna kompilacje.

Uwaga: konieczne są importy pakietów z tools.jar - zob. przykładowy kod źródłowy.


Jako przykład rozpatrzmy prosty sposób zapisu klas typu JavaBeans.
Na podstawie tych bardzo uproszczonych zapisów będą mogły być generowane "prawdziwe" duże klasy JavaBeans. Przykład jest raczej ilustracyjny i do dalszego znaczącego rozszerzania i modyfikowania.

Umówimy się, że na podstawie zapisu:

@BeanTemplate class NazwaBeanaTemplate {
       typ1 nazwa1;
       typ2 nazwa2;
       ....
       tyoN nazwaN;
}

ma być wygenerowana klasa JavaBean o nazwie NazwaBeana, zawierające właściwe deklaracje pól (podanych) oraz odpowiednie dla nich settery i gettery.

Musimy więc mieć adnotację @BeanTemplate:
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface BeanTemplate {
}
 
 i odpowiedni procesor, dostarczany przez naszą implementację fabryki procesorow adnotacji:

import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.*;
import com.sun.mirror.type.*;
import com.sun.mirror.util.*;

import java.beans.*;
import java.io.*;
import java.util.*;


public class BeanTemplateAnnotationFactory
       implements AnnotationProcessorFactory
{

   // Typ adnotacji
   private final String annoType = "BeanTemplate";

   public Collection<String> supportedAnnotationTypes() {
      return Arrays.asList(annoType);
   }

   public Collection<String> supportedOptions() {
      return Arrays.asList(new String[0]);
   }

   public AnnotationProcessor   // ważna metoda getProcessorFor
          getProcessorFor(Set<AnnotationTypeDeclaration> atds,
                          final AnnotationProcessorEnvironment env)
{
return new AnnotationProcessor() { // procesor - w klasie wewnętrznej public void process() { // deklaracje markowane adnotacją annoType (teraz "BeanTemplate") Collection<Declaration> dcls = env.getDeclarationsAnnotatedWith( (AnnotationTypeDeclaration) env.getTypeDeclaration(annoType)); for (Declaration d : dcls) {  if (d instanceof ClassDeclaration) { // jeżeli klasa ClassDeclaration cdcl = (ClassDeclaration) d; String name = cdcl.getSimpleName(); if (!name.endsWith("Template")) { // od env uzyskujemy środki raportowania błędów, ostrzeżeń, info. env.getMessager().printWarning("Wadliwa nazwa klasy bean template"); continue; } String qname = cdcl.getQualifiedName(); env.getMessager().printNotice(qname); qname = qname.substring(0, qname.lastIndexOf("Template")); name = name.substring(0, name.lastIndexOf("Template")); try { // od env uzyskamy plik i skojarzony z nim PrintWriter PrintWriter out = env.getFiler().createSourceFile(qname); // generujemy świeży kod na podstawie info o zaznaczonej klasie out.println("public class " + name + " {" ); Collection<FieldDeclaration> fdcl = cdcl.getFields(); for (FieldDeclaration f : fdcl) { String fname = f.getSimpleName(); String mname = Character.toUpperCase(fname.charAt(0)) + fname.substring(1); String ftype = f.getType().toString(); out.println(" private " + ftype + " " + fname + ";"); out.println(" public " + ftype + " get" + mname + "() { return " + fname + "; }"); out.println(" public void set" + mname + "(" + ftype + " v) { " + fname + " = v; }"); } out.println("}"); out.close(); } catch(IOException exc) { exc.printStackTrace(); } } else // jeśli nie byla klasa, to ktoś się pomylił i zazn. interfejs env.getMessager().printWarning("Adnotacja dotyczy interfejsu"); } } }; } }

Po skompilowaniu tej fabryki umieszczamy wynik kompilacji w katalogu, w którym znajdują się  inne pliki źródłowe. Np. takie przykładowe zastosowanie:

Uproszczona definicja beana:
import java.awt.*;
@BeanTemplate public class Bean1Template {
  String txt;
  Color color;
}
Ponieważ, zgodnie z umową ma z tego powstać prawdziwa klasa JavaBean, to plik ją wykorzystujący będzie się odwoływał do Bean1, a nie Bean1Template:

import java.awt.*;
public class Annot1 {


  public Annot1() {
    Bean1 b = new Bean1();
    b.setTxt("Pies");
    b.setColor(Color.BLUE);

    System.out.println(b.getTxt() + "\n" + b.getColor());

  }

  public static void main(String[] args) {
     new Annot1();
  }

}

Aby to wszystko ze sobą polaączyć, musimy wywołać apt, podając mu lokalizację fabryki procesorow:

apt -factory BeanTemplateAnnotationFactory *.java

APT nie tylko przeprowadzi analizę adnotacji i odpowiednie ich substytucje, nie tylko skorzysta z naszego procesora i pozwoli mu zapisać nowy plik źródłowy, ale również wywoła normalny kompilator javy, aby wszystko złożył do kupy.

W efekcie uzyskamy nowowygenerowany plik źródłowy (który "zastępuje" uproszczony "template"):
public class Bean1 {
  private java.awt.Color color;
  public java.awt.Color getColor() { return color; }
  public void setColor(java.awt.Color v) { color = v; }
  private java.lang.String txt;
  public java.lang.String getTxt() { return txt; }
  public void setTxt(java.lang.String v) { txt = v; }
}
a w wyniku kompilacji wszystkiego razem działający plik Annot1.class, który wyprodukuje napis:

Pies
java.awt.Color[r=0,g=0,b=255]



Zobacz multimedialną prezentację użycia APTr



Podany przykład jest prosty, a także trochę dyskusyjny. Ale dobrze pokazuje potencjalnie olbrzymie możliwości tkwiące w przetwarzaniu adnotacji w fazie kompilacji.


6. Adnotacje a transformowanie kodu binarnego


Polityka utrzymywania adnotacji oznaczana przez metaadnotację @Retention jako RetentionPolicy.CLASS utrzymuje adnotację w klasie (kodzie binarnym), ale nie udostępnia jej mechanizmom refleksji.

Taki typ adnotacji może być wykorzystywany przez różne narzędzia modyfikacji kodu binarnego klas. Należą do nich takie narzędzia jak:

Javassist pozwala na bardzo łatwe transformowanie kodu binarnego klas, polegające m.in na:
Modyfikacje są łatwe, ponieważ możemy zapisać je w naturalnym języku Javy.
Javassist ma pewne ograniczenia.
Znacznie potężniejszym narzędziem jest BCEL (Byte Code Engineering Library), który pozwala robić praktycznie wszystko, ale wymaga zapisów w postaci podobnej do bajtkodu.

Zobacz więcej o:
Javassist - http://jboss.com/products/javassist
BCEL - http://jakarta.apache.org/bcel/


7. Podsumowanie


Zapoznaliśmy się z mechanizmem adnotacji w Javie, w szczególności:


8. Zadania


Zadanie 1

Przejrzeć różne technologie Javy (np. JDBC, EJB, JPA) i zidentyfikować stosowane adnotacje, które stają się standardowymi dla tych technologii.


Zadanie 2

Opracować przykładowe zastosowania adnotacji (programy przykładowe) w fazie wykonania i kompilacji.

Zadanie 3

Zbadać i wypróbować możliwości binarnych iniekcji za pomocą Javassist. Jakie mogą być ich zastosowania?