Spring configureren via een geannoteerde Java klasse. Voor de één een mooie oplossing en voor de ander is het een gruwel. Dit artikel gaat niet over deze discussie, maar over de werking en mogelijkheden van het configureren van de Spring container door middel van de @Configuration annotatie.
Dit artikel is gebaseerd op ervaringen met het gebruik hiervan en op bestudering van de javadocs van de besproken annotaties en classes. Doel is dan ook niet om volledig te zijn, daar biedt Spring voldoende documentatie voor. Het doel is om een eenvoudig overzicht te hebben van de mogelijkheden, punten waar op te letten en mogelijk toepassing van de functionaliteit. Ook zullen we belichten hoe een Spring container omgaat met een mix van XML en op annotaties gebaseerde configuratie. Als laatste tonen we hoe in een webcontext de Spring container te instantiëren wanneer je een geannoteerde configuratie hebt.
De @Configuration annotatie biedt de mogelijkheid om naast of zonder de reguliere XML configuratie een simpele Java klasse te annoteren met @Configuration. In deze Java klasse kun je de Beans configureren zoals je in XML zou doen. Deze geannoteerde Java klasse is zelf gewoon een Bean zoals een @Component en kan als zodanig gebruikt worden. Sinds Spring 3.1 zijn er geen beperkingen tegenover XML configuratie, alles wat je in XML kunt definiëren, kun je dus ook in een Java klasse annoteren met @Configuration.
Enkele voordelen van het configureren op deze manier zijn onder andere dat de configuratie door middel van Java geschreven is, mogelijke fouten eerder zichtbaar zijn (tijdens compileren), het mogelijk is Beans van een anonymous inner type te declareren en het conditioneel creëren van een Bean. De container kan prima overweg met een mix van XML en geannoteerde configuratie, het ene sluit het andere niet uit.
Om Java configuratie te kunnen gebruiken, moet naast de reguliere Spring afhankelijkheden ook CGlib toegevoegd worden wanneer je een Spring versie onder 3.2 gebruikt. Dit komt omdat CGlib door Spring als een optionele afhankelijkheid gedefinieerd is. Naast Java configuratie wordt CGlib ook door Spring AOP gebruikt. De reden waarvoor CGlib noodzakelijk is, is omdat de @Configuration geannoteerde klasse een gewone bean is die daarnaast ook de context, scope en Bean semantiek moet weten. Hiervoor maakt Spring een CGlib proxy voor elke @Configuration Bean aan. Vanaf Spring 3.2 is deze afhankelijkheid niet meer nodig, en is de CGlib code van versie 3.0 geïntegreerd in Spring en te vinden in de org.springframework.cglib package.
Doordat CGlib tijdens het opstarten dynamisch functionaliteit toevoegt, zijn er twee eisen waar een @Configuration geannoteerde klasse aan moet voldoen: de klasse mag niet final zijn en er moet een default no args constructor zijn.
De simpelste form kan er zo uitzien:
Package com.foo.config;
@Configuration
public class MyConfig{
public MyConfig(){}
@Bean coffeeBean(){
Return new CoffeeBean();
}
}
In XML zou de configuratie er zo uitzien:
<beans>
<bean id=”coffeBean” class=”com.foo.config.CoffeBean” > </>
</ beans>
Om deze configuratie te laden in een XML geconfigureerde container, moet de welbekende (als je al met Spring annotaties werkt) context:component-scan of de context:annotation-config tag aanwezig zijn. Zoals je misschien al verwacht, is een @Configuration geannoteerde klasse inderdaad gewoon een bean zoals een @Component of soortgelijke geannoteerde klasse. Als je met de context:component-scan wil werken is dit in je XML voldoende:
<context:component-scan />
Zoals getoond in bovenstaand voorbeeld, kun je in een bestaande XML gebaseerde Spring configuratie door middel van de context:component-scan Java gebaseerde configuratie toevoegen. Wil of kan je de component-scan niet gebruiken, dan is dat ook mogelijk door middel van de context:annotation-config en voeg je deze tag toe in je XML:
<context:annotation-config />
Daarnaast is het ook mogelijk om alleen op annotatie gebaseerde configuratie in te laden. Spring heeft hiervoor een tweetal bootstrap classes. De AnnotationConfigApplicationContext en de web versie AnnotationConfigWebApplicationContext.
De AnnotationConfigApplicationContext kan op drie manieren geïnstantieerd worden. De eerste manier lijkt op de manier zoals je dit met een XML gebaseerde Spring configuratie zou doen:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Configuratie.class);
De tweede manier gebruikt de default constructor en het scanning mechanisme:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan(“de.package.die.je.wil.scannen”);
ctx.refresh();
Als laatste de manier die erg op de tweede manier lijkt maar de register methode gebruikt om de container te voorzien van de configuratie:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigA.class);
ctx.register(ConfigB.class);
ctx.refresh();
De AnnotationConfigWebApplicationContext is op dezelfde manieren te instantiëren als de XML variant.
De op annotaties gebaseerde configuratie heeft twee annotaties om andere configuraties in te laden. Als je een andere Java geannoteerde configuratie wilt inladen, gebruik je de @Import annotatie. Met @ImportResource kun je een XML configuratie inladen. Tijdens het schrijven van de voorbeeld code, kwam ik er achter dat je hiermee moet uitkijken wanneer je meer dan een @Configuration geannoteerde klasse heb. Doordat een Java klasse met de @Configuration onderhuids ook een gewone Bean is, kun je een andere configuratie klasse ook door middel van @Autowired injecteren. Dit heeft als voordeel dat het meteen duidelijk is waar de gebruikte Bean geconfigureerd wordt. Het nadeel is echter dat hierdoor de beide configuratie klassen tightly coupled zijn.
Een voorbeeld hiervan is bijvoorbeeld:
Gegeven een xml configuratie genaamd springapp.xml
@Configuration
@ImportResource(“springapp.xml”)
public class MyConfig{
rest van de klasse...
Externe properties kunnen geladen worden door middel van de @PropertySource annotatie of door middel van de @Value annotatie met een geconfigureerde propertyPlaceholderConfigurator. Als je de @PropertySource manier gebruikt, moet je ook een Spring Environment laden door middel van bijvoorbeeld @Autowired in de configuratie klasse. Daarna kun je de property inlezen met de getProperty('naam”) van de geïnjecteerde Environment klasse. Belangrijk om te weten is dat wanneer twee ingeladen properties bestanden dezelfde property bevatten, de waarde van de laatst geladen properties bestand gebruikt wordt en dat de properties alleen binnen een @Bean definitie te gebruiken zijn.
Gegeven een property file genaamd props.properties met een key genaamd propKey
@Configuration
@propertySource(“props.properties”)
public class MyConfig{
@Autowired Environment env
@Bean InjectedBean injectedBean(){
InjectedBean b = new InjectedBean();
b.setProp(env.getProperty(“propKey”));
return b;
}
rest van de klasse...
Met de PropertyPlaceholderConfigurator heb je twee keuzes. Als deze al in een XML gedefinieerd wordt, kun je direct aan de slag met de @Value annotatie om properties in te lezen. Mocht die er niet zijn, dan kun je deze ook instantiëren in de Java configuratie klasse. Om dit te doen moet je op klasse niveau door middel van de @PropertySource annotatie de locatie van het properties bestand, een PropertyPlaceholderConfigurator bean definiëren en properties inlezen met @Value.
Sinds Spring 3.1 kun je verschillende profielen gebruiken. Met een profile creëer je een logisch bij elkaar horende Bean verzameling. Deze verzameling wordt alleen geladen wanneer het profiel als actief wordt aangemerkt. Het activeren van een profile kan door middel van het toevoegen van een System property genaamd spring.profiles.active met als waarde een of meerdere profiel namen, of door de methode setActiveProfile aan te roepen. Vanaf Spring 3.1 is voor de XML versie aan de beans tag het attribuut profile gekomen en in een op basis van annotaties geconfigureerde container kun je de @Profile annotatie toevoegen. Interessant aan de profielen is dat je deze runtime kan laden.
@Configuration
@profile(“st”)
public class MyConfig{
rest van de klasse...
// Profiel laden in context
ApplicationContext ctx = new annotationConfigContext();
ctx.setActiveProfile{“st”);
ctx.refresh();
Zoals je kunt zien is de flexibiliteit erg groot en kun je een Java configuratie op veel verschillende manieren gebruiken. Je kunt bijvoorbeeld binnen een bestaande XML geconfigureerde container met de component-scan of annotation-config Java configuratie toevoegen, of een bestaande XML gebaseerde container ombouwen tot een op annotatie gebaseerd container door gebruik te maken van de @ImportResource om zodoende een geleidelijke transitie te kunnen maken. Na het maken van de voorbeeld code ben ik er achter gekomen dat hoewel een mix tussen XML en geannoteerde goed te doen is het de onderhoudbaarheid niet ten goede komt. Zeker wanneer je Spring profielen, XML en geannoteerde configuratie combineert, moet je de geconfigureerde container goed testen op de aanwezigheid van de door jou verwachte Beans.
Beans zijn te definiëren door de @Bean annotatie toe te voegen aan een methode. Het interessante van een Bean annotatie is dat deze ook gedeclareerd kunnen worden in een niet @Configuration geannoteerde klasse. Let er wel op dat als de Beans gedeclareerd worden in een klasse die niet de annotatie @Configuration heeft, de scope niet gerespecteerd wordt.
De optionele attributen van de @Bean annotatie zijn name, autowire, initmethod en destroymethod. Als er geen specifieke naam gegeven is aan de bean zal de naam hetzelfde zijn als de method. Dit is te veranderen door het attribuut name toe te voegen. Interessant is dat de name attribuut een array van Strings accepteert om dus meerdere aliassen toe te kennen aan de Bean.
Met het autowire attribuut geeft men aan of de bean attributen automatisch geïnjecteerd moeten worden. Standaard staat dit uit. De mogelijke andere waarden zijn net als bij een XML geconfigureerde container injectie door middel van bean type of naam.
Met de init methode parameter kun je aangeven welke methode aangeroepen moet worden na instantiëring van de bean. De bruikbaarheid hiervan is discutabel, want hetzelfde kan worden gedaan door in de Bean methode body de bewuste methode aan te roepen op het object.
De destroy method parameter is wel iets bruikbaarder. Hiermee kun je, zoals de naam al aanduidt, aangeven welke method moet worden aangeroepen wanneer de Bean gedestroyed wordt. Als je deze parameter niet meegeeft, gaat de Spring container standaard bij het destroyen op zoek naar een publieke no arg methode close() en voert het deze uit. Deze standaard kun je overrulen door een lege String destroy methode naam mee te geven. Wanneer je dit doet zal de aanwezige close methode niet aangeroepen worden.
In de javadoc staat een specifieke uitzondering waar je op moet letten wanneer je een Bean definieert die een BeanFactoryPostProcessor type retourneert. Omdat deze Beans meestal zeer vroeg in de container lifecycle benodigd zijn, kunnen ze storend werken bij het verder instantiëren van andere Beans. Het advies is dan ook om deze type Beans als static te definiëren. Mocht de Spring container deze type Beans instantiëren vanuit een methode die niet static is, dan zal er een waarschuwing in de log vermeld worden. Nadeel is echter dat deze Beans niet voor Bean scoping of AOP in aanmerking komen wanneer ze door middel van een static methode zijn geïnstantieerd.
Ik hoop dat je met dit artikel een goede indruk hebt gekregen van de mogelijkheden die beschikbaar zijn wanneer je Spring wil gebruiken met een configuratie door middel van annotaties. Op bitbucket kun je een maven eclipse project uitchecken op https://bitbucket.org/bendh/spring-configuration-sandbox waarbij de in dit artikel besproken mogelijkheden gedemonstreerd worden. Het project kun je direct gebruiken als een Spring sandbox om met Spring te experimenteren.