Table of Contents
Einführung: Die Notwendigkeit einer flexiblen Reporting Engine
Unternehmensanwendungen erfordern häufig eine Berichts-Engine, die sich an ständig wechselnde Geschäftsanforderungen anpassen kann. Ein statischer, fest codierter Berichtsgenerator wird schnell zu einer Wartungslast, wenn Stakeholder neue Datenquellen, Filter, Ausgabeformate oder visuelle Layouts benötigen. Das Builder-Muster, ein kreatives Designmuster aus dem Gang of Four, bietet eine saubere Möglichkeit, komplexe Objekte Schritt für Schritt zu konstruieren und gleichzeitig den Konstruktionsprozess unabhängig von der Darstellung des Objekts zu halten. Wenn es auf eine Berichts-Engine in einer Java Spring Boot-Anwendung angewendet wird, ermöglicht dieses Muster echte Anpassbarkeit, Testbarkeit und Erweiterbarkeit.
In diesem Artikel werden wir eine Reporting-Engine von Grund auf entwerfen, beginnend mit einer Kernklasse und einer flexiblen -Schnittstelle. Anschließend werden wir Betonbauer implementieren, sie als Spring Boot-Bohnen integrieren, einen Director für vordefinierte Berichtsvorlagen hinzufügen und reale Überlegungen wie Caching, Thread-Sicherheit und Testen diskutieren. Am Ende haben Sie einen produktionsbereiten Entwurf für ein Reporting-Subsystem, das mit Ihrem Unternehmen wachsen kann.
Das Builder-Muster in der Tiefe verstehen
Das Builder-Muster wird oft mit den Mustern der Abstract Factory oder Factory Method verwechselt, aber sein Zweck ist unterschiedlich: Es leitet die Konstruktion eines Produkts Schritt für Schritt, so dass der Client auswählen kann, welche Schritte aufgerufen werden sollen und in welcher Reihenfolge. Ein klassisches „Design Patterns-Buch Beispiel ist die Erstellung eines Dokuments – Sie möchten vielleicht eine PDF-Version, eine HTML-Version oder eine Klartextversion, die alle aus der gleichen Schrittfolge aufgebaut sind (Header hinzufügen, Absatz hinzufügen, Fußzeile hinzufügen).
Hauptteilnehmer am Muster:
- Produkt – Das komplexe Objekt, das gebaut wird (unser ).
- Builder – Abstrakte Schnittstelle, die die Bauschritte definiert.
- ConcreteBuilder – Implementiert die Builder-Schnittstelle, stellt das Produkt zusammen und stellt eine Methode zum Abrufen des Ergebnisses bereit.
- Director (optional) – Orchestriert die Bauschritte über die Builder-Schnittstelle, kapselt oft eine Standard- oder häufig verwendete Bausequenz ein.
Diese Trennung von Bedenken bedeutet, dass der gleiche Konstruktionsprozess verschiedene Darstellungen einfach durch Austausch des ConcreteBuilder erzeugen kann. für eine Reporting-Engine bedeutet dies, dass in der Lage ist, einen "Zusammenfassungsbericht" oder einen "detaillierten Bericht" unter Verwendung der gleichen FLT: 3 -Schnittstelle, aber unterschiedliche Implementierungen zu generieren.
Entwerfen der Reporting Engine
Unsere Reporting-Engine wird um das Produkt und eine fließende Schnittstelle herum aufgebaut. fließende Schnittstellen (Methodenverkettung) passen natürlich zum Builder-Muster und führen zu lesbarem Client-Code.
Definieren des Berichtsprodukts
Die Klasse FLT:6 enthält die Kerndaten, die für die Erstellung eines Berichts benötigt werden. In einem realen System können Sie Felder für Kopfzeilen, Fußzeilen, Diagrammdefinitionen, Unterberichte usw. hinzufügen.
public class Report {
private String title;
private String dataSource; // e.g., "jdbc/myDb" or "file:/data.csv"
private String query; // SQL or a query identifier
private List<String> columns; // columns to display
private Filter filter; // complex filter object
private String outputFormat; // PDF, CSV, XLSX, HTML
private boolean showTotals;
// private constructor – only builders create instances
private Report() {}
// Builder inner class or external – we'll use an external builder
// Getters (no setters after construction) – omitted for brevity
public String getTitle() { return title; }
public String getDataSource() { return dataSource; }
// etc.
}
Beachten Sie den privaten Konstruktor. Dies erzwingt, dass ein nur durch einen Builder erstellt werden kann, um sicherzustellen, dass jede Instanz richtig konfiguriert ist.
Erstellen des ReportBuilder Interface
Um die Methodenverkettung zu unterstützen, gibt jeder Setter selbst zurück. Eine finale Methode gibt die konstruierte zurück.
public interface ReportBuilder {
ReportBuilder setTitle(String title);
ReportBuilder setDataSource(String dataSource);
ReportBuilder setQuery(String query);
ReportBuilder setColumns(List<String> columns);
ReportBuilder setFilter(Filter filter);
ReportBuilder setOutputFormat(String outputFormat);
ReportBuilder showTotals(boolean showTotals);
Report build();
}
Konkrete Builder können bestimmte Methoden ignorieren (z. B. ein einfacher zusammenfassender Report Builder könnte ignorieren) oder die Konfiguration vor dem Erstellen validieren.
Umsetzung von Concrete Builders
Lassen Sie uns zwei Builder implementieren, um Flexibilität zu demonstrieren: a und a . Beide implementieren die gleiche Schnittstelle, produzieren aber verschiedene Arten von Berichten.
DetailedReportBuilder
public class DetailedReportBuilder implements ReportBuilder {
private Report report = new Report();
@Override
public ReportBuilder setTitle(String title) {
report.setTitle(title);
return this;
}
@Override
public ReportBuilder setDataSource(String dataSource) {
report.setDataSource(dataSource);
return this;
}
@Override
public ReportBuilder setQuery(String query) {
report.setQuery(query);
return this;
}
@Override
public ReportBuilder setColumns(List<String> columns) {
report.setColumns(columns);
return this;
}
@Override
public ReportBuilder setFilter(Filter filter) {
report.setFilter(filter);
return this;
}
@Override
public ReportBuilder setOutputFormat(String outputFormat) {
report.setOutputFormat(outputFormat);
return this;
}
@Override
public ReportBuilder showTotals(boolean showTotals) {
report.setShowTotals(showTotals);
return this;
}
@Override
public Report build() {
// Validate critical fields
if (report.getDataSource() == null) {
throw new IllegalStateException("DataSource must be set");
}
// Additional validation logic...
return report;
}
}
ZusammenfassungReportBuilder
Ein zusammenfassender Bericht ignoriert möglicherweise Spalten, Filter und Gesamtwerte und aggregiert stattdessen alles in einer einzelnen Zahl oder einer einfachen Tabelle.
public class SummaryReportBuilder implements ReportBuilder {
private String title;
private String dataSource;
private String query;
// other fields are ignored or given defaults
@Override
public ReportBuilder setTitle(String title) {
this.title = title;
return this;
}
@Override
public ReportBuilder setDataSource(String dataSource) {
this.dataSource = dataSource;
return this;
}
@Override
public ReportBuilder setQuery(String query) {
this.query = query;
return this;
}
// All other setter methods either do nothing or throw UnsupportedOperationException
@Override
public ReportBuilder setColumns(List<String> columns) {
return this; // summary report ignores columns
}
// ... similar for filter, outputFormat, showTotals
@Override
public Report build() {
Report report = new Report();
report.setTitle(title);
report.setDataSource(dataSource);
report.setQuery(query);
report.setOutputFormat("CSV"); // default format
return report;
}
}
Mit diesem Ansatz kann ein Kunde den Builder auswählen, der der erforderlichen Ausgabekomplexität entspricht, ohne die Baureihenfolge zu ändern.
Hinzufügen eines Directors für vordefinierte Templates
Oftmals möchte man gängige Konstruktionssequenzen einkapseln. Eine Director-Klasse kann das:
public class ReportDirector {
private final ReportBuilder builder;
public ReportDirector(ReportBuilder builder) {
this.builder = builder;
}
public Report constructMonthlySalesReport(String region) {
return builder
.setTitle("Monthly Sales – " + region)
.setDataSource("jdbc/sales_db")
.setQuery("SELECT * FROM sales WHERE region = :region")
.setColumns(List.of("Product", "Units Sold", "Revenue"))
.setFilter(new DateFilter(LocalDate.now().minusMonths(1), LocalDate.now()))
.setOutputFormat("PDF")
.showTotals(true)
.build();
}
public Report constructQuickSummary() {
return builder
.setTitle("Quick Summary")
.setDataSource("jdbc/sales_db")
.setQuery("SELECT count(*) as cnt, sum(revenue) as total FROM sales")
.setOutputFormat("CSV")
.build();
}
}
Der Direktor kann mit jeder Implementierung injiziert werden.
Builder Pattern im Spring Boot: Verdrahtung und Nutzung
Spring Boots Dependency Injection macht es einfach, Builder als Bohnen zu verwalten und zur Laufzeit zu wechseln.
Schritt 1: Definieren Sie Builder als Spring Beans
Wir können unsere konkreten Erbauer mit kommentieren oder sie in einer Klasse deklarieren:
@Configuration
public class ReportConfig {
@Bean
@Scope("prototype") // because each builder session uses a fresh instance
public DetailedReportBuilder detailedReportBuilder() {
return new DetailedReportBuilder();
}
@Bean
@Scope("prototype")
public SummaryReportBuilder summaryReportBuilder() {
return new SummaryReportBuilder();
}
@Bean
@Scope("prototype")
public ReportDirector reportDirector(ReportBuilder builder) {
// This bean will not resolve without specifying the builder – we'll discuss later
return new ReportDirector(builder);
}
}
Die Verwendung von scope ist wichtig: Jeder Aufruf von sollte eine neue Builder-Instanz mit einem neuen internen Zustand erstellen.
Um die Tatsache zu bewältigen, dass einen bestimmten Builder erfordert, können wir oder ein Fabrikmuster verwenden.
@Bean
public ReportDirector detailedReportDirector(@Qualifier("detailedReportBuilder") ReportBuilder builder) {
return new ReportDirector(builder);
}
@Bean
public ReportDirector summaryReportDirector(@Qualifier("summaryReportBuilder") ReportBuilder builder) {
return new ReportDirector(builder);
}
Schritt 2: Integrieren Sie Builder / Directors in Controller oder Services
Ein typischer Controller kann einen Berichtstypparameter akzeptieren und die entsprechende Komponente verwenden:
@RestController
@RequestMapping("/reports")
public class ReportController {
@Autowired
private ReportDirector detailedReportDirector;
@Autowired
private ReportDirector summaryReportDirector;
@GetMapping("/monthly/{region}")
public ResponseEntity<Report> getMonthlySales(@PathVariable String region) {
Report report = detailedReportDirector.constructMonthlySalesReport(region);
// Execute report generation logic...
return ResponseEntity.ok(report);
}
@GetMapping("/summary")
public ResponseEntity<Report> getSummary() {
Report report = summaryReportDirector.constructQuickSummary();
return ResponseEntity.ok(report);
}
}
Alternativ können Sie Builder direkt einfügen und die Serviceschicht auswählen lassen. Der Schlüsselpunkt: Der Client-Code kennt die Builder-Interna nie - er ruft nur oder eine Director-Methode auf.
Advanced Customization: Dynamische Builder mit Spring's ObjectProvider
Manchmal muss die Builder-Auswahl zur Laufzeit erfolgen, basierend auf Konfigurationseigenschaften oder Benutzerrollen. Springs kann helfen, eine Prototyp-Bohne faul zu injizieren:
@Service
public class ReportService {
private final ObjectProvider<DetailedReportBuilder> detailedBuilderProvider;
private final ObjectProvider<SummaryReportBuilder> summaryBuilderProvider;
public ReportService(ObjectProvider<DetailedReportBuilder> detailedBuilderProvider,
ObjectProvider<SummaryReportBuilder> summaryBuilderProvider) {
this.detailedBuilderProvider = detailedBuilderProvider;
this.summaryBuilderProvider = summaryBuilderProvider;
}
public Report generateReport(String type, Map<String, String> params) {
ReportBuilder builder;
if ("detailed".equalsIgnoreCase(type)) {
builder = detailedBuilderProvider.getObject();
} else {
builder = summaryBuilderProvider.getObject();
}
// Apply common params (e.g., title, dataSource)
String title = params.getOrDefault("title", "Report");
builder.setTitle(title)
.setDataSource(params.get("dataSource"));
// Build
return builder.build();
}
}
Dieses Muster vermeidet es, jeden möglichen Builder direkt vorverdrahten zu müssen, während der Code sauber und testbar bleibt.
Gewährleistung von Unveränderlichkeit und Thread-Sicherheit
Da Builder typischerweise in einem Thread verwendet werden und nicht geteilt werden, müssen wir den Builder nicht selbst synchronisieren.
Um Unveränderlichkeit zu erzwingen, mache die Klasse [FLT: 33] wirklich unveränderlich:
- Alle Felder als markieren.
- Übergeben Sie alle Werte durch den Konstruktor (der Builder ruft einen privaten Konstruktor auf, der alles festlegt).
- Geben Sie nur Getters, keine Setters.
- Für Sammlungen (z. B. Spalten) erstellen Sie Verteidigungskopien im Konstruktor oder verwenden Sie .
public class Report {
private final String title;
private final String dataSource;
private final String query;
private final List<String> columns;
private final Filter filter;
private final String outputFormat;
private final boolean showTotals;
Report(String title, String dataSource, String query,
List<String> columns, Filter filter,
String outputFormat, boolean showTotals) {
this.title = title;
this.dataSource = dataSource;
this.query = query;
this.columns = columns == null ? List.of() : List.copyOf(columns);
this.filter = filter;
this.outputFormat = outputFormat;
this.showTotals = showTotals;
}
// getters...
}
Dann erschafft das das über diesen vollständigen Konstruktor, indem es alle gesammelten Werte übergibt.
Testen der Reporting Engine
Das Builder-Muster macht das Testen einfach, weil Sie Schein-Builder oder testspezifische Builder einspritzen können. Für Unit-Tests des -Produkts können Sie es direkt mit einem Builder instanziieren. Für Integrationstests können Sie überprüfen, ob der richtige Builder aufgerufen wird und der Abschlussbericht den Erwartungen entspricht.
Unit Testing eines Concrete Builders
@Test
void testDetailedReportBuilder() {
DetailedReportBuilder builder = new DetailedReportBuilder();
Report report = builder
.setTitle("Test")
.setDataSource("jdbc/test")
.setOutputFormat("PDF")
.build();
assertThat(report.getTitle()).isEqualTo("Test");
assertThat(report.getDataSource()).isEqualTo("jdbc/test");
assertThat(report.getOutputFormat()).isEqualTo("PDF");
assertThat(report.isShowTotals()).isFalse(); // default
}
Testen mit Mocks
Beim Testen eines Dienstes, der einen Builder verwendet, sollten Sie die Builder-Schnittstelle verspotten, um Interaktionen zu überprüfen:
@Test
void testReportServiceUsesBuilderCorrectly() {
ReportBuilder mockBuilder = mock(ReportBuilder.class);
when(mockBuilder.setTitle(any())).thenReturn(mockBuilder);
when(mockBuilder.setDataSource(any())).thenReturn(mockBuilder);
// ... other stubs
Report expectedReport = new Report(/* ... */);
when(mockBuilder.build()).thenReturn(expectedReport);
ReportService service = new ReportService(/* ... */);
// inject mockBuilder via a test specific method
Report result = service.generateReport("detailed", Map.of("title", "Test", "dataSource", "jdbc/db"));
assertThat(result).isSameAs(expectedReport);
verify(mockBuilder).setTitle("Test");
verify(mockBuilder).setDataSource("jdbc/db");
verify(mockBuilder).build();
}
Leistungsbetrachtungen und Caching
Das Erstellen eines -Objekts selbst ist billig – es ist lediglich das Zusammenstellen von Konfigurationsdaten. Der teure Teil ist das Ausführen der zugrunde liegenden Abfrage, das Transformieren von Daten und das Generieren der Ausgabedatei (PDF, XLSX). Daher sollte der Builder keine I/O auslösen. Diese Verantwortung gehört zu einem separaten oder einem ähnlichen Dienst.
Wenn dieselbe Berichtskonfiguration wiederholt angefordert wird (z. B. der gleiche monatliche Verkaufsbericht für dieselbe Region), können Sie das Objekt zwischenspeichern (die Konfiguration) und es wiederverwenden. Zum Zwischenspeichern können Sie Springs auf der Director-Methode oder Service-Methode verwenden. Da das unveränderlich ist, ist es sicher, ohne defensive Kopien zwischenzuspeichern.
@Cacheable("reportConfigs")
public Report getMonthlySalesConfig(String region) {
return detailedReportDirector.constructMonthlySalesReport(region);
}
Durch das Zwischenspeichern der Konfiguration kann der Builder nur einmal pro eindeutigem Satz von Parametern ausgeführt werden, wodurch nachfolgende Anforderungen noch vor der Ausführung der Abfrage beschleunigt werden.
Vergleich des Builder-Musters mit anderen Ansätzen
Beim Entwerfen einer Berichtsmaschine können Sie andere Muster berücksichtigen:
- Factory Method – Gut für die Erstellung eines Berichtsobjekts in einem Schritt, unterstützt aber keine schrittweise Konfiguration.
- Konstruktor mit vielen Parametern – Teleskop-Konstruktoren sind fehleranfällig und schwer zu lesen. Das Builder-Muster bietet einen klaren, benannten Parameter-Stil.
- JavaBeans-Muster (mutable setters) – Ermöglicht eine schrittweise Konfiguration, unterbricht jedoch die Unveränderlichkeit und kann zu teilweise initialisierten Objekten führen.
- Strategy Pattern – Könnte mit Builder kombiniert werden; der Builder könnte eine Strategie für das Rendern oder Datenabrufen akzeptieren.
Das Builder-Muster zeichnet sich aus, wenn das Produkt viele optionale Komponenten wie einen Bericht enthält. Es unterstützt auch das Open/Closed-Prinzip - Sie können neue Berichtstypen hinzufügen, indem Sie einen neuen Builder implementieren, ohne den vorhandenen Code zu ändern.
Real-World Erweiterungen
Eine Produktions-Reporting-Engine benötigt oft mehr als nur eine einfache Konfiguration.
- Verschachtelte Builder für Subreports – jeder Subreport kann einen eigenen Builder haben.
- Ein -Konzept – vorkonfigurierte Builder, die in einer Datenbank oder YAML-Dateien gespeichert sind.
- Integration mit Spring Cloud Config, um Berichtsvorlagen zu ändern, ohne sie neu zu implementieren.
- Mit der Lombok-Annotation wird die Builder-Klasse automatisch generiert. Vorsicht: Lombok generiert einen statischen verschachtelten Builder, der möglicherweise keine polymorphen Builder für verschiedene Berichtstypen erlaubt. Für unseren Zweck geben benutzerdefinierte Builder mehr Kontrolle.
Beispielsweise könnte ein YAML‐basiertes Template geladen werden:
monthly-sales:
title: "Monthly Sales - ${region}"
dataSource: "jdbc/sales"
query: "SELECT ..."
columns: ["Product", "Units Sold"]
outputFormat: "PDF"
showTotals: true
Ein Dienst könnte diese Vorlage analysieren und die entsprechenden Builder-Methoden aufrufen, wodurch die Berichtsmaschine vollständig datengesteuert wird.
Schlussfolgerung
Das Builder-Muster, wenn es auf eine Java Spring Boot-Reporting-Engine angewendet wird, bietet eine saubere Trennung zwischen der Konstruktion von Berichtskonfigurationen und ihrer Darstellung. Durch die Definition einer fließenden -Schnittstelle und die Implementierung mehrerer Beton-Builder ermöglichen Sie eine dynamische, laufzeitabhängige Anpassung von Berichten, ohne technische Schulden zu sammeln. Die optionale Director-Klasse kapselt häufig verwendete Sequenzen ein und die Abhängigkeitsinjektion von Spring macht es trivial, alles miteinander zu verkabeln.
Dieses Design ist nicht nur erweiterbar – man kann neue Berichtstypen durch das Schreiben eines neuen Builders hinzufügen – sondern auch testbar, denn Builder sind einfache Java-Objekte, die isoliert verspottet oder instanziiert werden können. In Kombination mit unveränderlichen Produkten und Caching bleibt die Engine performant und sicher.
Egal, ob Sie ein einfaches Dashboard oder eine vollwertige Business Intelligence-Plattform erstellen, das Builder-Muster bietet Ihnen die Flexibilität, sich ändernden Anforderungen zu entsprechen und gleichzeitig eine Codebasis zu pflegen, mit der Sie gerne arbeiten können. Weitere Informationen finden Sie in der offiziellen Fring Framework-Dokumentation zu Bohnenbereichen und im klassischen Design Patterns-Buch für weitere Kontexte zu Schöpfungsmustern.