Skip to content

14. [TD]: Web-Exposure der [Business]-Schicht

Stichworte: mehrschichtige Architektur, Spring, Dependency Injection, Webservice / JSON, Client / Server.

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

Wir werden diese Architektur wie folgt weiterentwickeln:

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

14.1. Support

  

Die Projekte zu diesem Kapitel befinden sich im Ordner [support / chap-14].

14.2. Das Eclipse-Projekt für die [business]-Schicht

  

14.2.1. Maven-Konfiguration

Das Projekt der [Business]-Schicht ist ein Maven-Projekt, das durch die folgende [pom.xml]-Datei 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>
 
    <!-- dependencies -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>
    <dependencies>
        <!-- layer [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>
        <!-- use UTF-8 for everything -->
        <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 Absatz 12 erstellten [DAO]-Schicht;
  • Zeilen 23–34: die für das Testen erforderlichen Abhängigkeiten;

14.2.2. Spring-Konfiguration

  

Das [Business]-Layer-Projekt ist ein Spring-Projekt, das durch die folgende [MetierConfig]-Datei 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 Annotation [@Configuration], die die Klasse zu einer Spring-Konfigurationsklasse machen würde. Das Vorhandensein der Annotationen [@Import] und [@ComponentScan] macht sie automatisch zu einer Konfigurationsklasse;
  • Zeile 8: Wir importieren die Konfigurationsdatei aus der [DAO]-Schicht. Dadurch haben wir Zugriff auf alle in dieser Datei definierten Beans;
  • Zeile 9: Weitere Spring-Beans befinden sich im Ordner [elections.metier.service];

14.2.3. Implementierung der [business]-Schicht

  

Die Implementierung der [Business]-Schicht entspricht der in Abschnitt 8.5 definierten.

14.2.4. Testen der [Business]-Schicht

  

Die Testklasse ist die in Abschnitt 8.6 beschriebene.


Aufgabe: Implementieren Sie das Projekt der [Business]-Schicht und bestehen Sie dessen Unit-Test. Erstellen Sie das Archiv der Schicht im lokalen Maven-Repository (Ausführen als / Maven / Installieren).


14.3. Das Eclipse-Projekt für die [Web]-Schicht

Die Web-Schicht ist eine Spring-MVC-Schicht:

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 URLs des Webdienstes generiert wird;
  • [ElectionsController] ist die Implementierungsklasse des Webdienstes;

14.4. Maven-Konfiguration

Das Projekt ist ein Maven-Projekt, das durch die folgende [pom.xml]-Datei 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>
        <!-- business layer -->
        <dependency>
            <groupId>istia.st.elections</groupId>
            <artifactId>elections-metier-dao-spring-data</artifactId>
            <version>0.1.0</version>
        </dependency>
        <!-- layer 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 [Business]-Schicht. Dies ist dasjenige, das wir in Absatz 14 erstellt haben;
  • Zeilen 25–28: die Abhängigkeit für eine Spring-MVC-Anwendung;

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 {
    // -------------------------------- layer configuration [web]
    @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 werden nur die neuen Funktionen erläutern:
  • Zeile 22: Wir importieren die Konfigurationsdatei aus der [business]-Ebene, um alle darin enthaltenen Beans nutzen zu können;
  • Zeile 23: Wir geben an, 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] verwendet die Konfigurationsdatei [WebConfig]. Aufgrund der Annotation [@EnableAutoConfiguration] startet Spring Boot den Tomcat-Server und stellt den Webdienst darauf bereit;

14.7. Die Antwort von den Webdienst-URLs

 

Alle URLs des Webdienstes / JSON senden denselben Antworttyp:


package elections.webjson.service;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}

Diese Klasse wurde in Abschnitt 13.5.5.3 vorgestellt und erläutert.

14.8. 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 dependencies
    @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 {
        // answer
        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);
        }
        // answer
        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 methods -----------------------------
    // list of RuntimeException error messages
    private List<String> getErreursForException(Exception e) {
        // retrieve the list of exception error messages
        Throwable cause = e;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // the message is retrieved only if it is !=null and not blank
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // next cause
            cause = cause.getCause();
        }
        return erreurs;
    }
 
}

Aufgabe: Vervollständigen Sie den Code für die Klasse [ElectionsController] gemäß den Schritten in Abschnitt 13.5.5.


Hinweise:

  • Hier gibt es keine JSON-Filter, da die Tabellen [CONF] und [LISTES] nicht durch eine Fremdschlüsselbeziehung verknüpft sind, was den Webservice-Code erheblich vereinfacht;
  • Vergessen Sie nicht die verschiedenen erforderlichen Spring-Annotationen;
  • Die URLs werden nach den zugehörigen Methoden benannt;
  • Die Methode [setListeElectorales] wird mit einem [POST]-Aufruf aufgerufen. Der übermittelte Wert ist das Array der konkurrierenden Listen (vom Typ ListeElectorale[]) mit ihren Attributen [Sitze, Stimmen, ausgeschieden], die in der Datenbank gespeichert werden müssen. Diese Methode gibt eine [Response<Void>] mit dem Feld [status=0] zurück, wenn keine Fehler aufgetreten sind, andernfalls etwas anderes;
  • Die Methode [calculateSeats] wird mit einer [POST]-Operation aufgerufen. Der übermittelte Wert ist das Array der konkurrierenden Listen (vom Typ ElectoralList[]) mit ihren Attributen [name, votes]. Diese Methode gibt ein [Response<ElectoralList[]>] zurück, dessen Hauptteil die Wahllisten mit initialisierten Feldern [seats, eliminated] enthält;

14.9. Tests

Führen Sie nach dem Start des Webdienstes die folgenden Tests durch, um sicherzustellen, dass der Webdienst ordnungsgemäß funktioniert, und verwenden Sie dazu das Dienstprogramm [Advanced Rest Client]:

 

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

1
2

Kopieren Sie in [2] die Antwort in die Zwischenablage und fügen Sie sie dann in einen beliebigen Texteditor ein [3]:

Isolieren Sie den Wert des Feldes [body] und ändern Sie beispielsweise die Stimmen für die Listen. Unter [4] setzen wir die Stimmen für alle Listen auf 100:

Stellen Sie sicher, dass Ihre JSON-Zeichenkette mit [ beginnt und mit ] endet. Diese Zeichen dienen zur Abgrenzung eines JSON-Arrays. Fügen Sie in [5] die obige JSON-Zeichenkette ein. Dies ist der Wert, der an die nächste URL gesendet wird. Wählen Sie dazu die HTTP-Methode [POST] [7] aus.

  • In [6] wird die URL [setListesElectorales] aufgerufen. Diese URL wird mittels einer POST-Anfrage aufgerufen. Der übermittelte Wert ist das JSON-Array der konkurrierenden Listen, deren Ergebnisse in der Datenbank gespeichert werden müssen;

Es wird folgendes Ergebnis erhalten:

 

Das Feld [status=0] zeigt an, dass keine Fehler aufgetreten sind. Um dies zu überprüfen, rufen Sie die konkurrierenden Listen erneut ab und vergewissern Sie sich, dass die von Ihnen an den Listen vorgenommenen Änderungen übernommen wurden:

Wir senden eine weitere [POST]-Anfrage, um die von den Listen gewonnenen Sitze zu berechnen:

  • in [1]: die URL zur Berechnung der Sitze;
  • in [2]: Wir senden eine [POST]-Anfrage;
  • in [3]: die konkurrierenden Listen. Wir setzen das Feld [votes] auf die Werte aus dem Tutorial, alle [seats] werden auf 0 gesetzt und alle [eliminate]-Felder werden auf false gesetzt;

Das Ergebnis lautet wie folgt: