Blog
De verborgen lessen van Spring: één framework, drie perspectieven
Hoe verandert je kijk op het Spring framework naarmate je ervaring groeit? Ontdek de belangrijkste lessen en valkuilen voor junior, medior en senior developers.
Spring behoort al jaren tot de meest gebruikte frameworks binnen het Java-ecosysteem. Met Dependency Injection en een uitgebreid containermodel neemt het heel wat complexiteit weg voor developers.
Toch verandert je kijk op Spring naarmate je ervaring groeit. De vragen die je jezelf stelt als junior developer verschillen fundamenteel van die van een medior of senior developer. Niet omdat het Spring framework verandert, maar omdat je steeds meer ontdekt wat er achter de schermen gebeurt.
In deze blog bekijken we hoe developers op verschillende ervaringsniveaus naar het Spring framework kijken en welke inzichten daarbij horen.
1. Junior developer: leren vertrouwen op de container
Wanneer je start met Spring, draait alles rond één fundamenteel principe: de container neemt verantwoordelijkheden over die je vroeger zelf beheerde. In plaats van objecten zelf aan te maken met het new-keyword, laat je de Spring-container bepalen wanneer en hoe afhankelijkheden worden geïnjecteerd via Inversion of Control (IoC) en Dependency Injection (DI).
Best practice: Constructor vs. Setter Injection
Gebruik Constructor Injection voor verplichte afhankelijkheden. Dit maakt onveranderlijke (immutable) objecten mogelijk, garandeert dat afhankelijkheden nooit null zijn en maakt je code veel beter testbaar.
Gebruik Setter Injection uitsluitend voor optionele afhankelijkheden die een zinvolle standaardwaarde kunnen hebben.
De “gotcha”: circulaire afhankelijkheden
Een klassieke valkuil voor juniors is het creëren van een circulaire afhankelijkheid. Als Klasse A Klasse B nodig heeft in de constructor en Klasse B op zijn beurt Klasse A nodig heeft, kan Spring de applicatie niet opstarten en wordt er een BeanCurrentlyInCreationException gegooid.
De oplossing
Vaak ligt de oplossing niet in een technische workaround (zoals setter-injectie als laatste redmiddel), maar in een beter ontwerp waarbij je de componenten ontkoppelt.
2. Medior developer: begrijpen waarom het Spring framework zich anders gedraagt dan verwacht
Op dit niveau verschuift de focus. Je gebruikt het Spring framework niet langer blindelings, maar probeert ook te begrijpen waarom bepaalde zaken gebeuren. De focus ligt hier op het beheersen van Bean scopes, proxy’s en Aspect-Oriented Programming (AOP).
Veelvoorkomende valkuil #1: Singleton versus Prototype
Veel developers verwachten dat een Prototype Bean altijd een nieuwe instantie oplevert. Wat gebeurt er echter als een Singleton Bean (één keer aangemaakt) afhankelijk is van een Prototype Bean (elke keer een nieuwe instantie)?
De Prototype Bean wordt slechts één keer geïnjecteerd op het moment dat de Singleton Bean wordt aangemaakt. Je krijgt daarna telkens dezelfde “prototype”-instantie terug, wat vaak een ander resultaat geeft dan verwacht.
De oplossing
Gebruik Method Injection (zoals @Lookup) of een ObjectProvider om telkens een nieuwe instantie op te halen wanneer dat nodig is.
Veelvoorkomende valkuil #2: de “lite” mode-valstrik (@Configuration vs. @Component)
Een @Bean-method binnen een klasse met @Component werkt fundamenteel anders dan binnen een klasse met @Configuration. Een klein verschil in annotatie leidt tot een volledig ander resultaat.
- Full mode (@Configuration): Spring gebruikt CGLIB-subclassing om te garanderen dat een aanroep naar een @Bean-method altijd dezelfde gecachte singleton-instantie teruggeeft.
- Lite mode (@Component): Spring ziet dit als een gewone factory-method. Als je de method handmatig aanroept (bijvoorbeeld otherBean() binnen een andere @Bean-method), krijg je een nieuwe instantie buiten het beheer van de container om. Scoping, AOP en andere post-processors worden dan niet meer correct toegepast op die aanroep.
Veelvoorkomende valkuil #3: de AOP self-invocation-valkuil (waarom werkt @Transactional soms niet?)
In de standaard (proxy-based) Spring AOP worden proxy’s gebruikt om functionaliteiten zoals transacties toe te voegen.
Als een klasse haar eigen method intern aanroept (bijvoorbeeld this.bar() vanuit foo()), loopt de oproep niet via de proxy. Hierdoor worden aspecten op bar() (zoals @Transactional of @Async) onverwacht niet uitgevoerd.
3. Senior developer: begrijpen wat er achter de schermen gebeurt
Op senior niveau verschuift de aandacht opnieuw. De focus ligt niet langer op het gebruik of het verklaren van gedrag, maar op de werking van de container zelf, de bean lifecycle, container extensibility en de onderliggende Spring-infrastructuur.
Je gebruikt de container niet alleen meer; je breidt deze uit.
Belangrijk concept #1: de “power move” (BFPP vs. BPP)
Het begrijpen van het verschil tussen een BeanFactoryPostProcessor (BFPP) en een BeanPostProcessor (BPP) is essentieel voor infrastructuurcode. Dit vormt de basis van veel Spring-functionaliteiten:
- BFPP (BeanFactoryPostProcessor): werkt op de bean-configuratiemetadata voordat er überhaupt beans worden aangemaakt. Hier kun je eigenschappen aanpassen via bijvoorbeeld placeholders.
- BPP (BeanPostProcessor): werkt op de daadwerkelijke bean-instanties nadat ze zijn aangemaakt en geconfigureerd. Dit is hoe Spring @Autowired en AOP-proxy’s afhandelt.
De senior valkuil: vroege initialisatie (early initialization)
Wees uiterst voorzichtig met het injecteren van afhankelijkheden in een @Configuration-klasse of een BeanPostProcessor. Omdat deze zeer vroeg in de levenscyclus worden verwerkt, kunnen ze ervoor zorgen dat andere beans voortijdig worden geïnitialiseerd. Hierdoor worden die beans niet correct verwerkt door latere BPP’s (zoals AOP auto-proxying).
De oplossing
Gebruik static @Bean-methodes voor post-processordefinities zoals BPP/BFPP om te voorkomen dat de omliggende configuratieklasse te vroeg wordt geïnstantieerd.
Belangrijk concept #2: geavanceerd lifecycle beheer
Bij grotere applicaties wordt de volgorde waarin componenten opstarten steeds belangrijker. Vertrouw voor complexe opstartvereisten niet alleen op @PostConstruct. Kijk naar SmartLifecycle.
Hiermee kun je specifieke fasen (getPhase()) definiëren, zodat bijvoorbeeld een message listener pas start nadat databaseverbindingen en caches volledig gereed zijn.
Conclusie
Hoewel elke developer met hetzelfde Spring framework werkt, verschuift de focus naarmate de ervaring groeit. Een junior developer leert vertrouwen op de container. Een medior developer probeert onverwacht gedrag te verklaren. Een senior developer richt zich op de mechanismen achter de schermen en begrijpt hoe Spring zichzelf organiseert en hoe dat gedrag kan worden uitgebreid of beïnvloed.
Het Spring framework verandert niet. Het perspectief waarmee je naar het framework kijkt wel.
< Keep reading />
More from our team
Explore more insights, tips, and deep dives from the CraftCode team.
Aan de slag!
Klaar om jouw visie werkelijkheid te maken?
Laten we iets bouwen waar je bedrijf écht mee verder kan.