~/home of geeks

Mit Interfaces und Annotationen in Spring markieren

· 653 Wörter · 4 Minute(n) Lesedauer

In einer recht großen Spring-Applikation kann es mal notwendig sein, Komponenten im ApplicationContext zu markieren, um sie bei Bedarf wiederzufinden. Mit Interfaces und eigenen Annotationen kann man hier Komponenten markieren und wiederfinden.

Wenn man bestimmte Beans im ApplicationContext suchen will, ist es sehr nützlich diese mithilfe eines Interfaces abzufragen. Tatsächlich ist das m. E. die beste Möglichkeit, sich zusammen mit der ComponentScan-Funktion eine Art Plugin-Architektur mit Spring zu bauen. Man definiert einfach ein Interface und fragt den ApplicationContext nach allen Implementierungen:

public interface MyInterface {
    void doSomething();
}

Alle Implementierungen können anschließend abgerufen werden:

@Service
@Log4j2
public class MyMarkerService {
    private final ApplicationContext applicationContext;
    private List<MyInterface> markedWithInterface;

    @Autowired
    public MyMarkerService(final ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public synchronized List<MyInterface> getMarkedWithInterface() {
        if (markedWithInterface == null) {
            markedWithInterface = new ArrayList<>(applicationContext.getBeansOfType(MyInterface.class).values());
            log.info("Found {}: {}", MyInterface.class.getSimpleName(), 
                    converters.stream().map(Object::getClass).map(Class::getSimpleName).collect(Collectors.toList()));
        }
        return new ArrayList<>(markedWithInterface);
    }
}

Das Ganze kann natürlich nur funktionieren, wenn die zu markierenden Klassen Komponenten im Spring-ApplicationContext sind, weil sie z. B. eine Component-Annotation haben.

@Component
public class ImplementationOne implements MyInterface {
    public void doSomething() {
        System.out.println("Hello!");
    }
}
@Component
public class ImplementationTwo implements MyInterface {
    public void doSomething() {
        System.out.println("Adios!");
    }
}

Diesen Mechanismus kann man natürlich auch mit beliebigen Marker-Interfaces machen, also Interfaces analog zu java.io.Serializable, die keine Methoden definieren.

In meinem Fall ging es um org.springframework.core.convert.converter.Converter<S, T>, welches lediglich definiert, dass ein Objekt vom Format S(ource) ins Format T(arget) konvertiert werden kann. Einige der Converter waren nur dazu bestimmt, einzelne Felder, wie ein Datum, zu konvertieren, andere konnten ganze Objektstrukturen konvertieren. Entsprechend ihrer Funktionalität mussten sie in der Applikation an unterschiedlichen Stellen als Converter registriert werden. Daher half mir der Abruf via Interface allein nicht weiter, ich musste innerhalb der Implementierungen weiter unterscheiden.

Ich konnte ein weiteres Interface definieren, welches lediglich als Marker fungiert, oder Annotationen verwenden. Der Vorteil einer Annotation ist, dass dieser zusätzliche Parameter mitnehmen kann und so z. B. weitere Unterscheidungskriterien ermöglicht.

@Retention(RetentionPolicy.RUNTIME)
public @interface MyMarker {}

Wichtig an der Annotation ist, dass diese die RetentionPolicy.RUNTIME hat.

@Service
@Log4j2
public class MyMarkerService {
    private final ApplicationContext applicationContext;
    private List markedWithAnnotation;

    @Autowired
    public MyMarkerService(final ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public synchronized List getMarkedWithAnnotation() {
        if (markedWithAnnotation == null) {
            markedWithAnnotation = new ArrayList<>(applicationContext.getBeansWithAnnotation(MyMarker.class).values());
            log.info("Found {}: {}", MyMarker.class.getSimpleName(), 
                    converters.stream().map(Object::getClass).map(Class::getSimpleName).collect(Collectors.toList()));
        }
        return new ArrayList<>(markedWithAnnotation);
    }
}

Nun kann ich meine Komponenten anhand der Annotation ermitteln.

@Component
@MyMarker
public class ImplementationOne implements MyInterface {
    public void doSomething() {
        System.out.println("Hello!");
    }
}

Aber leider kann man nun alle Klassen mit der Annotation markieren und ich kann das an der Annotation selber nicht einschränken. Wenn jetzt ein Entwickler eine Klasse annotiert, die nicht MyInterface implementiert, kann ich Class-Cast-Probleme kriegen.

Bei den Convertern entschied ich mich dazu, beim Abruf der infrage kommenden Converter beides zu kombinieren: Abfrage von Komponenten, die sowohl ein Interface als auch eine Annotation haben.

@Service
@Log4j2
public class MyMarkerService {
    private final ApplicationContext applicationContext;
    private List<MyInterface> markedWithInterfaceAndAnnotation;

    @Autowired
    public MyMarkerService(final ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public synchronized List<MyInterface> getMarkedWithInterfaceAndAnnotation() {
        if (markedWithInterfaceAndAnnotation == null) {
            final Set<Object> collection = new LinkedHashSet<>();
            // Alle Interface-Instanzen
            collection.addAll(applicationContext.getBeansOfType(MyInterface.class).values());
            // Nur die mit Annotation behalten
            collection.retainAll(applicationContext.getBeansWithAnnotation(MyMarker.class).values());
            markedWithInterfaceAndAnnotation = new ArrayList<>((Collection<MyInterface>) (Collection<?>) collection);
              log.info("Found {}: {} of Interface {}", 
                      MyMarker.class.getSimpleName(), converters.stream().
                              map(Object::getClass).
                              map(Class::getSimpleName)
                              .collect(Collectors.toList()), 
                      MyInterface.class.getSimpleName());
        }
        return new ArrayList<>(markedWithInterfaceAndAnnotation);
    }
}

An dieser Stelle war ich erst einmal zufrieden. Praktisch wäre es natürlich auch, wenn man Fehlermeldungen generiert, wenn eine nicht erlaubte Klasse die Annotation hat.

Der Annotationsansatz an sich bietet aber noch mehr Möglichkeiten. Über eigene Eigenschaften könnte man z. B. die Markierung auf einen übergebenen String ausweiten und so noch mehr Klassen von Komponenten unterscheiden:

@Retention(RetentionPolicy.RUNTIME)
public @interface MyMarker {
    String[] type() default "";
}

Mit einer Verwendung, die in etwa so aussehen könnte:

@Component
@MyMarker(type={"field", "test"})
public class ImplementationOne implements MyInterface {
    public void doSomething() {
        System.out.println("Hello!");
    }
}
@Component
@MyMarker(type={"class", "test"})
public class ImplementationTwo implements MyInterface {
    public void doSomething() {
        System.out.println("Adios!");
    }
}

Und dem Aufruf myMarkerService.getMarked("test");