Wenn du in Java schon ein paar Klassen geschrieben hast, bist du Annotationen ziemlich sicher begegnet: @Override, @SuppressWarnings, vielleicht auch @Deprecated. Am Anfang wirken die Dinger wie „Meta-Kram“, den man halt drüber schreibt. In Wahrheit sind Annotationen ein sehr pragmatisches Werkzeug: du hängst einer Klasse, Methode oder einem Feld zusätzliche Informationen an, ohne deine eigentliche Logik damit zu vermischen.

Der Clou: diese Infos können je nach Typ entweder vom Compiler ausgewertet werden (z.B. @Override) oder zur Laufzeit von Frameworks und Tools gelesen werden (z.B. für Dependency Injection, Validierung, Mapping). Und genau da wird es spannend: du kannst dir solche Annotationen auch selbst bauen.

 

Was sind Annotationen eigentlich?

Eine Annotation ist erstmal nur ein Marker mit optionalen Parametern. Technisch ist das ein Interface-ähnlicher Typ, der mit @interface definiert wird. Du platzierst ihn z.B. auf einer Methode oder Klasse, und irgendwer „liest“ diese Information später.

Wichtig ist dabei: eine Annotation tut von sich aus nichts. Sie ist wie ein Aufkleber. Wert bekommt sie erst, wenn Code (Compiler, Tooling oder dein eigener Code) diesen Aufkleber auswertet.

 

Was kann man damit machen?

In der Praxis lösen Annotationen meistens eines dieser Probleme:

- Du willst klare, lesbare Metadaten direkt am Code haben („diese Methode ist deprecated“, „dieses Feld muss validiert werden“).
- Du willst Konfiguration aus externen Dateien in den Code ziehen (statt XML oder Properties-Orgie).
- Du willst Verhalten deklarativ beschreiben („diese Methode ist ein HTTP-Endpoint“, „diese Klasse ist ein Service“).
- Du willst Tools/Frameworks eine stabile Struktur geben, um Klassen zu finden und zu verarbeiten (Scanning + Reflection).

Gerade Frameworks wie Jakarta EE/Spring leben davon: du schreibst wenig Glue-Code, weil das Framework Annotationen scannt und daraus „macht“: DI, REST-Routen, Security, Transaktionen, Validierung, Mapping usw.

 

Wo wirken Annotationen? TARGET und RETENTION

Beim Erstellen eigener Annotationen sind zwei Meta-Annotationen entscheidend:

- @Target: wo darf die Annotation verwendet werden? (Klasse, Methode, Feld, Parameter, ...)
- @Retention: wie lange bleibt die Annotation erhalten?

Retention hat drei typische Stufen:

- SOURCE: nur im Quellcode, verschwindet beim Kompilieren (typisch für reines Tooling).
- CLASS: landet in der .class-Datei, ist aber zur Laufzeit nicht via Reflection sichtbar (Default, wenn du nichts angibst).
- RUNTIME: bleibt zur Laufzeit sichtbar und kann per Reflection gelesen werden (das brauchst du für Framework- und Runtime-Logik).

 

Eine eigene Annotation erstellen

Wir bauen jetzt einen einfachen, aber ziemlich nützlichen Anwendungsfall: ein Mini-Command-System. Du markierst Methoden mit @Command, und ein kleiner Dispatcher findet diese Methoden zur Laufzeit und führt sie aus. Das ist simpel, wirkt aber direkt „frameworkig“ und zeigt, wofür Annotationen gut sind.

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

@Retention(RetentionPolicy.RUNTIME) 
@Target(ElementType.METHOD) 
public @interface Command { 
  String name(); 
  String description() default ""; 
}

Ein paar Dinge dazu:

- Wir setzen RUNTIME, weil wir die Annotation später per Reflection auslesen wollen.
- Wir erlauben die Annotation nur an Methoden.
- Die Annotation hat Parameter: name ist Pflicht, description hat einen Default.

 

Annotation verwenden

Jetzt schreiben wir eine Klasse mit ein paar Befehlen:

public class AppCommands { 
  @Command(name = "hello", description = "Sagt hallo") 
  public void hello() { 
    System.out.println("Hallo!"); 
  } 

  @Command(name = "time", description = "Zeigt Systemzeit in ms") 
    public void time() { 
    System.out.println(System.currentTimeMillis()); 
  } 
}

Bis hier passiert noch nichts Magisches. Wir haben nur Metadaten angebracht.

 

Annotationen zur Laufzeit auslesen

Jetzt kommt der Teil, der Annotationen „aktiv“ macht: Reflection. Wir scannen Methoden, prüfen ob @Command drauf ist und merken uns den Namen.

import java.lang.reflect.Method; 
import java.util.HashMap; 
import java.util.Map; 

public class CommandDispatcher { 
  private final Object target; 
  private final Map<String, Method> commands = new HashMap<>(); 

  public CommandDispatcher(Object target) { 
    this.target = target; 

    for (Method m : target.getClass().getDeclaredMethods()) { 
      Command cmd = m.getAnnotation(Command.class); 

      if (cmd != null) { 
        commands.put(cmd.name(), m); 
        } 
    } 
  } 

  public boolean run(String name) { 
    Method m = commands.get(name); 

    if (m == null) {
      return false; 
    }

    try { 
      m.invoke(target); 
      return true; 

    } catch (Exception e) { 
      throw new RuntimeException("Command failed: " + name, e); 
    } 
  } 
}

Ein paar praxisnahe Punkte dazu:

- getDeclaredMethods() liefert alle Methoden der Klasse (ohne Vererbung). Für den Anfang reicht das.
- m.getAnnotation(...) gibt dir entweder die Annotation oder null.
- invoke führt die Methode aus. Hier nur ohne Parameter, damit es simpel bleibt.

 

Das Ganze benutzen

Ein minimales Main-Programm, das den Dispatcher nutzt:

public class Main { 

  public static void main(String[] args) { 
    CommandDispatcher dispatcher = new CommandDispatcher(new AppCommands()); 
    String cmd = (args.length > 0) ? args[0] : "hello"; 
    boolean ok = dispatcher.run(cmd); 

    if (!ok) { 
      System.out.println("Unbekannter Command: " + cmd); 
    } 
  } 
}

Beispielaufrufe:

$ java Main hello Hallo! 
$ java Main time 1734086400000

Das ist ein bewusst kleines Beispiel, aber das Prinzip ist exakt das, was du bei grösseren Frameworks wiederfindest: Klassen/Members werden gescannt, Annotationen werden ausgelesen, und daraus wird Verhalten abgeleitet.

 

Warum ist das ein „guter“ Anwendungsfall?

Weil du hier sofort siehst, was Annotationen dir bringen:

- Du trennst Deklaration (welche Methode ist ein Command?) von der Ausführung (Dispatcher).
- Du musst keine riesige if/else- oder switch-Struktur pflegen, in der du jeden neuen Befehl manuell registrierst.
- Du kannst mit description später z.B. automatisch eine Hilfe-Liste generieren, ohne doppelten Text in mehreren Stellen.

Und ja: genau dieses Muster skaliert. Heute „Mini-Commands“, morgen „REST-Endpunkte“, „Handler“, „Jobs“, „Validierungen“ oder „Security-Rollen“.

 

Ein paar typische Stolperstellen

Wenn du eigene Annotationen baust, sind das die Klassiker:

- Retention vergessen: ohne RUNTIME ist zur Laufzeit nichts per Reflection sichtbar, und du suchst dir einen Wolf.
- Zu viel Magie: Annotationen sind super, aber wenn die Auswertung zu komplex wird, wird Debugging unangenehm. Halte den Mechanismus transparent.
- Fehlende Konventionen: wenn du dich auf Namen wie name() oder Defaults verlässt, dokumentiere das kurz (oder setze sinnvolle Defaults).

 

Fazit

Annotationen sind keine Deko. Sie sind ein sauberes Mittel, um Metadaten direkt am Code zu platzieren und daraus später Verhalten abzuleiten. Sobald du verstanden hast, dass eine Annotation nur „Daten“ ist und der eigentliche Wert aus der Auswertung kommt, kannst du sie sehr bewusst einsetzen: für klare APIs, weniger Boilerplate und besser wartbaren Code.

Wenn du Lust hast, das Beispiel auszubauen: erlaube Commands mit Parametern, generiere automatisch help aus description, oder scanne mehrere Klassen. Das ist ein super Übungsfeld, um Reflection, Design und saubere Grenzen zwischen Deklaration und Logik zu lernen.