Wanneer je Spring boot veel gebruikt, dan zal je vaak voor meerdere Spring boot applicaties dezelfde omgevingsafhankelijke code schrijven. Veelal schrijf je dan ook standaard Java libraries om deze code te delen als dit de moeite waard is. Helaas is altijd nog boilerplate code nodig om het binnen een service te gebruiken. Zou het niet fijn zijn als je een Spring boot starter in je dependencies kan opnemen die net zo werkt als de ‘spring-boot-starter-web’? Waarbij belangrijke parameters via propertjes kunnen worden veranderd en zelfs auto-completion in de IDE werkt? In dit artikel schrijven we een eigen starter.
Auteurs: Mohamed Ochalhi en Ben Ooms
Een ander, ook zeer belangrijk, voordeel tegenover de ‘Standard’ boilerplate is ook dat de onderhoudsbaarheid toeneemt. Dit komt doordat de code gemakkelijk op één plek geüpdatet wordt en de services een nieuwe versie kunnen gebruiken. Maar let op, teveel gedeelde code kan ook voor een groot risico zorgen betreffende onderhoudsbaarheid, dus ga hier zuinig mee om. Een laatste valide reden is om ook een starter te schrijven waar (nog) geen starter publiekelijk beschikbaar voor is.
In dit artikel gaan we, na een korte introductie over de werking door middel van een voorbeeld, een eigen starter schrijven. Dit zal een zelfstandige starter zijn. Hierna gaan we verder kijken naar het uitbreiden van een bestaande starter. In de praktijk heeft het over het algemeen niet veel zin om een compleet zelfstandige starter te schrijven, maar zal je een bestaande starter uitbreiden. De reden om dit wel in het artikel te behandelen, is om de focus te hebben op autoconfiguratie en geen last te hebben van randzaken.
Dit artikel borduurt voort op het artikel van Bas Passon in Java magazine 03-2018 genaamd “Spring boot – Een blik onder de motorkap”. Dat is een aanrader als je wilt weten hoe Spring boot werkt. Dit artikel is ook online te vinden op de site van de NL JUG, https://nljug.org/java-magazine/2018-editie-3/java-magazine-3-2018/.
De automatische configuratie van een zelf geschreven starter start met een klasse geannoteerd met @Configuration. Het automatische deel kan je dan sturen door gebruik te maken van predicaten. Er zijn verschillende predicaatsoorten die je hiervoor kan gebruiken en dit kun je zowel op klassen geannoteerd met @Configuration en op @Bean declaraties.
De klasse- en bean condities worden vaak gebruikt, maar Spring biedt ook condities op basis van properties, resources, web en SpEL (Spring expression language). De meeste condities spreken voor zich, op de laatste twee na. Met de web conditie kan je aangeven of een configuratie actief moet zijn als een web context aanwezig is. Of juist niet. De SpEL variant stelt je in staat om de objectgraaf te bevragen.
Nadat je een automatische configuratie geschreven hebt, moet je dit ook in Spring bekend maken. Dit doe je door in jouw Jar een spring.factory bestand toe te voegen in de META-INF map. Spring scant deze locatie standaard op dit bestand. In het bestand declareer je een property genaamd “org.springframework.boot.autoconfigure.EnableAutoConfiguration” waarbij je als waarde de volle gekwalificeerde namen van de configuratieklassen meegeeft, gescheiden door komma’s. Dit zorgt ervoor dat Spring je autoconfiguratie klasse gebruikt wanneer je de applicatie start.
Natuurlijk wil je de automatische configuratie testen. Dit kan heel goed door een combinatie te gebruiken van het spring boot test framework en Assert4J. Er zijn speciale klassen die het simpel maken om je configuratie te testen, namelijk ApplicationContextRunner, WebApplicationContextRunner, en ReactiveWebApplicationContextRunner. Uit de naam van de klassen kan je wel afleiden waar elke klasse voor gebruikt wordt. Het speciale aan deze klassen is dat ze je de automatische configuraties laten vervangen door jouw eigen configuraties wanneer jij dat wilt. Zo kan je dan bijvoorbeeld controleren of een automatische configuratie niet wordt uitgevoerd wanneer een gebruiker van de starter zijn eigen configuratie definieert.
We gaan een starter maken die mensen verwelkomt. Uiteindelijk willen we een custom property maken die het welkomstbericht bepaalt. Daarnaast gaan we ook automatische configuratie toevoegen, die ervoor zal zorgen dat altijd een standaard verwelkomer bean beschikbaar is, tenzij de gebruiker van de starter zelf een verwelkomer bean definieert. Je wilt natuurlijk wel dat de gebruiker van je starter de automatische configuratie kan overschrijven. Uiteindelijk zal de verwelkomer een bericht “Welkom, naam” uitprinten naar de console. De code voor deze starter is beschikbaar op https://github.com/Ochalhi/welcomer-spring-boot-starter.
Om je eigen starter te bouwen, is het handig om te beginnen met een leeg maven project. Nadat je een leeg maven project hebt gemaakt, gaan we de pom.xml aanpassen. De module naam van een custom spring boot starter heeft altijd de suffix ‘-spring-boot-starter’. In ons geval wordt het dus ‘welcomer-spring-boot-starter’. Deze naam conventie bestaat voor het geval dat spring in de toekomst zelf een officiële starter maakt, zo worden overlappende namen voorkomen.
De volgende stap is om de juiste dependencies toe te voegen. Als eerste voegen we de ‘spring-boot-starter’ dependency toe. Dit is de core starter, die automatische configuratie, logging en yaml modules bevat. Je zou ook alleen de ‘spring-boot-autoconfigure’ dependency kunnen toevoegen als je geen logging en yaml modules wilt. Verder wil je ook dat je jouw automatische configuraties kunt testen, hiervoor voegen we de ‘spring-boot-starter-test’ dependency toe. Deze dependency bevat libraries, zoals JUnit, Hamcrest and Mockito. Daarnaast bevat het ook ‘spring-boot-test-autoconfigure’ en ‘spring-boot-test’ dependencies. Deze dependencies bevatten speciale klassen waarmee je extra simpel je zelfgecreëerde spring klasse kan testen.
Als laatste willen we dat auto-completion van onze spring starter properties in onze IDE werkt. Hiervoor moeten we ‘spring-boot-configuration-processor’ toevoegen. Deze dependency is een annotatie processor. Het genereert meta-data over jouw klassen die geannoteerd zijn met @ConfigurationProperties. Deze meta-data wordt dan weer gebruikt door jouw IDE om auto-completion en documentatie te ondersteunen wanneer je bijvoorbeeld de application.properties aanpast. Let wel op dat de meta-data alleen wordt geüpdatet wanneer je het project opnieuw bouwt. Wanneer je jouw custom properties toevoegt, dan zullen deze niet meteen door auto-completion worden ondersteund, totdat je het project herbouwt. Op afbeelding 1 zie je de uiteindelijke inhoud van de pom file.
Nu gaan we de logica van de applicatie in elkaar zetten. We starten met het maken van een klasse voor de properties die we willen hebben. Deze klasse noemen we ‘WelcomerProperties’. Vervolgens definiëren we een property ‘message’ met bijbehorende getter en setter en met initialisatie waarde “Hello”. Dit wordt de standaard begroeting die we gaan gebruiken om een persoon welkom te heten. Als spring een waarde voor de ‘welcomer.message’ property kan vinden in de application.properties file, dan zal het message veld de standaard waarde “Hello” niet behouden. Als laatste voegen we de @ConfigurationProperties annotatie toe met als waarde ‘welcomer’ aan de klasse. Zie afbeelding 2 voor de code.
Hierdoor kunnen we later de variabelen binnen deze klassen veranderen door middel van de application.properties file. Wanneer je de annotatie processor runt, zal deze een file genaamd ‘spring-configuration-metadata.json’ in je compile output META-INF folder genereren. Dit is hoe je IDE weet welke properties beschikbaar zijn. Op afbeelding 3 is de inhoud van ons voorbeeld zichtbaar.
Nu de properties ready zijn, gaan we verder met het maken van een klasse voor onze verwelkomer. Deze noemen we ‘Welcomer’. Deze klasse heeft 1 methode die een String parameter genaamd ‘name’ neemt. Deze methode noemen we ‘welcome’. Zoals de naam zegt, zal deze methode de persoon welkom heten. In afbeelding 4 is de inhoud van deze klasse te zien.
De automatische configuratie klasse is wat jouw spring-magie daadwerkelijk gaat realiseren. We maken een nieuwe klasse ‘WelcomerAutoConfiguration’ met de @Configuration annotatie. Deze annotatie vertelt spring dat er bruikbare beans gedefinieerd staan in deze klasse. Belangrijk om te weten, is dat deze klasse niet automatisch door Spring gescand mag worden. Om dit te realiseren, is het handig om deze klasse in een eigen package te plaatsen.
Vervolgens voegen we de @EnableConfigurationProperties annotatie toe met de waarde ‘WelcomerProperties.class’. Spring creëert dan een WelcomerProperties bean. Je zou als alternatief ook zelf een bean kunnen creëren, zoals je normaal een bean zou maken. Maar op deze manier zijn de configuratie en service niet gescheiden.
Het volgende doel is om een ‘Welcomer’ bean te maken wanneer er nog geen een is geregistreerd. Hiervoor gebruik je het beste de @ConditionalOnMissingBean annotatie. Als je geen waarde meegeeft aan de annotatie, dan gebruikt hij de return value van de geannoteerde method. De code van deze klasse is te zien op afbeelding 5.
We moeten spring wel laten weten dat deze automatische configuratie bestaat, dit doen we door de klasse toe te voegen aan de ‘org.springframework.boot.autoconfigure.EnableAutoConfiguration’ property in de spring.factories file. Aan deze property kun je meerdere klassen meegeven, gescheiden door komma’s. Deze file staat in de META-INF folder van het project. Meestal zal je dit bestand en de map zelf moeten aanmaken.
Om een starter te testen, heb je hulp van Spring nodig. Elke test zal een eigen zorgvuldig opgebouwde Spring context nodig hebben. In ons voorbeeld hebben we minimaal een case nodig waarbij geen welcomer bean aanwezig is en een case waarbij wel een welcomer bean aanwezig is. Als voorbeeld zijn beide cases uitgewerkt en te zien op afbeelding 6.
Een bestaande starter uitbreiden is erg eenvoudig. Alle zaken die besproken zijn bij het bouwen van een zelfstandige starter gelden ook voor het uitbreiden van een bestaande starter. Het enige verschil is dat je de andere starter als dependency toevoegt. Hierna kan je net zoals bij een zelfstandige starter automatische configuratie toevoegen en functionaliteit van de starter die je uitbreid gaan configureren of aanpassen.
Zoals je in dit artikel las, is het niet moeilijk om een Spring boot starter zelf te implementeren. De overhead om een starter zelf te schrijven valt mee, zeker wanneer dit wordt afgezet tegen de voordelen die daarbij komen kijken. Vooral in grote enterprise omgevingen waar Spring boot gebruikt wordt, zijn veel voordelen te behalen. Denk aan bijvoorbeeld een “grote-corporate-web-starter” waarbij alle configuratie die noodzakelijk is (denk hier aan veel boilerplate dat ieder team moet schrijven) al default geconfigureerd is en is uit te breiden, zoals de Spring boot starters.
Het is niet alleen meer fun voor de ontwikkelaar, maar ook qua onderhoudsbaarheid zeer waardevol. Infrastructurele wijzigingen kunnen bijvoorbeeld door middel van een update op de starter op alle projecten toegepast worden. Dit is niet alleen minder foutgevoelig, maar geeft de teams ook meer tijd om business value te ontwikkelen. En is dat niet wat we het liefste doen in plaats van boilerplate te schrijven?