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.
- 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.
- 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.
- Parallelbetrieb:
- Übergangsweise Verbindung zwischen alter und neuer Lösung über APIs.
- Daten schrittweise migriert, um Risiken zu minimieren.
- 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.
- 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:
- Modularisierung:
- Trennung in eigenständige Spring-Boot-Module für bessere Wartbarkeit.
- Mandantenfähigkeit:
- Datenbank-Strategien für die Unterstützung mehrerer Wohnbaugenossenschaften (z. B. Multi-Tenant-Ansatz).
- Automatisierter Rechnungsversand:
- Verwendung von Spring Mail und einem QR-Code-Rechnungs-Generator (SwissQRBill).
- Integration in Buchhaltungssysteme:
- REST-APIs und Message Queues (z. B. RabbitMQ) für künftige Erweiterungen.
- Datenmigration:
- Einsatz von Flyway zur schrittweisen Anpassung des Datenbankschemas.
- 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.
Sequenzdiagramm
Das folgende Sequenzdiagramm veranschaulicht den Ablauf der Erstellung und des Versands von Rechnungen. Dabei wurden ausschliesslich die wichtigsten beteiligten Klassen berücksichtigt.
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.
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.