Av: Jonas Heder

2017-06-15

Dependency injection inom Java

Systemutveckling kan underlättas om man strävar efter en struktur i applikationen som möjliggör att komponenter enkelt kan bytas ut utan att behöva modifiera stora delar av kodbasen. Att komponenterna är löst kopplade till varandra. Några skäl till detta är:

  • Möjliggöra enkel enhetstestning av komponenten
  • Flexibelt kunna byta ut komponentens specifika implementation vid behov
  • Återanvända komponenten på olika ställen
  • Separera komponentens eventuellt komplexa konfigurering från där den används
  • Använda olika konfigurationer i olika miljöer

Om klass A använder klass B så är det därför bra om man kan undvika att hårdkoda vilken specifik implementation av klass B som används, mitt bland resten av programkoden i klass A, då klass A i så fall är beroende till just den implementationen av klass B.

Det ger större flexibilitet att istället skicka in den specifika implementationen av klass B utifrån, något som kallas för dependency injection. Det är en typ av inversion of control, alltså att implementationen som klassen är beroende av definieras utanför de ställen där den faktiskt används, där implementationen enklare kan kontrolleras.

Att separera konfigureringen från användningen är speciellt fördelaktigt ifall klass B:s sådan är komplex, exempelvis upprättning av en databaskoppling, som inte har med klass A att göra. Ju mindre klass A behöver känna till om klass B, desto lösare är beroendet. Det ger dessutom större frihet att kunna styra hur många instanser av klass B som ska användas i applikationen överlag, något som inte klass A bör bestämma.

För att tillåta olika implementationer av klass B att skickas in i klass A så definieras ett interface som klass B i sin tur implementerar. Klass A anger sedan interface:et istället för klass B:s konkreta implementation. Alla klasser som korrekt implementerar interface:et ska då fungera att skickas in.

Det finns olika sätt att tillgodose beroenden utifrån, antingen via klass A:s konstruktor (constructor injection), den metod som anropas (setter injection) eller genom att direkt sätta värdet på ett fält utifrån (field injection). Om fältet är privat kan detta bara göras genom reflection, vilket inte är att rekommendera då det både ger sämre prestanda i längden och är ett anti-pattern då det går emot den objektorienterade strukturen om man faktiskt definierat fältet som privat.

Objektgraf

När ett beroende skickas in till en klass utifrån istället för att deklareras direkt i klassen så uppstår en kedja där beroendenas egna beroenden i sin tur också skickas in i dem. Denna struktur bildar en s.k. objektgraf. För att bygga upp en objektgraf används oftast färdiga ramverk.

Scope och livscykel

I en applikation har oftast olika objekt olika scope. Ett visst objekt kanske bara ska instansieras en enda gång och återanvändas genom hela applikationen, medan ett annat ska skapas på nytt vid varje injection-punkt. Exempelvis kan ett objekt med sessionsinformation ofta ha ett bredare scope än ett objekt som hör till en viss vy i applikationen. Objekt-scope går att specificera i de flesta dependency injection-ramverk. Vissa ramverk tillhandahåller även livscykelhantering, där metoder i en klass kan anropas av ramverket vid olika tillfällen under objektets instansiering, eller senare, exempelvis om en databaskoppling ska upprättas eller tas ner. Ibland kan termerna scope och livscykelhantering blandas ihop lite i olika artiklar, men det är ovanstående definition som menas i denna text.

JSR-330 och JSR-299

JSR-330 (Dependency Injection for Java) är en standardiserad samling annotationer och interface som släpptes 2009. Dependency injection-ramverk kan baseras på JSR-330 för att göra det enklare att byta ramverk i applikationen. Annotationerna är @Inject, @Named, @Scope, @Qualifier, @Singleton och interface:et Provider. Det är vanligt att dependency injection-ramverk anger att de följer denna standard.

JSR-299 (Java Contexts and Dependency Injection) är en påbyggnad av JSR-330 och innehåller en mycket större mängd annotationer för bl.a. livscykelhantering, interceptors, decorators, events och type safety.

Ramverk

Inom Java finns det många dependency injection-ramverk, och dessa varierar beroende på vilket område inom Java man arbetar inom. I enterprise-sammanhang är det vanligare att applikationen körs inom en omgivande container-applikation, som erbjuder en större mängd funktionalitet, där ibland dependency injection. Exempel är Java EE-baserade applikationsservrar som JBoss och WebSphere (Context Dependency Injection / Weld) eller Spring-ramverket. Dessa analyserar klassers beroenden i runtime via reflection. Inom Android, där batteritid är en viktig faktor, är runtime-analys av beroenden mindre lämpligt då det kräver mer processorkraft. Ett populärt ramverk inom Android är Dagger, som istället genererar strukturen för att koppla ihop klasserna i compile time, vilket då inte kräver mer processorkraft än om man definierat strukturen själv i sin kod.

Nedan följer en kortare beskrivning av de vanliga dependency injection-ramverken inom Java, och lite om hur de skiljer sig.

Java EE Contexts and Dependency Injection (JSR-299)

Java EE släpptes 1999 och är en specifikation som definierar strukturen och sambanden mellan ett stort antal komponenter som är användbara i en enterprise-applikation, bl.a. dependency injection (tidigare nämnda JSR-299). Det finns olika implementationer av denna specifikation, i form av olika applikationsservrar, bl.a. WildFly (JBoss), WebSphere och GlassFish. Varje implementation av Java EE kan använda antingen en egen implementation av CDI eller referensimplementationen Weld.

Objektgrafen i CDI definieras via annotationer i koden som gör att applikationsserverns container hittar rätt klass att skicka in i runtime, via reflection. Det finns en stor mängd annotationer, för exempelvis livscykelhantering, interceptors, decorators, events och type safety. Även XML-baserat stöd för objektgrafen finns, men det är inte lika vanligt och inte det rekommenderade användningssättet.

Injection-punkter markeras med @Inject, och när CDI:s container ska tillgodose en sådan så tittar den på det interface som angetts och undersöker om det finns någon lämplig klass som implementerar interface:et. Finns det flera kommer man få ett fel, så isåfall gäller det att genom annotationerna peka ut vilken som ska prioriteras. Detta kan göras exempelvis med @Default och @Alternative. Ska en implementation annoterad @Alternative användas istället kan detta deklareras i filen beans.xml. Denna fil är obligatorisk i CDI-sammanhang men kan vara tom. Genom att byta ut denna fil kan man peka ut alternativa klasser i olika miljöer.

Man kan ställa in scope för varje klass, exempelvis globalt eller enbart nuvarande request:en. Även livscykelhantering stöds genom att man på olika sätt anger callback-metoder i de inject:ade klasserna, exempelvis @PostCreate som anropas efter att klassen i övrigt är färdiginstansierad men ännu inte inject:ad någon annanstans.

Det går att explicit begära en viss klass genom att ange egna annotationer som innehåller @Qualifier, vid injection-punkten. Dessa kan detaljstyras, exempelvis med egna fält eller enums i annotationen.

För att integrationstesta i CDI kan ramverket Arquillian användas. Med Arquillian:s egna test runner kan integrationstester köras i enhetstestformat, utan att en tung CDI container behöver startas upp.

Spring

Spring släpptes 2002 och är ett ramverk för enterprise-applikationer bestående av en samling moduler, bl.a. dependency injection. Objektgrafen i Spring DI definieras antingen explicit i XML eller implicit via annoteringar i koden som gör att ramverket hittar rätt implementation att skicka in i runtime, via reflection. I början fanns det enbart XML-baserad deklaration men numer finns bra stöd för annotationsbaserad. Spring implementerar JSR-330.

En XML-baserad objektgraf anges i en separat XML-fil och beskriver vilka beroenden en klass ska få in samt hur. Även beroendena i sig anges, för att Spring ska hitta dem. En annotationsbaserad objektgraf anges genom annotationer i koden som Spring sedan analyserar. Detta sker när man genererar klassen genom att använda Spring:s BeanFactory-klass.

Spring-ramverket uppmuntrar till en arkitektur bestående av service-, repository- och controller-lager, för att få en tydligare struktur. I ramverkets dependency injection reflekteras detta med de vanliga annotationerna @Autowired, @Component, @Service, @Repository och @Controller. Dessa anger komponenttyp i Spring:s arkitektur. @Autowired anger en injection-punkt, alltså där Spring förväntas skicka in ett beroende. I klasser annoterade med @Configuration kan man manuellt konfigurera objektgrafen.

Likt CDI stöds olika scopes och livscykelhantering, exempelvis att instansen ska existera globalt eller enbart inom nuvarande request:en. Livscykelhantering stöds genom callback-metoder i de inject:ade klasserna, som anges via antingen annotationer eller XML.

Flera klasser med samma interface kan särskiljas genom annotationen @Qualifier. Det går också att specificera olika klasser för olika miljöer, exempelvis om man vill ha en viss databasintegration i testmiljön och en annan i produktionsmiljön, via annotationen @Profile.

Spring har inbyggt stöd för integrationstestning i enhetstestformat, via en egen test runner, där beroenden tillgodoses utan att man behöver starta igång en server med hela ramverket körandes.

Guice

Guice släpptes 2007 av Google och är ett ramverk där objektgrafen deklareras i separata modulklasser. Guice genererar sedan ett Injector-objekt som används för att i sin tur generera objektet man vill ha, med beroenden tillgodosedda enligt det man tidigare deklarerat i modulklasserna. Då Guice är reflection-baserat är det mindre lämpligt för Android. Guice implementerar JSR-330.

Guice stödjer olika scopes men har inte inbyggt stöd för livscykelhantering likt CDI och Spring. Det behövs inte lika mycket annotationer som i Spring, då mappningen istället sker i modulklasserna. Annotationer och konfigureringar utanför modulklasserna används mer i undantagsfall. Injection-punkter anges med @Inject.

Klasser med samma interface kan särskiljas genom antingen annotationen @Named- eller custom-annotationer med @Qualifier. Vill man använda olika klasser i olika miljöer finns möjligheten att skriva en egen @Provider-klass, där man kan välja hur objektet ska instansieras och då göra det utefter miljöparametrar.

För att testa med Guice behövs en separat modulklass där man mappar mock-objektet som ska användas.

Det går att expandera Guice dependency injection med ny funktionalitet via egna plugins, då den interna injection-funktionaliteten är exponerad.

Dagger

Dagger släpptes ursprungligen av betalningsföretaget Square under 2012. Numer är Google med och vidareutvecklar den nya versionen Dagger2. Det fungerar annorlunda än de tidigare nämnda ramverken, då det inte behöver köras i runtime. Hela objektgrafen genereras till Java-kod genom annotationsprocessering i compile time. Genom att undvika att använda reflection i runtime är ramverket lämpligt för Android då det inte blir någon prestandaförlust. Dagger2 implementerar JSR-330.

Objektgrafens grundstruktur deklareras i komponenter i form av interface (annoterat med @Component) som anger vilka beroenden de tillgodoser. Därefter har varje komponent en eller flera modulklasser (annoterade med @Module) där själva objekten skapas. Dagger använder komponenterna för att generera den grundläggande objektgrafen. Modulerna instansierar man sedan själv på lämpligt ställe och skickar in i komponenten, varefter dess objekt genereras. Det är stället som modulen skapas på som bestämmer vilket scope modulens olika objekt får. Varje gång modulen skapas på nytt kommer man få en ny uppsättning ingående objekt.

Klasser med samma interface kan särskiljas genom antingen @Named- eller custom-annotationer med @Qualifier. För att använda olika klasser i olika miljöer kan man låta ens komponent-interface ärva från ett annat komponent-interface, där det senare varierar beroende på vilken miljö man är i, något som man inom t.ex. Android kan konfigurera i Gradle.

Dagger2 kan ses som hårt typat eftersom annotationsprocessorn körs i compile time, vilket minskar risken för fel.

Andra ramverk

De beskrivna ramverken är några av de vanligaste inom Java-utveckling. Det finns även en del mindre vanliga ramverk, exempelvis:

  • PicoContainer
  • Feather
  • Transfuse (Android)
  • Silk DI
  • Apache Commons Inject

/ Jonas Heder, Java- och Android-utvecklare på Dynabyte

#Android #CDI #Dagger2 #Dependency-injection #Guice #Java #Spring

Relaterade inlägg

Controller 2017

DevSum17