Von Legacy Java Code zu einer professionellen Spring Boot 3.3 Anwendung mit 3NF

Ausgangslage

In der Schweiz gibt es eine beachtliche Zahl von Wohnbaugenossenschaften (siehe https://www.wbg-schweiz.ch). Obwohl genaue Zahlen fehlen, ist bekannt, dass viele Wohnbaugenossenschaften in der Schweiz selbstverwaltete Strukturen aufweisen.

Unabhängig von der Form (z.B. Reiheneinfamilienhäuser, Blockwohnungen etc.) ist bei Wohnbaugenossenschaften das Teilen von Ressourcen wie Gemeinschaftsräumen, Waschküchen, Gärten und Werkzeugen sehr verbreitet.

Gerade bei selbstverwalteten Wohnbaugenossenschaften ist es wichtig, dass die Nutzung der Ressourcen effizient und fair verrechnet wird. Nur so können Konflikte vermieden und die langfristige Nutzung der gemeinsamen Infrastruktur gesichert werden. Dabei gibt es grundsätzlich zwei Modelle, um diese Verrechnung zu gestalten:

  • Pauschalmodelle, bei denen die Kosten gleichmässig auf alle Mitglieder umgelegt werden, unabhängig von der tatsächlichen Nutzung.
  • Nutzungsbezogene Abrechnung, bei der die Kosten individuell nach der tatsächlichen Nutzung der Ressourcen berechnet werden.

Während Pauschalmodelle einfach umzusetzen sind, können sie für Bewohnende, welche die Ressourcen selten oder gar nicht nutzen, als ungerecht empfunden werden. Nutzungsbezogene Modelle bieten eine höhere Transparenz und Fairness, erfordern jedoch eine einfache und effiziente IT-Lösung, um die Nutzung der Ressourcen zu erfassen und die Verrechnung ohne grossen Aufwand zu ermöglichen.

Durch ein nutzungsorientiertes Modell können Kosten fair verteilt werden, während gleichzeitig die Bewirtschaftung und Pflege der gemeinsamen Infrastruktur verbessert wird. Dies geschieht, weil die Bewohnenden vermehrt auf eine bedarfsgerechte und verantwortungsbewusste Nutzung achten.

Zweck dieses Blogbeitrages

In diesem Blogbeitrag berichten wir über unsere Erfahrungen aus einem einschlägigen Migrationsprojekt, bei welchem eine bestehende, Java-basierte Legacy-Anwendung, die ein nutzungsbasiertes Modell verwendet, zu einer modernen und zukunftssicheren Lösung mit Spring Boot überführt wurde.

Der Schwerpunkt liegt nicht auf der detaillierten Beschreibung jedes einzelnen Migrationsschrittes, sondern auf einer Skizze der zentralen Herausforderungen und Lösungen, die während des Projekts entstanden sind. Ziel war es, eine Anwendung zu schaffen, die nicht nur die Verrechnung von Nutzungsgebühren für gemeinschaftlich genutzte Ressourcen effizient gestaltet, sondern auch für zukünftige Anforderungen gewappnet ist.

Im Vergleich zur alten Legacy-Lösung bietet die neue Anwendung bedeutende Vorteile:

  • Automatischer Versand von Rechnungen mit QR-Code per E-Mail, was die Effizienz und Benutzerfreundlichkeit erheblich verbessert.
  • Mandantenfähigkeit, damit die Lösung von verschiedenen Wohnbaugenossenschaften genutzt werden kann.
  • Flexibilität und Erweiterbarkeit, beispielsweise durch die geplante Integration einer Schnittstelle zu Buchhaltungssystemen, um Prozesse weiter zu automatisieren.

Dieser Beitrag richtet sich an Entwickler:innen und Architekt:innen, die vor ähnlichen Herausforderungen stehen und von den Erfahrungen aus diesem Projekt profitieren möchten. Die Migrationsstrategie wird dabei grob skizziert, wobei der Fokus auf den entscheidenden Konzepten und technologischen Entscheidungen liegt, die das Projekt erfolgreich gemacht haben.

Migrationsstrategie

Im vorliegenden Projekt wurde die nachstehend kurz zusammengefasste Migrationsstrategie angewendet.

  1. Analyse und Zieldefinition:
    • Identifizierung der erweiterten Anforderungen zur bisherigen Lösung: Mandantenfähigkeit, automatisierter Rechnungsversand mit QR-Code, Erweiterbarkeit und Integration in Buchhaltungssysteme.
    • Überprüfung und Normalisierung des Datenschemas.
  2. Modularisierung der Anwendung:
    • Aufteilung der Legacy-Anwendung in funktionale Module wie Rechnungsstellung und Benutzerverwaltung.
    • Entwicklung eines Pilotmoduls (z. B. Rechnungen), um Architektur und gewählter Technologie-Stack (Spring Boot) zu validieren.
  3. Parallelbetrieb:
    • Übergangsweise Verbindung zwischen alter und neuer Lösung über APIs.
    • Daten schrittweise migriert, um Risiken zu minimieren.
  4. Schrittweise Erweiterung:
    • Einführung neuer Features wie Rechnungsversand per E-Mail und Mandantenfähigkeit in iterativen Schritten.
    • Kontinuierliches Testing und Einholen von Nutzer:innen-Feedback.
  5. Abschluss und Rollout:
    • Vollständige Ablösung der Legacy-Lösung.
    • Einführung von Entwickler:innen und Architekt:innen-Schulungen sowie Überwachung der neuen Anwendung.

Entscheidende Konzepte und technologische Entscheidungen

Im Verlaufe des Projektes wurden nachstehende Entscheidungen gefällt:

  1. Modularisierung:
    • Trennung in eigenständige Spring-Boot-Module für bessere Wartbarkeit.
  2. Mandantenfähigkeit:
    • Datenbank-Strategien für die Unterstützung mehrerer Wohnbaugenossenschaften (z. B. Multi-Tenant-Ansatz).
  3. Automatisierter Rechnungsversand:
    • Verwendung von Spring Mail und einem QR-Code-Rechnungs-Generator (SwissQRBill).
  4. Integration in Buchhaltungssysteme:
    • REST-APIs und Message Queues (z. B. RabbitMQ) für künftige Erweiterungen.
  5. Datenmigration:
    • Einsatz von Flyway zur schrittweisen Anpassung des Datenbankschemas.
  6. Testing:
    • Umfassende Unit- und Integrationstests mit Spring Boot und JUnit.

Pseudo-Code of the Legacy Application

In einer vereinfachten Darstellung präsentiert sich der Pseudocode der Legacy Anwendung wie folgt:

// Main entry point
Main:
    Try:
        // Load CSV file with usage data
        Load CSV file into memory

        // Parse CSV data into a list of records
        For each line in CSV:
            Extract person details, object type, usage, and price per usage
            Add record to in-memory collection

        // Aggregate data by person and object type
        Create a data structure to store aggregated totals
        For each record in the collection:
            If person exists in aggregation:
                If object type exists for person:
                    Update usage and total cost for this object type
                Else:
                    Initialize totals for this object type
            Else:
                Initialize entry for person and their object usage

        // Loop through all aggregated persons
        For each person in aggregated data:
            // Retrieve person details
            Get name, address, and aggregated totals by object type

            // Prepare data for template
            Populate template context with person details and object-specific totals

            // Render HTML template
            Render template using Thymeleaf

            // Convert rendered HTML to PDF
            Generate PDF file for the invoice

            // Create QR bill data
            Generate QR bill details

            // Add QR bill to the PDF
            Attach QR bill to the last page of the PDF

            // Save final PDF with QR bill

    Catch any errors:
        Print error message

            Add a new entry for the object type
        Else:
            Create a new entry for the person and their object usage

Vereinfacht kann man sich die aggregierten Daten wie folgt vorstellen:

 Aggregation = {
    "Person A": {
        "Rasenmäher": {"usage": 3, "totalCost": 15},
        "Trimmer": {"usage": 2, "totalCost": 6}
    },
    "Person B": {
        "Rasenmäher": {"usage": 1, "totalCost": 5}
    }
}

Mängel der Legacy-Anwendung

Die bestehende legacy Anwendung weist einig (offensichtliche) Schwächen auf:

  • Datenmanagement: Flat File (CSV) ohne Normalisierung führt zu redundanten und fehleranfälligen Daten.
  • Wartbarkeit: Eng gekoppelte Logik erschwert Änderungen und Erweiterungen.
  • Fehlende Automatisierung: Kein automatischer E-Mail-Versand, manuelle Arbeit nötig.
  • Erweiterbarkeit: Fest codierte Strukturen und keine Mandantenfähigkeit limitieren die Nutzung.
  • Fehleranfälligkeit: Rudimentäre Fehlerbehandlung mindert Stabilität und Benutzerfreundlichkeit.

Übergang zur neuen Spring Boot-basierten Lösung

Nachdem also die Schwächen der bestehenden Legacy-Anwendung deutlich wurden, stand die Modernisierung und Weiterentwicklung im Vordergrund. Um den Anforderungen eines nutzungsbasierten Modells gerecht zu werden und die Effizienz sowie die Benutzerfreundlichkeit signifikant zu steigern, haben wir uns wie oben erwähn entschieden, die Anwendung auf Spring Boot umzustellen.

In den folgenden Abschnitten werden wir die Architektur der neuen Lösung detailliert vorstellen. Anstatt den gesamten Quellcode zu präsentieren, fokussieren wir uns auf zentrale Architekturdiagramme wie das Klassendiagramm und das Sequenzdiagramm, die die Struktur und den Datenfluss der Anwendung veranschaulichen. Zudem zeigen wir ausgewählte Codeausschnitte, die die wichtigsten Funktionalitäten und technischen Verbesserungen hervorheben.

Diese Herangehensweise ermöglicht es uns, die wesentlichen Konzepte und technologischen Entscheidungen transparent darzustellen, ohne dabei den Leser bzw. die Leserin mit zu vielen technischen Details zu überladen. So erhalten Entwickler:innen und Architekt:innen einen klaren Einblick in die neue Lösung und können von den implementierten Best Practices und Optimierungen profitieren.

Architektur der neuen Spring Boot-Lösung

Um die Migration erfolgreich zu gestalten, haben wir eine modulare Architektur mit Spring Boot implementiert. Diese Struktur ermöglicht eine bessere Wartbarkeit, Skalierbarkeit und Erweiterbarkeit der Anwendung.

Klassendiagramm

Das nachstehende Klassen-Diagramm zeigt der besseren Übersicht halber lediglich die wichtigsten Klassen und deren Beziehungen untereinander.

Vereinfachtes Klassendiagramm

Sequenzdiagramm

Das folgende Sequenzdiagramm veranschaulicht den Ablauf der Erstellung und des Versands von Rechnungen. Dabei wurden ausschliesslich die wichtigsten beteiligten Klassen berücksichtigt.

Vereinfachtes Sequenzdiagramm

Code-Ausschnitte

Um die Implementierung der zentralen Funktionen zu verdeutlichen, präsentieren wir hier einige wichtige Codeausschnitte:

/**
 * Erzeugt Rechnungen für alle Personen im Repository für ein bestimmtes
 * Datum, speichert die PDFs zur Überprüfung durch Rechnungsrevisoren und 
 3 versendet sie per E-Mail.
 *
 * @param invoiceDate Das Datum, das zur Erstellung der Rechnungen 
 *                    verwendet wird.
 */
public void generateInvoices(LocalDate invoiceDate) {
  List<Person> allPeople = personRepository.findAll();
  Path pdfDir = createPdfDirectory();

  allPeople.parallelStream().forEach(person -> {
    try {
          processPersonInvoice(person, invoiceDate, pdfDir);
    } catch (Exception e) {
        logger.error("Error processing invoice for {}: {}", 
        person.getName(), e.getMessage(), e);
    }
  });
}

Obiger Codeausschnitt illustriert die Methode, mit welcher die Rechnungen erzeugt werden. Den Rechnungen werden jeweils Einzahlungsscheine mit QR-Code angefügt, was mit nachstehender Klasse realisiert wird.

@Service
public class QRBillService {

    // Logger for logging information and errors
    private static final Logger logger = 
    LoggerFactory.getLogger(QRBillService.class);

    // Configuration properties containing company details
    private final CompanyProperties companyProperties;

    /**
     * Constructor for QRBillService.
     *
     * @param companyProperties Configuration properties for company 
     * information used in QR Bills.
     */
    public QRBillService(CompanyProperties companyProperties) {
        this.companyProperties = companyProperties;
    }

    /**
     * Generates a QR Bill PDF as a byte array based on the provided 
     * QRBillDTO.
     *
     * @param qrBillDTO Data transfer object containing all necessary 
     *        information to generate the QR Bill.
     * @return A byte array representing the generated QR Bill PDF.
     * @throws QRBillGenerationException If validation fails or QR Bill
     *         generation encounters an error.
     */
    public byte[] generateQRBill(QRBillDTO qrBillDTO) {
        try {
            // Build the QR Bill object from the DTO
            Bill bill = buildQRBill(qrBillDTO);
            // Generate the QR Bill PDF and return it as a byte array
            return QRBill.generate(bill);
        } catch (QRBillValidationError e) {
            // Retrieve validation results from the exception
            ValidationResult result = e.getValidationResult();
            // Log the validation error messages
            logger.error("QR Bill validation error: {}", 
            result.getValidationMessages());
            // Throw a custom exception to indicate QR Bill generation 
            // failure
            throw new QRBillGenerationException("QR Bill validation 
            failed", e);
        }
    }

    /**
     * Constructs a Bill object required by the QR Bill generator from the 
     * provided QRBillDTO.
     *
     * @param dto Data transfer object containing QR Bill details.
     * @return A Bill object populated with the necessary information for 
     *         QR Bill generation.
     */
    private Bill buildQRBill(QRBillDTO dto) {
        // Initialize a new Bill object
        Bill bill = new Bill();
        // Set the creditor's IBAN account
        bill.setAccount(dto.getIban());
        // Set the total amount for the bill
        bill.setAmount(dto.getAmount());
        // Set the currency to Swiss Francs
        bill.setCurrency("CHF");

        // Map and set the creditor's address using the provided 
        // AddressDTO
        Address creditor = mapToQRBillAddress(dto.getCreditorAddress());
        bill.setCreditor(creditor);

        // Map and set the debtor's address using the provided AddressDTO
        Address debtor = mapToQRBillAddress(dto.getDebtorAddress());
        bill.setDebtor(debtor);

        // Set any additional unstructured message for the bill
        bill.setUnstructuredMessage(dto.getAdditionalInfo());

        // Configure the bill format
        BillFormat format = new BillFormat();
        // Set the output size to A4 portrait
        format.setOutputSize(OutputSize.A4_PORTRAIT_SHEET);
        // Set the graphics format to PDF
        format.setGraphicsFormat(GraphicsFormat.PDF);
        // Set the language to English
        format.setLanguage(Language.EN);
        // Uncomment and configure the following lines if specific fonts 
        // are needed
        // format.setFontFamily(FontFamily.HELVETICA);
        // format.setFontEmbeddingEnabled(true);
        bill.setFormat(format);

        return bill;
    }

    /**
     * Maps an AddressDTO to the Address object required by the QR Bill 
     * generator.
     *
     * @param dto Data transfer object containing address details.
     * @return An Address object populated with the necessary address 
     *         information.
     */
    private Address mapToQRBillAddress(AddressDTO dto) {
        // Initialize a new Address object
        Address address = new Address();
        // Set the name of the address holder (creditor or debtor)
        address.setName(dto.getName());
        // Set the street address
        address.setStreet(dto.getStreet());
        // Set the postal code
        address.setPostalCode(dto.getPostalCode());
        // Set the city or town
        address.setTown(dto.getCity());
        // Set the country code (e.g., "CH" for Switzerland)
        address.setCountryCode(dto.getCountryCode());
        return address;
    }
}

Und abschliessend sei noch ein Test angefügt, welcher sicher stellen soll, dass die PdfGenerationService-Klasse zuverlässig funktioniert, indem sie sowohl normale als auch fehlerhafte Szenarien abdeckt. Durch den Einsatz von Mock-Objekten werden externe Abhängigkeiten isoliert, was die Tests schneller und unabhängiger von der tatsächlichen Implementierung dieser Abhängigkeiten macht.

@ExtendWith(MockitoExtension.class) // Integrate Mockito with JUnit 5 for mocking
public class PdfGenerationServiceTest {

    @Mock
    // Mocked dependency for processing Thymeleaf templates
    private SpringTemplateEngine templateEngine; 

    @Mock
    // Mocked mapper for converting InvoiceDTO to domain objects
    private InvoiceMapper invoiceMapper; 

    @Mock
    // Mocked repository for rental prices
    private RentalPriceRepository rentalPriceRepository; 

    @Mock
    // Mocked repository for assets
    private AssetRepository assetRepository; 


    @InjectMocks
    // The service under test, with mocks injected
    private PdfGenerationService pdfGenerationService; 

    /**
     * Tests the successful generation of invoice HTML when provided with 
     * valid InvoiceDTO data.
     *
     * <p>
     * This test verifies that the PdfGenerationService correctly processes 
     * a valid InvoiceDTO and interacts with the SpringTemplateEngine to 
     * produce the expected HTML output.
     * </p>
     */
    @Test
    public void testGenerateInvoiceHtml_Success() {
        // Arrange: Create a sample InvoiceDTO with usage details
        InvoiceDTO invoiceDTO = new InvoiceDTO(
                "Alice Brown",
                "202 Pine St, Hamletville",
                Arrays.asList(new UsageDetailDTO(
                        "Asset C",
                        LocalDate.of(2024, 1, 1),
                        LocalDate.of(2024, 12, 31),
                        2,
                        20.0,
                        40.0
                )),
                40.0
        );
         // Set the formatted total amount
        invoiceDTO.setFormattedTotalAmount("40,00"); 
       

        // Mock the templateEngine to return a predefined HTML string 
        // when processing the "invoice-template"
        when(templateEngine.process(eq("invoice-template"), 
        any(Context.class)))
                .thenReturn("<html>Invoice HTML Content</html>");

        // Act: Call the method under test to generate the invoice HTML
        String invoiceHtml = 
        pdfGenerationService.generateInvoiceHtml(invoiceDTO);

        // Assert: Verify that the returned HTML is not null and matches 
        // the expected content
        assertNotNull(invoiceHtml, "The generated invoice HTML should
        not be null.");
        assertEquals("<html>Invoice HTML Content</html>", invoiceHtml, 
        "The generated HTML content does not match the expected value.");

        // Verify that the templateEngine.process method was called 
        //exactly once with the specified arguments
        verify(templateEngine, times(1)).process(eq("invoice-template"), 
        any(Context.class));
    }

    // other tests: omitted for clarity
}

Fazit

Die Migration der bestehenden, Java-basierten Legacy-Anwendung zu einer modernen Spring Boot-Lösung war ein Erfolg. Durch die Beibehaltung des nutzungsbasierten Modells konnten wir eine gerechte Kostenverteilung sicherstellen und gleichzeitig die Effizienz sowie die Benutzerfreundlichkeit der Anwendung deutlich verbessern.

Erfahrungen und Erkenntnisse:

  • Strategische Planung: Klare Zieldefinition und Anforderungsanalyse waren entscheidend für den Erfolg.
  • Modularisierung: Pilotprojekte ermöglichten frühe Validierung der neuen Architektur.
  • Kontinuierliches Testing: Sicherte die Zuverlässigkeit jeder Komponente.
  • Benutzerfeedback: Unterstützte das Projektteam dabei, die Anwendung besser an die tatsächlichen Bedürfnisse der Kunden anzupassen.

Ausblick:

Die neue Spring Boot-basierte Lösung ist nun besser gerüstet, um zukünftige Anforderungen flexibel zu erfüllen und die Verwaltung gemeinschaftlicher Ressourcen weiterhin effizient und fair zu gestalten.

Schlusswort:

Dieses Projekt zeigt, wie eine gezielte Modernisierung nicht nur technische Verbesserungen bringt, sondern auch die Transparenz und Fairness innerhalb von Wohnbaugenossenschaften nachhaltig stärkt. Entwickler:innen und Architekt:innen, die ähnliche Herausforderungen bewältigen möchten, können von unseren Erfahrungen und den implementierten Best Practices profitieren.


💡
Möchten Sie tiefer in die Entwicklung von Microservices mit Spring Boot eintauchen? Dann laden ich Sie herzlich ein, an meinen Microservice mit Spring Boot Kurs bei letsboot.ch teilzunehmen.
In diesem umfassenden Kurs führe ich Sie Schritt für Schritt durch die wichtigsten Konzepte und Best Practices, um moderne, skalierbare und wartbare Anwendungen zu entwickeln. Nutzen Sie die Gelegenheit, Ihr Wissen zu erweitern und praktische Fähigkeiten zu erwerben, die Sie in Ihren eigenen Projekten anwenden können.