Skip to content

14. [TD]: Web-Exposition der Schicht [metier]

Stichworte: mehrschichtige Architektur, Spring, Abhängigkeitsinjektion, Webdienst / jSON, Client/Server.

Kehren wir zur aktuellen Architektur der Anwendung TD zurück:

Wir werden diese Architektur wie folgt weiterentwickeln:

um die Schnittstelle [IMetier] der Geschäftssschicht im Web verfügbar zu machen. Dazu werden wir die in Abschnitt 13.5 beschriebene Methodik befolgen.

14.1. Support

  

Die Projekte dieses Kapitels befinden sich im Ordner [support / chap-14].

14.2. Das Eclipse-Projekt der Schicht [métier]

  

14.2.1. Maven-Konfiguration

Das Projekt der Ebene [métier] ist ein Maven-Projekt, das durch die folgende Datei [pom.xml] konfiguriert wird:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.elections</groupId>
    <artifactId>elections-metier-dao-spring-data</artifactId>
    <version>0.1.0</version>

    <!-- Abhängigkeiten -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>
    <dependencies>
        <!-- Schicht [DAO] -->
        <dependency>
            <groupId>istia.st.elections</groupId>
            <artifactId>elections-dao-spring-data-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot-Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <!-- verwendet UTF-8 für alles -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
</project>
  • Zeilen 18–22: die Abhängigkeit von der in Abschnitt 12 erstellten Schicht [DAO];
  • Zeilen 23–34: die für die Tests erforderlichen Abhängigkeiten;

14.2.2. Spring-Konfiguration

  

Das Projekt der Schicht [métier] ist ein Spring-Projekt, das durch die folgende Datei [MetierConfig] konfiguriert wird:


package elections.metier.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import elections.dao.config.DaoConfig;

@Import({ DaoConfig.class })
@ComponentScan({ "elections.metier.service" })
public class MetierConfig {
}
  • Wir verwenden hier nicht die Notation [@Configuration], die die Klasse zu einer Spring-Konfigurationsklasse macht. Das Vorhandensein der Annotationen [@Import, @ComponentScan] macht sie automatisch zu einer Konfigurationsklasse;
  • Zeile 8: Die Konfigurationsdatei der Schicht [DAO] wird importiert. Damit stehen alle in dieser Datei definierten Beans zur Verfügung;
  • Zeile 9: Weitere Spring-Beans sind im Ordner „[elections.metier.service]“ zu suchen;

14.2.3. Implementierung der Schicht [métier]

  

Die Implementierung der Schicht [métier] entspricht der in Abschnitt 8.5 definierten.

14.2.4. Test der Schicht [métier]

  

Die Testklasse entspricht der in Abschnitt 8.6 beschriebenen.


Aufgabe: Implementieren Sie das Projekt der Schicht [métier] und bestehen Sie dessen Unit-Test. Erstellen Sie das Archiv der Schicht im lokalen Maven-Repository (Run as / Maven / Install).


14.3. Das Eclipse-Projekt der Schicht [web]

Die Webschicht ist eine Spring-Schicht MVC:

Das Eclipse-Projekt hat folgende Struktur:

  • [Boot.java] ist die Klasse, die den Webdienst startet;
  • [WebConfig.java] ist die Konfigurationsklasse des Webdienstes;
  • [Response.java] ist die Antwort, die von den verschiedenen URL des Webdienstes zurückgegeben wird;
  • [ElectionsController] ist die Implementierungsklasse des Webdienstes;

14.4. Maven-Konfiguration

Das Projekt ist ein Maven-Projekt, das durch die folgende Datei „[pom.xml]“ konfiguriert wird:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.elections</groupId>
    <artifactId>elections-webjson-metier-dao-spring-data</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>elections-webjson-metier-dao-spring-data</name>
    <description>couche métier exposée comme un service web / jSON</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Geschäftsschicht -->
        <dependency>
            <groupId>istia.st.elections</groupId>
            <artifactId>elections-metier-dao-spring-data</artifactId>
            <version>0.1.0</version>
        </dependency>
        <!-- Schicht MVC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • Zeilen 19–23: die Abhängigkeit vom Archiv der Schicht [métier]. Dies ist das Archiv, das wir in Abschnitt 14 erstellt haben;
  • Zeilen 25–28: die Abhängigkeit von der Spring-Anwendung MVC;

14.5. Spring-Konfiguration

 

Die Klasse [WebConfig] konfiguriert den Webdienst:


package elections.webjson.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import com.fasterxml.jackson.databind.ObjectMapper;

import elections.metier.config.MetierConfig;

@EnableWebMvc
@Import({ MetierConfig.class })
@ComponentScan({ "elections.webjson.service" })
public class WebConfig {
    // -------------------------------- Konfiguration der [web]-Schicht
    @Autowired
    private ApplicationContext context;

    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
        return servlet;
    }

    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }

    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8080);
    }
    // Mapper jSON
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

}
  • Die Bedeutung dieser Konfiguration wurde in Abschnitt 13.5.3.1 erläutert. Wir erklären hier nur die Neuerungen:
  • Zeile 22: Die Konfigurationsdatei der Schicht [métier] wird importiert, um alle darin enthaltenen Beans nutzen zu können;
  • Zeile 23: Es wird angegeben, dass weitere Beans im Ordner [elections.webjson.server.service] zu finden sind;

14.6. Die Startklasse des Webdienstes

 

Die Klasse [Boot] startet den Webdienst wie folgt:


package elections.webjson.boot;

import org.springframework.boot.SpringApplication;

import elections.webjson.config.WebConfig;

public class Boot {

    public static void main(String[] args) {
        SpringApplication.run(WebConfig.class, args);
    }
}
  • Zeile 10: Die statische Methode [SpringApplication.run] wird die Konfigurationsdatei [WebConfig] verwenden. Aufgrund der Annotation [@EnableAutoConfiguration] startet Spring Boot den Tomcat-Server und stellt den Webdienst darauf bereit;

14.7. Die Antwort des Webdienstes URL

 

Alle URL des Webdienstes / jSON senden denselben Antworttyp:


package elections.webjson.service;

import java.util.List;

public class Response<T> {

    // ----------------- Eigenschaften
    // Status des Vorgangs
    private int status;
    // etwaige Fehlermeldungen
    private List<String> messages;
    // der Antworttext
    private T body;

    // Konstruktoren
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // Getter und Setter
...
}

Diese Klasse wurde in Abschnitt 13.5.5.3 vorgestellt und behandelt.

14.8. Die Implementierung des Webdienstes / jSON

 

Der Webdienst / jSON wird durch die folgende Klasse [ElectionsController] implementiert:


package elections.webjson.service;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import elections.dao.entities.ElectionsConfig;
import elections.dao.entities.ElectionsException;
import elections.metier.service.IElectionsMetier;

@Controller
public class ElectionsController {

    // Spring-Abhängigkeiten
    @Autowired
    private ObjectMapper jsonMapper;

    @Autowired
    private IElectionsMetier metier;

    @RequestMapping(value = "/getElectionsConfig", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getElectionsConfig() throws JsonProcessingException {
        // Antwort
        Response<ElectionsConfig> response;
        try {
            response = new Response<>(0, null,
                    new ElectionsConfig(metier.getNbSiegesAPourvoir(), metier.getSeuilElectoral()));
        } catch (ElectionsException e1) {
            response = new Response<>(e1.getCode(), e1.getErreurs(), null);
        } catch (RuntimeException e2) {
            response = new Response<>(1000, getErreursForException(e2), null);
        }
        // Antwort
        return jsonMapper.writeValueAsString(response);
    }

    @RequestMapping(value = "/getListesElectorales", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getListesElectorales() throws JsonProcessingException {
        throw new UnsupportedOperationException("Not supported yet");
    }

    @RequestMapping(value = "/setListesElectorales", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String setListesElectorales(HttpServletRequest request) throws JsonProcessingException {
        throw new UnsupportedOperationException("Not supported yet");
    }

    @RequestMapping(value = "/calculerSieges", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String calculerSieges(HttpServletRequest request) throws JsonProcessingException {
        throw new UnsupportedOperationException("Not supported yet");
    }

    // private Methoden -----------------------------
    // Liste der Fehlermeldungen einer RuntimeException
    private List<String> getErreursForException(Exception e) {
        // Die Liste der Fehlermeldungen der Ausnahme wird abgerufen
        Throwable cause = e;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // Die Meldung wird nur abgerufen, wenn sie != null und nicht leer ist
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // nächste Ursache
            cause = cause.getCause();
        }
        return erreurs;
    }

}

Aufgabe: Ergänzen Sie den Code der Klasse [ElectionsController] entsprechend den Ausführungen in Abschnitt 13.5.5.


Hinweise:

  • Hier gibt es keine Filter jSON, da die Tabellen [CONF] und [LISTES] nicht durch eine Fremdschlüsselbeziehung miteinander verknüpft sind, was den Code des Webdienstes erheblich vereinfacht;
  • die verschiedenen erforderlichen Spring-Annotationen nicht vergessen;
  • Die URL erhalten die Namen der zugehörigen Methoden;
  • Die Methode [setListeElectorales] wird mit einer Operation [POST] aufgerufen. Der übermittelte Wert ist das Array der konkurrierenden Listen (vom Typ ListeElectorale[]) mit ihren Attributen [sieges, voix, elimine], die in der Datenbank gespeichert werden müssen. Diese Methode gibt bei erfolgreicher Ausführung einen Typ [Response<Void>] mit einem Feld [status=0] zurück, andernfalls einen anderen Wert;
  • Die Methode [calculerSieges] wird mit einer Operation [POST] aufgerufen. Der übergebene Wert ist das Array der Wahllisten (vom Typ ListeElectorale[]) mit ihren Attributen [nom, voix]. Diese Methode gibt einen Typ [Response<ListeElectorale[]>] zurück, dessen Inhalt die Wählerlisten mit ihren initialisierten Feldern [sieges, elimine] sind;

14.9. Tests

Nachdem Sie den Webdienst gestartet haben, führen Sie die folgenden Tests durch, um sicherzustellen, dass der Webdienst mit dem Dienstprogramm [Advanced Rest Client] ordnungsgemäß funktioniert:

 

Die Antwort jSON auf die vorherige Anfrage lautet wie folgt: [1]:

1
2

Kopieren Sie in [2] die Antwort in die Zwischenablage und fügen Sie diese anschließend in einen beliebigen Texteditor ein: [3]:

Isolieren Sie den Wert des Feldes [body] und ändern Sie beispielsweise die Stimmen der Listen. Im folgenden Beispiel [4] werden die Stimmen aller Listen auf 100 gesetzt:

Stellen Sie sicher, dass Ihre Zeichenfolge jSON mit [ et se termine par ] beginnt. Diese Zeichen dienen dazu, ein Array jSON abzugrenzen. Fügen Sie in [5] die oben genannte Zeichenfolge jSON ein. Dies ist der Wert, der für das nächste URL übermittelt wird. Dazu muss die Methode HTTP [POST] [7] ausgewählt werden.

  • in [6], fordern Sie die URL [setListesElectorales] an. Diese URL wird mit einer POST angefordert. Der eingestellte Wert ist die Tabelle jSON der Wettkampflisten, deren Ergebnisse in der Datenbank gespeichert werden müssen;

Man erhält folgendes Ergebnis:

 

Das Feld [status=0] zeigt an, dass kein Fehler aufgetreten ist. Um dies zu überprüfen, fordern Sie die Wettbewerbslisten erneut an und vergewissern Sie sich, dass die Änderungen, die Sie an den Listen vorgenommen haben, berücksichtigt wurden:

Wir führen erneut einen [POST] durch, um die von den Listen erzielten Sitze zu berechnen:

  • in [1]: das URL aus der Sitzberechnung;
  • in [2]: Wir erstellen ein [POST];
  • in [3]: die konkurrierenden Listen. Dem Feld [voix] werden die Werte aus TD zugewiesen, alle [sieges]-Felder werden auf 0 gesetzt, alle [elimine]-Felder werden auf „false“ gesetzt;

Das Ergebnis sieht wie folgt aus: