Teil 18: Backend meets Frontend: Trainer for kids 8

Nach der Dependency Injection kommt das Aufräumen

Sven Ruppert

© Shutterstock / HelenField

Dependency Injection kann dabei helfen, Anwendungen übersichtlicher zu gestalten und Komponenten klar zu trennen. Im ersten Anlauf ist das aber nicht unbedingt der Fall. Deswegen gehört zu jedem Bootstrappen mit Dependency Injection auch das Aufräumen.

Wir haben uns im letzten Teil angesehen, wie man grundsätzlich eine Vaadin-App mit Dependency Injection bootstrappen kann. Das haben wir exemplarisch mit zwei DI-Frameworks realisiert. Außerdem haben wir gezeigt, was typische Migrationspunkte bei einem Wechsel von einem DI-Framework auf ein anderes sind. Ebenfalls kann man diese Punkte als Grundlage verwenden, wenn man Komponenten anbieten möchte, die mit verschiedenen DI-Frameworks zusammenarbeiten. Das Ziel in diesem Teil der Kolumne ist es, die Anwendung aufzuräumen und die jeweiligen technologischen Komponenten in die unterschiedlichen Module zu zerlegen. Hierbei werden wir ein paar interne Dinge über Vaadin lernen.

Zusätzlich zu den Quelltextbeispielen zu diesem Artikel verwende ich auch die Sourcen des Open-Source -Projekts Functional-Reactive. Die Sourcen befinden sich auf GitHub. Ebenfalls werde ich damit beginnen, funktionale Aspekte in die Entwicklung einfließen zu lassen. Hierzu stütze ich mich auf die Serie hier auf JAXenter unter dem Namen Checkpoint Java.

Beginnen wir mit der Projektstruktur und entfernen erst einmal die Module, die wir für die beiden DI-Frameworks erzeugt haben. Die löschen wir einfach. Um die für den Start der Vaadin-Applikation benötigten Teile von den fachlichen Anwendungen zu isolieren, erzeugen wir ein Modul mit dem Namen vaadin. Hier werden wir das Servlet und das Boostrapping des DI-Containers generisch ablegen. Bisher hatten wir eine recht starke Kopplung zwischen dem Servlet, dem DI-Container-Bootstrap und unserer primären UI-Komponente. Diese Gruppe werden wir nun auftrennen.

Beginnen wir mit dem Servlet. Hier hatten wir vom originalen VaadinServlet abgeleitet. Das bleibt auch weiterhin so und ist immer noch in der Klasse DDIVaadinServlet enthalten. Allerdings hatten wir im letzten Teil den DI-Container mit einer selektiven Menge an Packages versorgt. Das fand im Bootstrap des Undertow statt. Da dies jedoch von der jeweiligen Anwendung abhängig ist, ziehen wir es tiefer in unseren Startprozess und damit in unser Servlet. Weil aber die genaue Liste der zu aktivierenden Packages noch nicht bekannt ist, definieren wir eine abstrakte Methode, die uns eine Liste von Packages zurückliefert.

public abstract class DDIVaadinServlet extends VaadinServlet {

  @Override
  protected void servletInitialized() throws ServletException {
    super.servletInitialized();
  }

  @Override
  protected VaadinServletService createServletService(
                    final DeploymentConfiguration deploymentConfiguration) 
                    throws ServiceException {
    final DDIVaadinServletService service 
        = new DDIVaadinServletService(this, deploymentConfiguration, topLevelPackagesToActivate());
    service.init();
    return service;
  }

  // return a list of pkg names that are available for Injection
  // public abstract List<String> topLevelPackagesToActivate();

}

Betrachten wir nun den DDIVaadinServletService. In unserer letzten Version des DDIVaadinServletService haben wir die Aktivierung des DI-Frameworks auf das Servlet selbst angewendet. Das bleibt auch hier wieder so, jedoch werden wir vorher noch alle definierten Packages dem DI-Container bekannt geben (siehe DDIVaadinServlet, Methode topLevelPackagesToActivate) . Das wird direkt im Konstruktor selbst erledigt. Für jedes genannte Package rufen wir die Methode DI.activatePackages(pkg) auf. Dieser Aufruf ist natürlich spezifisch für den jeweiligen DI-Container.

  
public class DDIVaadinServletService extends VaadinServletService {


  public DDIVaadinServletService(VaadinServlet servlet,
                                 DeploymentConfiguration deploymentConfiguration,
                                 List<String> topLevelPackagesToActivated)
      throws ServiceException {

    super(servlet, deploymentConfiguration);

    topLevelPackagesToActivated
        .stream()
        .filter(pkg -> ! DI.isPkgPrefixActivated(pkg))
        .forEach(DI::activatePackages);

    addSessionInitListener(event -> event.getSession().addUIProvider(new DefaultUIProvider() {
      @Override
      public UI createInstance(final UICreateEvent event) {
        final UI instance = super.createInstance(event);
        return DI.activateDI(instance);
      }
    }));

    addSessionDestroyListener(event -> {});
  }

  @PostConstruct
  public void initialize() {
  }
}

Nun können wir in einem Projekt ein Servlet definieren, ohne dass wir uns um den Boostrapping-Prozess Gedanken machen müssen. Es reicht demnach aus, die Liste der zu aktivierenden Packages anzugeben. Innerhalb eines größeren Projektes kann natürlich auch das für die jeweiligen Unterprojekte schon abgebildet sein. In unserem Projekt ist es das Hauptpackage org.rapidpm, das nun alles inkludiert.

@WebServlet(urlPatterns = "/*", name = "JumpstartServlet", asyncSupported = true, displayName = "JumpstartServlet")
@VaadinServletConfiguration(ui = JumpstartUI.class, productionMode = false)
public class JumpstartServlet extends DDIVaadinServlet {

  @Override
  public List<String> topLevelPackagesToActivate() {
    return singletonList("org.rapidpm");
  }
}

Jedoch haben wir hier wieder die harte Kopplung zu einer UI-Klasse. Das wollten wir doch eigentlich loswerden. In diesem Fall ist es die Klasse ui = JumpstartUI.class. Leider können wir nicht einfach ein Marker-Interface erzeugen und dieses definieren. Die Implementierung in Vaadin selbst zwingt uns dazu, etwas anzugeben, das direkt in der Vererbung der abstrakten Klasse UI steht. Hier die Quelltextstelle aus Vaadin 8: VaadinServletConfiguration.

    @InitParameterName(VaadinSession.UI_PARAMETER)
    public Class<? extends UI> ui();

Hier hilft uns das DI-Framework. Entweder man gibt direkt die Klasse UI an oder es wird eine abstrakte Klasse JumpstartUI definiert, die von UI erbt, wenn man noch eigene Dinge dort verankern möchte. In jedem Projekt muss demnach nur eine Implementierung von der verwendeten abstrakten Klasse vorhanden sein. Damit ist die harte Kopplung indirekt aufgehoben.

Nun stellt sich die Frage, warum das so sein sollte. Eine Kleinigkeit vorweg an dieser Stelle: Wenn man in die aktuelle Implementierung der Klasse UIProvider reinschaut, befindet sich dort die Methode, die eine Instanz der angegebenen UI-Klasse erzeugt.

    public UI createInstance(UICreateEvent event) {
        try {
            return event.getUIClass().newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException("Could not instantiate UI class", e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Could not access UI class", e);
        }
    }

Der für uns interessante Teil ist return event.getUIClass().newInstance();. Es wird also lediglich mittels Default-Konstruktor die Instanz erzeugt und nichts anderes. Wichtig ist hier der Umstand, dass an dieser Stelle keine Annotationen von der übergebenen Klasse ausgewertet werden. Dank der einfachen Implementierung können wir diesen Schritt innerhalb der Vererbung sofort umgehen und durch einen Aufruf an den DI-Container ersetzen. Somit verändern wir die Implementierung vom letzten Teil, indem wir den Aufruf an final UI instance = super.createInstance(event); einfach entfernen und den Vorgang komplett an den DI-Container delegieren. In unserem Fall ist es der Aufruf return DI.activateDI(event.getUIClass());. Und hier nochmal das komplette Quelltextstück.

    addSessionInitListener(event -> event.getSession().addUIProvider(new DefaultUIProvider() {
      @Override
      public UI createInstance(final UICreateEvent event) {
        return DI.activateDI(event.getUIClass());
      }
    }));

Der DI-Container kann dies nun auflösen und liefert die Implementierung der angeforderten abstrakten Klasse. Wie der jeweilige DI-Container das realisiert, ist natürlich unterschiedlich. Komfortabel ist es selbstverständlich, wenn der DI-Container einfach die einzige zu findende Implementierung selbständig liefert. In diesem Projekt ist es die Implementierung mit dem Namen MyUI. Wir werden uns später ansehen, was alles in die Klasse hineingehört und was eventuell nicht mehr.

Damit haben wir die generischen Vaadin-spezifischen Dinge extrahiert. Kommen wir nun zum Servlet-Container.

Extraktion von Undertow und DI-Container

Ebenfalls werden wir nun die im Projekt noch enthaltene Implementierung zum Starten des Undertow entfernen. Das Herauslösen ist meist eine lohnenswerte Aufgabe, wenn man mit mehr als nur einer Implementierung zu rechnen hat. Das kann auch bedeuten, das bestimmte Teile in externe Services ausgelagert werden, die dann mittels REST eingebunden werden. Wer möchte, kann hier von Microservices sprechen. Wir werden das zu einem späteren Zeitpunkt ebenfalls durchgehen.

Zum jetzigen Zeitpunkt werden wir lediglich unsere Vaadin-App betrachten. Das herausgelöste Projekt ist auch auf GitHub zu finden. Hierbei handelt es sich ebenfalls um eine Undertow/DDI-Kombination, die eine Erweiterung von der hier besprochenen ist und in Industrieprojekten zum Einsatz kommt. Um nun unsere Implementierungen von den anderen Quelltexten zu separieren, werden wir ein Modul mit den Namen microkernel anlegen.

Die Klasse Main bleibt weiterhin unser Einstiegspunkt. Hier wird initial der DI-Container initialisiert und in der gewünschten Form allgemein der Anwendung zur Verfügung gestellt. Danach erst wird der Initialisierungsprozess für den Servlet-Container erstellt.

  public static void deploy() {
    final String packages = Main.class.getPackage().getName().replace("." , "/") + "/" + DI_PACKAGE_FILE;
    String property = System.getProperty(DI.ORG_RAPIDPM_DDI_PACKAGESFILE , packages);
    System.setProperty(DI.ORG_RAPIDPM_DDI_PACKAGESFILE , property);

    final LocalDateTime dependencyBootstrapStart = LocalDateTime.now();
    DI.bootstrap();
    final LocalDateTime dependencyBootstrapStop = LocalDateTime.now();

    final LocalDateTime deployStart = LocalDateTime.now();
    MainUndertow.deploy();
    final LocalDateTime deployStop = LocalDateTime.now();

    printFooter(dependencyBootstrapStart , dependencyBootstrapStop , deployStart , deployStop);
  }

Hier werden noch einige Serviceinformationen gesammelt und beim Start angezeigt. Zum Beispiel wie lange der Start des DI-Containers benötigt und welche Zeit der Undertow beim Start braucht. In dieser Klasse Main kann man all die Einstiegsdinge realisieren, die unabhängig von den eingesetzten Technologien sind. Der Servlet-Container, in unserem Fall der Undertow, wird dann in der separaten Klasse behandelt. Um nun alle Servlets zu erhalten, die in dem Klassenpfad zur Verfügung gestellt worden sind, kann man den DI-Container befragen. In unserem Fall, indem einfach davon ausgegangen wird, dass die Klasse über eine Annotation von dem Typ @WebServlet verfügt.

final Set<Class<?>> typesAnnotatedWith = DI.getTypesAnnotatedWith(WebServlet.class , true);

Über diese Collection kann nun an die übrigen Informationen zugegriffen werden, wie die Definition der WebInitParams, URL-Pattern und einiges andere.

    final List<ServletInfo> servletInfos = typesAnnotatedWith
        .stream()
        .filter(s -> new ReflectionUtils().checkInterface(s , HttpServlet.class))
        .map(c -> {
          Class<HttpServlet> servletClass = (Class<HttpServlet>) c;
          final ServletInfo servletInfo = servlet(c.getSimpleName() , servletClass , new ServletInstanceFactory<>(servletClass));
          if (c.isAnnotationPresent(WebInitParam.class)) {
            final WebInitParam[] annotationsByType = c.getAnnotationsByType(WebInitParam.class);
            for (WebInitParam webInitParam : annotationsByType) {
              final String value = webInitParam.value();
              final String name = webInitParam.name();
              servletInfo.addInitParam(name , value);
            }
          }
          final WebServlet annotation = c.getAnnotation(WebServlet.class);
          final String[] urlPatterns = annotation.urlPatterns();
          for (String urlPattern : urlPatterns) {
            servletInfo.addMapping(urlPattern);
          }
          servletInfo.setAsyncSupported(annotation.asyncSupported());
          servletInfo.setLoadOnStartup(annotation.loadOnStartup());
          return servletInfo;
        })
        .filter(servletInfo -> ! servletInfo.getMappings().isEmpty())
        .collect(Collectors.toList());

Auch die Weblistener werden so eingebunden.

    final List<ListenerInfo> listenerInfos = DI.getTypesAnnotatedWith(WebListener.class)
                                         .stream()
                                         .map(c -> new ListenerInfo((Class<? extends EventListener>) c))
                                         .collect(Collectors.toList());

Alles zusammen sind ein paar hundert Zeilen Quelltext und stellen die Basis für unseren Microkernel. Nun können wir einfach die main-Methode der Klasse org.rapidpm.microservice.Main starten.

================= Deployment Summary ================= 
Sum Servlets                   = 1
Sum RestEndpoints              = 0
Sum RestEndpoints (Singletons) = 0
================= Deployment Summary ================= 

List Servlet - URLs 
http://0.0.0.0:7080/microservice/*

List RestEndpoint - URLs

List RestEndpoints (Singletons) URLs


 ############  Startup finished  = 2017-09-13T05:36:58.384 ############  
 ############  DDI      =  478 [ms]                        ############
 ############  Undertow =  935 [ms]                        ############
 ############  Complete = 1413 [ms]                        ############
 ###############################  Enjoy ###############################

Ich habe mich hier absichtlich gegen Spring und Java EE entschieden, damit es so neutral und einfach wie möglich bleibt. Der Aufwand so einen angepassten Container zu erstellen ist sehr gering, angenommen man integriert nun nicht selbst den vollen Enterprise Stack. Wenn eine kleine Handvoll Technologien ausreichen, ist man mit dieser Lösung flexibel und unabhängig. Wir werden noch sehen, wie wir diesen Basis-Stack unterschiedlich einsetzen, um auch die Entwicklungsumgebung beim Testen angenehmer zu gestallten.

Die Anwendung

Kommen wir nun wieder zu der Anwendung selbst. Diese besteht nun nur noch aus den fachlichen Teilen der Vaadin-Web-App selbst. Dort ist kein Quelltext mehr, der sich mit der Infrastruktur auseinandersetzt. Der Startpunkt ist indirekt durch das UI vorgegeben. In unserem Fall ist es die Klasse LoginComponent, die als erste Komponente zum Einsatz kommt. Diese wird durch die Implementierung MyUIComponentFactory geliefert. Diese Indirektion ist sicherlich nicht immer notwendig, ist aber an der Stelle gut um technologisch sauber zu trennen. Meine Empfehlung an der Stelle ist, dass in der Instanz vom Typ UI keine Komponenten selbst zusammengestellt werden, sondern zum Beispiel lediglich der Ablauf zur Endscheidung, ob der Request wieder in einen Log-in-Vorgang führt oder nicht. Die bisherige Vaadin-Web-App wird nun ebenfalls in verschiedene Module zerlegt.

Im Modul api werden die Deklarationen für die jeweiligen in der Anwendung benötigten Services ausgelagert. Bisher hatten wir das lediglich im Package api im selben Modul wie die Anwendung so gemacht. Durch diese Trennung stellen wir sicher, dass wir keine zyklischen Abhängigkeiten produzieren. Das Modul api wird dann von den implementierenden Modulen als Abhängigkeit verwendet. Derzeitig befinden sich hier die beiden Services LoginService und UserService inklusive der dazu benötigten POJOs.

Neu hinzugekommen ist auch das Modul backend. Wie der Name schon vermuten lässt, werden hier die Implementierungen der Services realisiert. Bisher handelt es sich um dieselben In-Memory-Implementierungen, die wir bisher auch verwendet haben.

Und zu guter Letzt haben wir noch das Modul ui. Nachdem wir nun alle anderen Teile aus dem Modul entfernt haben, befinden sich hier ausschließlich die Vaadin-Elemente, mit der die Anwendung zusammengesetzt worden ist. Nun sehen wir uns die Auswirkungen der bisherigen Aktionen auf die Klasse LoginComponent an.

Die LoginComponent hatte bisher die Beschriftungen der grafischen Komponenten als Konstante direkt gespeichert. Zum Beispiel befindet sich die Beschriftung des Buttons OK in der Konstanten BUTTON_CAPTION_OK. Nun wird die Beschriftung in der Methode postProcess direkt als Caption gesetzt: ok.setCaption(BUTTON_CAPTION_OK);. Gut ist schon mal, dass der Wert nicht direkt gesetzt wurde, sondern eine Konstante verwendet worden ist. Sobald wir aber mit Themen wie Internationalisierung konfrontiert werden, ist das alles nicht mehr tragfähig. Hier hilft ein PropertyService. Gesagt getan: Definieren wir in unserer Anwendung einen solchen Service. Der erste Schritt besteht darin, diesen Service in dem Modul api zu definieren.

public interface PropertyService {
  String resolve(String key);
  boolean hasKey(String keyy);
}

Danach implementieren wir dieses Interface im Modul backend.

public class PropertyServiceInMemory implements PropertyService {
  private final Map<String, String> storage = new HashMap<>();

  @Override
  public String resolve(final String key) {
    return storage.get(key);
  }

  @Override
  public boolean hasKey(final String key) {
    return storage.containsKey(key);
  }

  @PostConstruct
  public void init() {
    //add data here
  }
}

Da wir nun über diesen Service verfügen, können wir uns eine Referenz in unsere Komponente LoginComponent definieren, zuzüglich einer kleinen Hilfsmethode.

  @Inject private PropertyService propertyService;
  
  private String property(String key) {
    return propertyService.resolve(key);
  }

Da alle benötigten Komponenten verfügbar sind, kann nun begonnen werden die Komponente selbst umzubauen. Die Beschriftungen in den Konstanten werden als erstes durch Schlüssel ersetzt. Zum Beispiel erhalten wir damit public static final String BUTTON_CAPTION_OK = "generic.ok";. Der Schlüssel generic.ok wird nun mit dem Wert Ok in der Implementierung des PropertyService hinterlegt.

public class PropertyServiceInMemory implements PropertyService {
  private final Map<String, String> storage = new HashMap<>();

  @Override
  public String resolve(final String key) {
    return storage.get(key);
  }

  @Override
  public boolean hasKey(final String key) {
    return storage.containsKey(key);
  }

  @PostConstruct
  public void init() {
    storage.put("generic.ok", "Ok");
    storage.put("generic.cancel", "Cancel");

    storage.put("login.name", "Login"); // i18n
    storage.put("login.info", "Please enter your username and password"); // i18n
    storage.put("login.username", "username"); // i18n
    storage.put("login.password", "password"); // i18n
    storage.put("login.failed", "Login failed..."); // i18n
    storage.put("login.failed.description", "Login failed, please use right User / Password combination"); // i18n
  }
}

Um nun noch den Wert zu setzen, wird der Aufruf, mit dem wir die Beschriftung in der Komponente selbst setzen, ersetzt durch den Aufruf des PropertyServices. Fertig sind wir.

    ok.setCaption(property(BUTTON_CAPTION_OK));

Backend meets Frontend

In der Artikelserie Backend meets Frontend stellt Sven Ruppert (Vaadin) Konzepte und Technologien rund um das UI-Framework Vaadin vor. Sein Fokus liegt dabei auf modernem Web-Design für Java-Backend-Entwickler.

Zum ersten Teil und damit dem Start der Tutorien rund um die UI-Entwicklung mit Java geht es hier entlang. Alle Teile der Serie Backend meets Frontend finden sich hier.

Fazit

Wir haben viel erreicht. Die Anwendung ist in die jeweiligen technologischen Komponenten zerlegt, die auf Wunsch auch in externe Projekte ausgelagert werden können. Der Quelltext, den ein Entwickler der Anwendung noch permanent überblicken muss, wurde damit stark reduziert. In diesem Beispiel werden die Services noch nicht als externe Services via REST konsumiert. Das lässt sich aber nun einfach realisieren. Die Komponenten selbst wurde um die Verwendung eines PropertyService erweitert. Hiermit ist der erste Schritt hin zu einer multilingualen Anwendung begonnen. Im nächsten Teil werden wir die MathComponent um einige grafische Elemente erweitern und uns ansehen, was Charts mit Bruchrechnen zu tun haben können. Den Quelltext findet ihr auf GitHub. Bei Fragen und Anregungen einfach melden unter sven@vaadin.com oder per Twitter @SvenRuppert.

Happy Coding!

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: