Vereinfachte View-Config
 
  Object-Converter ohne Abstraktionen
 
  EntityProcessor Interceptor
 
  DeltaSpike-Data Repository mit Interceptoren

6 CDI Lite

In den bisherigen Kapiteln mag bei manchen der Eindruck entstanden sein, dass CDI sehr umfangreich ist. Die Spezifikation wurde zwar von Grund auf neu geschrieben und leidet nicht unter Altlasten, allerdings sind am Ende auch Konzepte spezifiziert worden, die eher exotisch wirken können. In der Praxis ist es jedoch nicht erforderlich bzw. nur selten sinnvoll alle CDI-Konzepte in einer Applikation zu verwenden. Daher machen wir in diesem Kapitel eine Inventur des Zwischenstandes und vereinfachen die Implementierung von IdeaFork , indem wir nur die für IdeaFork sinnvollen Konzepte behalten.

6.1 Weniger Alternativen

In IdeaFork haben wir das Konzept alternativer Implementierungen verwendet, um zur Entwicklungszeit einfach zwischen Frameworks wie bspw. GSon und Jackson wechseln zu können. Da wir in IdeaFork recht früh auf @com.fasterxml.jackson.annotation.JsonView zurückgegriffen haben, sind unsere Implementierungen vom ObjectConverter -Interface nicht vollständig portabel. Folglich würde die Verwendung von Gson in IdeaFork aktuell zu einem anderen Resultat führen. Durch die Aktivierung von @JacksonConverter in der Datei beans.xml haben wir diesen Effekt nicht aktiv beobachtet. Sofern wir unsere auf Gson basierte Implementierung nicht ergänzen, haben wir für diese keine Verwendung mehr. Auch die beschriebenen Mechanismen zur dynamischen Bestimmung des aktuellen Export-Formats können zwar in verschiedenen Bereichen vielfältig eingesetzt werden, in IdeaFork können wir uns dennoch auf JSON als Export-Format beschränken. Die Kombination beider Vereinfachungen ermöglicht es neben @JacksonConverter auch den Qualifier @ExternalFormat , das ObjectConverter -Interface, sämtliche JAXB-Annotationen wie bspw. @XmlRootElement und das CurrentObjectConverterProducer -Bean zu entfernen. Übrig bleibt ein einfaches CDI-Bean, welches direkt an Jackson delegiert und in Listing Object-Converter ohne Abstraktionen zu sehen ist. Somit werden wir in diesem Bereich sämtliche Abstraktionen los, die wir primär zur Veranschaulichung diverser CDI-Mechanismen in IdeaFork eingeführt haben.
public class ObjectConverter {
  //...

  public String toString(Object entity, Class typeSafeDataView) {
    try {
      ObjectMapper objectMapper = new ObjectMapper();
      if (typeSafeDataView != null) {
        objectMapper.configure(
          MapperFeature.DEFAULT_VIEW_INCLUSION, false);
        return objectMapper.writerWithView(typeSafeDataView)
                 .writeValueAsString(entity);
      }
      return objectMapper.writeValueAsString(entity);
    } catch (JsonProcessingException e) {
      throw new IllegalArgumentException(e);
    }
  }
}

6.2 Von Decoratoren zu Interceptoren

CDI-Decoratoren gehören zu den Konzepten die in der freien (Applikations-)Wildbahn eher selten anzutreffen sind. In vielen Fällen ist ein einfacher Interceptor ausreichend. In IdeaFork haben wir mit Decoratoren zusätzliche Logik für UserRepository und IdeaRepository umgesetzt. Der primäre Vorteil besteht hierbei in der Typsicherheit von Decoratoren. Würden wir bspw. die Methode GenericRepository#save umbenennen, dann ist sichergestellt, dass diese Änderung in sämtlichen Implementierungen berücksichtigt wird. Hierzu zählt auch GenericRepositoryDecorator . Hätten wir hingegen einen Interceptor, dann müssten wir manuell mit Strings arbeiten, wodurch die Typsicherheit nicht mehr gegeben ist. Der Zusatzaufwand für Decoratoren kann somit durchaus gerechtfertigt sein. In manchen Fällen kann es auch sinnvoll sein auf die Vorteile von Decoratoren zu verzichten. So können wir bspw. GenericRepository , GenericJpaRepository , sowie unsere eigene @Repository -Annotation durch die Verwendung von DeltaSpike-Data entsorgen. Wir sind allerdings nicht nur auf die von DeltaSpike-Data generierte Logik angewiesen. Wollen wir diese mit eigenen Implementierungen kombinieren, dann müssen wir statt Interfaces auf abstrakte Klassen zurückgreifen. Während implementierte Methoden unverändert aufgerufen werden, formt DeltaSpike-Data abstrakte Methoden zu entsprechenden JPA-Queries um. Dieser Mechanismus erlaubt auf einfache Art und Weise eigene Logik mit generierter Logik zu mischen, wodurch wir sämtliche Repository-Beans deutlich vereinfachen können.

 

In einem weiteren Schritt können wir unseren Decorator-Mechanismus für Repositories überarbeiten. Da das org.apache.deltaspike.data.api.EntityRepository -Interface alle Methoden, die wir in unseren Decoratoren bisher "erweitert" haben, enthält und sich diese nicht ändern, können wir die typsicheren Decoratoren bedenkenlos durch einen generischen Interceptor ersetzen.
Listing DeltaSpike-Data Repository mit Interceptoren zeigt eine mögliche Umstellung von UserRepository auf Basis von DeltaSpike-Data. Nachdem wir alle anderen Repositories in IdeaFork ebenfalls umgestellt haben, können wir die zuvor genannten Artefakte, wie bspw. GenericJpaRepository und UserRepositoryDecorator , löschen.
@Monitored
@EntityProcessor

@Transactional
@Repository
public abstract class UserRepository
  implements EntityRepository<User, String> {

  @Inject
  private EntityManager entityManager;

  public User loadByNickName(String nickName) {
    //...
  }

  public User loadByEmail(String email) {
    //...
  }
}
Statt den entfernten Decoratoren können wir einen Interceptor namens EntityProcessorInterceptor einführen. Wie Listing EntityProcessor Interceptor verdeutlicht, müssen wir bei EntityProcessorInterceptor auf Strings zurückgreifen, wodurch die Implementierung nicht typsicher ist. In unserem Fall nutzen wir jedoch den Vorteil, dass die für uns interessanten Methoden durch DeltaSpike-Data vordefiniert sind und sich somit nicht ändern.
@Interceptor
@EntityProcessor
public class EntityProcessorInterceptor implements Serializable {

  @Inject
  private BeanManager beanManager;

  @Inject
  @Default
  private Event<UserChangedEvent> userChangedEvent;

  @Inject
  @Default
  private Event<IdeaChangedEvent> ideaChangedEvent;

  @AroundInvoke
  public Object intercept(InvocationContext ic) throws Exception {
    boolean saveMethod = false;
    boolean validateEntityParameter = false;

    Object[] parameters = ic.getParameters();
    if (parameters.length == 1) {
      Class parameterType = resolveParameterType(ic.getTarget());

      if (parameterType != null &&
          BaseEntity.class.isAssignableFrom(parameterType)) {

          String methodName = ic.getMethod().getName();

          if ("save".equals(methodName)) {
            saveMethod = true;
            validateEntityParameter = true;
          } else if ("remove".equals(methodName) ||
              "attachAndRemove".equals(methodName)) {

            validateEntityParameter = true;
          }
      }
    }

    if (validateEntityParameter) {
      checkEntity((BaseEntity) ic.getParameters()[0]);
    }

    Object result = ic.proceed();

    if (saveMethod) {
      if (parameters[0] instanceof User) {
        broadcastUserChangedEvent((User) ic.getParameters()[0]);
      } else if (parameters[0] instanceof Idea) {
        broadcastIdeaChangedEvent((Idea) ic.getParameters()[0]);
      }
    }

    return result;
  }

  private void checkEntity(BaseEntity entity) {
    //...
  }

  private Class resolveParameterType(Object target) {
    //...
  }

  private void broadcastUserChangedEvent(User entity) {
    UserChangedEvent userChangedEvent = new UserChangedEvent(entity);
        this.userChangedEvent.fire(userChangedEvent);
  }

  public void broadcastIdeaChangedEvent(Idea entity) {
        IdeaChangedEvent ideaChangedEvent = new IdeaChangedEvent(entity);
        this.ideaChangedEvent.fire(ideaChangedEvent);
  }
}
Durch die Umstellung auf einen Interceptor und auf DeltaSpike-Data hat sich der auf Reflection basierte Teil von GenericJpaRepository#detectConcreteEntityType nach EntityProcessorInterceptor#resolveParameterType verlagert. Somit haben wir zwar einige Teile wesentlich vereinfacht, aber ein kleiner Teil mit Reflection ist geblieben. Diesen könnten wir nur mit spezialisierten Interceptoren vermeiden, welche allerdings etwas aufwändiger wären.

6.3 Weniger ist mehr

Genaugenommen können wir in IdeaFork auch auf Interceptor-Strategies verzichten, da wir in einem der nachfolgenden Schritte das ideafork-core -Modul auflösen werden und die Anpassbarkeit der vorkonfigurierten MonitoredInterceptor -Klasse an Bedeutung verliert. Folglich können wir das Interface MonitoredInterceptorStrategy entfernen und die dazugehörige Implementierung nach MonitoredInterceptor verschieben.

 

In anderen Fällen, wie bei den Project-Stages von DeltaSpike, ist es oftmals ausreichend bestehende Mechanismen unverändert zu verwenden. So können wir in IdeaFork die vorgegebenen Project-Stages von DeltaSpike ohne die CustomProjectStage -Erweiterung einsetzen. Besonders in großen Projekten kann es erforderlich werden zusätzliche Stages zu definieren. DeltaSpike ist für solche Fälle erweiterbar und folglich gut für Anforderungen realer Applikationen gerüstet. In IdeaFork ist die Unterscheidung von Project-Stage Development , UnitTest und Production jedoch ausreichend.

6.4 Konventionen einhalten

DeltaSpike ist sehr flexibel und anpassbar. Eine Anpassungsmöglichkeit haben wir bei der typsicheren View-Config kennengelernt. In wenigen Schritten haben wir die Pfadkonvention eines Teilbereiches abändern können. Halten wir uns jedoch an die vordefinierten Konventionen, dann ist die resultierende View-Config einfacher und transparenter. Listing Vereinfachte View-Config zeigt den vereinfachten Ausschnitt für IdeaFork .
@ViewConfigRoot(
  configDescriptorValidators = IdeaForkViewMetaDataValidator.class)
@View(navigation = REDIRECT)
public interface Pages extends ViewConfig {
  @ViewControllerRef(IndexViewCtrl.class)
  class Index implements Pages {}

  @Secured(UserAwareAccessDecisionVoter.class)
  interface SecuredPages extends Pages {}

  interface User extends Pages {
    @EntryPoint
    class Login extends DefaultErrorView {}

    @EntryPoint
    class Registration implements User {}

    class Profile implements SecuredPages {}
  }

  //...

  interface Promotion extends SecuredPages {
    interface Selection extends Promotion {
      @View(viewParams = INCLUDE)
      @NavigationParameter(key = "searchHint", value = "*")
      @ViewControllerRef(PromotionRequestListViewCtrl.class)
      class List implements Selection {}

      @View(name = "promote")
      class SelectPromotion implements Selection {}
    }

    @Folder(name = "wizard")
    @Wizard
    interface PromotionWizard extends Promotion {
      @EntryPoint
      @ViewControllerRef(PromotionWizardCtrl.class)
      class Step1 implements PromotionWizard {}

      class Step2 implements PromotionWizard {}

      @View(name = "summary")
      class FinalStep implements PromotionWizard {}
    }
  }
}
Natürlich müssen die JSF-Seiten passend zur neuen View-Config ebenfalls verschoben werden, damit die Applikation weiterhin wie gewünscht funktioniert. Bei dieser Umstellung wird auch eine der Stärken von View-Configs erneut sichtbar. Während die Existenz der konfigurierten Seiten beim Applikationsstart geprüft wird, stellt der Compiler bereits beim Build-Prozess der Applikation sicher, dass alle betroffenen Seitennavigationen aktualisiert wurden.

6.5 Ebenen einsparen

Im letzten Schritt vereinfachen wir auch die Struktur, indem wir das unabhängige ideafork-core -Modul auflösen und dadurch überflüssige Layer entfernen. Ursprünglich haben wir das ideafork-core -Modul von der restlichen Java EE Applikation abgegrenzt, um zu verdeutlichen dass CDI auch unabhängig von Java EE verwendet werden kann. Um weiterhin die namentliche Trennung der beiden Teile zu behalten, verschieben wir das Package at.irian.cdiatwork.ideafork.core in das existierende ideafork-ee6 -Modul. Diese Änderung spiegelt sich auch im Maven-Build wider. In der Konfiguration für ideafork-ee6 entfernen wird die Verweise auf ideafork-core und fügen stattdessen alle Dependencies hinzu, welche bisher in ideafork-core definiert waren. Durch diese Vereinfachung können wir außerdem einige Buildkonfigurationen für die unterschiedlichen EE-Server stark vereinfachen.
Tipp: Wegen der starken Umstrukturierung sind sämtliche Änderungen in einem eigenen Git-Repository von IdeaFork namens IdeaForkLite zusammengefasst. In diesem Repository gibt es für jede Umstellung in diesem Kapitel einen eigenen Commit. Folglich lassen sich sämtliche Änderungen einfach nachvollziehen.
Die Umstrukturierung ermöglicht zusätzlich die Einsparung der Manager-Ebene, da wir diese Kapselung in IdeaFork -Core nicht mehr benötigen. Stattdessen verschieben wir die implementierte Logik in die entsprechenden Service-Klassen, welche schließlich direkt auf die neuen und stark vereinfachten Repository-Beans zugreifen.

 

Mit den gezeigten Vereinfachungen sind wir für die nachfolgenden Kapitel gut gerüstet, da wir bspw. im nächsten Kapitel durch die Integration von anderen Frameworks die Komplexität wieder etwas erhöhen. In realen Applikationen ist es keine Seltenheit, dass mehrere Frameworks kombiniert werden müssen. Je geringer wir die Komplexität der Grundstruktur halten, desto wartbarer wird die gesamte Applikation. Sowohl CDI als auch DeltaSpike ermöglichen nicht nur innovative und flexible Ansätze, sondern auch die Reduktion von Komplexität in Bereichen in denen diese nicht erforderlich ist.