Die Backend-Schnittstelle für die Finanzverwaltung (Teil 2)

Im ersten Teil habe ich das initiale Modell und die ersten Anforderungen der Anwendung beschrieben. Nun geht es um die Backend-Schnittstelle, die die beschriebenen Anwendungsfälle ermöglicht.

Das Projekt für die Anwendung

Als Basis für – insbesondere das Backend – dient Spring Boot. Mit Hilfe einer unterstützenden Entwicklungsumgebung oder auf der Startseite ist das Projekt schnell konfiguriert.

Ich ergänze das Projekt mit den Startern bzw. Abhängigkeiten

  • Spring Web,
  • Lombok,
  • Spring Boot DevTools,
  • Spring Data JPA und
  • H2

Für die (REST-)Schnittstelle benötige ich den Web-Starter. Mit Lombok spare ich mir den Standardcode (Getter, Setter, …). Die DevTools unterstützen die Entwicklung. Beispielsweise startet der Server nach Änderungen automatisch neu, damit die Änderungen wirksam werden.

Die übrigen Starter sind Grundlage für eine erste konkrete Implementierung der beschriebenen Anwendungsfälle.

Entwicklungshilfe

Für die Entwicklung und den Test füge ich Swagger hinzu. Die pom.xml erhält die Abhängigkeiten

<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger2</artifactId>
  <version>${springfox.swagger2.version}</version>
</dependency>

<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger-ui</artifactId>
  <version>${springfox.swagger2.version}</version>
</dependency>		

In den Property setze ich die aktuelle Version (aktuell 2.9.2) ein.

Es fehlt noch die Konfiguration, dann ist die Test-Oberfläche unter http://localhost:8080/swagger-ui.html verfügbar. 

@Configuration
@EnableSwagger2
public class SwaggerConfig {

	@Bean
    public Docket api() { 
        return new Docket(DocumentationType.SWAGGER_2)  
          .select()                                  
          .apis(RequestHandlerSelectors.any())              
          .paths(PathSelectors.any())                          
          .build();                                           
    }
}

Die Backend-Schnittstelle

Domain Modell

Zunächst lege ich das Domain-Modell in einem eigenen Package an. Die Umsetzung entspricht dem Diagramm aus dem vorhergehenden Beitrag.

@Data
public class Buchung {

	private String verwendung;
	private String empfaenger;
	private List<Umsatz> umsaetze = new ArrayList<>();
	
	public void addUmsatz(Umsatz umsatz) {
		getUmsaetze().add(umsatz);
	}
}
@Data
public class Umsatz {

	private LocalDate valuta;
	private BigDecimal betrag;
	private Konto konto;
	private Buchung buchung;
}

Das Konto enthält die Operation verbuche(.), die einen Umsatz zum Konto aufnimmt. Dabei übernimmt sie den Umsatz in die Liste der zum Konto gehörenden Umsätze und addiert gleichzeitig den Betrag aus dem Umsatz zum Saldo. Da Soll- und Haben-Beträge über das Vorzeichen unterschieden sind, ist keine weitere Logik notwendig.

@Data
public class Konto {

	private String name;
	private BigDecimal saldo;
	private List<Umsatz> umsaetze = new ArrayList<>();

	public void verbuche(Umsatz umsatz) {
		umsaetze.add(umsatz);
		saldo = saldo.add(umsatz.getBetrag());
	}
}

Die REST-Schnittstelle der Anwendung

Die Backend-Schnittstelle ist eine REST-Schnittstelle und entsteht in Spring durch eine Controller-Klasse. Dank der Spring Boot ist die Umsetzung einfach. Wer sich dabei unsicher ist, findet in den Spring Boot Guides schnelle Anleitungen.

Die BuchhaltungController-Klasse setzt dabei die im vorausgegangenen Beitrag definierten drei Anwendungsfälle um:

  • Buchen erstellen
  • Konten abfragen
  • Umsatz anzeigen

Die Umsetzung ist noch nicht final, denn beispielsweise ist die Abfrage der Buchungen je Konto ohne Paging nicht sinnvoll. Die Liste wird nach einiger Zeit einfach zu lang, was zu unnötigen Verzögerungen beim Laden, Übertragen und Darstellen in der Oberfläche führt. Auch mag ich das Konto lieber über eine technische Id abfragen, um Problemen mit Leerzeichen oder Sonderzeichen vorzubeugen.

@RestController
@RequestMapping("api/buchhaltung")
public class BuchhaltungController {

	@GetMapping("konto")
	public List<Konto> findAllKonten() {
	    throw new UnsupportedOperationException();
	}
	
	@GetMapping("konto/{kontoName}/umsatz")
	public List<Buchung> findKontoBuchungenByKontoName(
                @PathVariable("kontoName") String kontoName) {
	    throw new UnsupportedOperationException();
	}

	@PostMapping("buche")
	public ResponseEntity<String> buche(
                @RequestBody Buchung buchung) {
	    throw new UnsupportedOperationException();
	}
}

Eigene Klassen für die Datenübertragung in der Backend-Schnittstelle

Für die Datenübertragung in der Schnittstelle möchte ich eigene Transfer-Klassen einsetzten. Die Services sollen mit Domain-Objekten arbeiten. Deren Klassen möchte ich allerdings frei von Schnittstellen spezifischen Abhängigkeiten halten, und die wären für die Umwandlung in JSON erforderlich. Insbesondere sind die Relationen bidirektional implementiert, was ohne Zutun wegen zu endlosen Rekursionen führen würde. Die API-Klassen haben das folgende Aussehen:

@Data
public class Buchung {
	private String verwendung;
	private String empfaenger;
	private List<Umsatz> umsaetze = new ArrayList<>();
}

@Data
public class Umsatz {
	private LocalDate valuta;
	private BigDecimal betrag;
	private Konto konto;
}

@Data
@JsonInclude(Include.NON_NULL)
public class Konto {
	private String name;
	private BigDecimal saldo;
}

Im Konto sorgt die JsonInclude-Annation dafür, dass nur Felder in das JSON geschrieben werden, die auch tatsächlich Werte enthalten.

Die Übertragung erfolgt in Mapper-Klassen.

@Component
public class KontoMapper {
	public List<Konto> domainKontoListToApiKontoList(
                List<buchhaltung.domain.Konto> domainKonten) {
	    return domainKonten.stream()
		.map( domainKonto ->
                        domainKontoToApiKonto(domainKonto) )
				.collect(Collectors.toList());
	}

	public Konto domainKontoToApiKonto(
            buchhaltung.domain.Konto domainKonto) {
		Konto apiKonto = new Konto();
		apiKonto.setName(domainKonto.getName());
		apiKonto.setSaldo(domainKonto.getSaldo());
		return apiKonto;
	}
}

Der Konto-Mapper steht als Beispiel für das Codierungsmuster. Die Formalität sollte helfen, diese Transformation später ggf. durch Hilfsbibliotheken zu automatisieren, beispielsweise durch MapStruct.

Das folgende Bild zeigt in der Übersicht die Struktur der Schnittstelle.

Backend-Schnittstelle
Rest-Schnittstelle mit Mapper

Der Abschluss

Ergänzt um Service-Klassen mit Methoden-Signaturen entsteht als Schnittstelle der folgende Controller:

@RestController
@RequestMapping("api/buchhaltung")
public class BuchhaltungController {
	private KontoService kontoService;
	private BuchungService buchungService;
	private KontoMapper kontoMapper;
	private BuchungMapper buchungMapper;

	public BuchhaltungController(
			KontoService kontoService, 
			BuchungService buchungService, 
			KontoMapper kontoMapper,
			BuchungMapper buchungMapper) {

		this.kontoService = kontoService;
		this.buchungService = buchungService;
		this.kontoMapper = kontoMapper;
		this.buchungMapper = buchungMapper;
	}

	@GetMapping("konto")
	public List<Konto> findAllKonten() {
		return kontoMapper.domainKontoListToApiKontoList(
				kontoService.findAll());
	}
	
	@GetMapping("konto/{kontoName}/umsatz")
	public List<Buchung> findKontoBuchungenByKontoName(
           @PathVariable("kontoName") String kontoName) {
	    return buchungMapper
             .domainBuchungListToApiBuchungListe(
                buchungService
                  .findBuchungByKontoName(kontoName));
	}

	@PostMapping("buche")
	public ResponseEntity<String> buche(
                @RequestBody Buchung buchung) {
	  try {
            buchungService.buche(
              buchungMapper.apiBuchungToDomainBuchung(buchung));
	  } catch (Exception e) {
	    return ResponseEntity
                .unprocessableEntity()
                .body("Fehler: " + e.getMessage());
	  }
	  return ResponseEntity.accepted().body("Verarbeitet");
	}
}

finally

Die Schnittstelle ist damit beschrieben und es steht die eigentliche Implementierung an. Mein erster Wurf wird auf einer relationalen Datenbank zur Speicherung basieren. Für die Anbindung auf der grünen Wiese, verwende ich die Java Persistente API (JPA). Doch dazu beim nächsten mal mehr.

Happy Coding!