Dependency Injection med CDI, Guice och Spring

Detta inlägg visar hur Dependency Injection (DI) kan användas med hjälp av CDI, Guice och Spring. Depencency Injection beskrivs bland annat i Robert Buréns artikel Spring som DI-ramverk. Exemplet i detta inlägg är kraftigt influerat av en artikel jag läste på DZone.

Spring och Guice är två av de vanligaste ramverken för DI. Dessa ramverk kan användas i standalone-, web- och enterprise-applikationer. CDI är ett standardiserat sätt för DI som kommer med Java EE 6 och kan tills vidare därför endast användas i en enterprise-miljö. Spring erbjuder dock stöd för några av de features som definieras av CDI.

Exemplet jag använder mig av (från DZone) är en uttagsautomat eller ATM – Automated Teller Machine. På denna uttagsautomat kan man göra uttag eller insättningar (withdraw, deposit). Jag har tillhandahållit en implementation av denna ATM för respektive DI-teknologi. Dessutom injiceras en implementation av AtmTransport till respektive Atm.

Denna kod finns på GitHub – https://github.com/cygni-stacktrace/di-sample.

Gränssnittet för uttagsautomaten ser ut enligt följande:

package se.cygni.stacktrace.di;
 
import java.math.BigDecimal;
 
public interface Atm {
    void deposit(BigDecimal bd);
    void withdraw(BigDecimal bd);
}

En AtmTransport används för att kommunicera med banken och definieras så här:

package se.cygni.stacktrace.di;
 
public interface AtmTransport {
    void communicateWithBank(byte[] datapacket);
}

Som jag nämnde tidigare kommer jag att tillhandahålla tre stycken implementationer av Atm och tre stycken av AtmTransport. Vi börjar med att kika på Spring.

Spring

Jag använder mig av Spring 3.1 och dess konfiguration via @Configuration. Detta kan ni läsa mer om på SpringSource-bloggen men genom detta eliminerar jag helt och hållet behovet av XML. Först sätter jag upp komponentskanning med hjälp av @ComponentScan. Detta ersätter helt XML:ens component-scan.

package se.cygni.stacktrace.di.config;
 
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import se.cygni.stacktrace.di.spring.SpringAtm;
 
/** Spring 3.1 configuration with component scanning */
@Configuration
@ComponentScan(basePackageClasses = SpringAtm.class)
public class SpringConfig {
    // No impl. needed - only annotations are used
}

Som ni ser har så sker magin med hjälp av @ComponentScan(basePackageClasses = SpringAtm.class). Detta talar om för Spring att paketet där klassen SpringAtm finns ska scannas. Det går att ersätta detta med vanliga strängvärden som innehåller paketnamn men genom att ange en klass enligt ovan så är konfigurationen typsäker och klarar av refaktoreringar.

Implementationen av uttagsautomaten ser ut så här:

package se.cygni.stacktrace.di.spring;
 
import java.math.BigDecimal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import se.cygni.stacktrace.di.Atm;
import se.cygni.stacktrace.di.AtmTransport;
 
@Component
public class SpringAtm implements Atm {
    private final AtmTransport transport;
 
    @Autowired
    public SpringAtm(final AtmTransport transport) {
        this.transport = transport;
    }
 
    private byte[] createData() {
        return "testdata".getBytes();
    }
 
    public void deposit(final BigDecimal bd) {
        System.out.println("Spring: deposit called");
        transport.communicateWithBank(createData());
    }
 
    public void withdraw(final BigDecimal bd) {
        System.out.println("Spring: withdraw called");
        transport.communicateWithBank(createData());
    }
}

Annotationen @Component används för att markera klassen som en komponent som ska skapas via ett Spring-kontext. @Autowired används för att injicera en AtmTransport via konstruktorn. Transportobjektet, som också annoteras med @Component ser ut så här:

package se.cygni.stacktrace.di.spring;
 
import org.springframework.stereotype.Component;
import se.cygni.stacktrace.di.AtmTransport;
 
@Component
public class SpringTransport implements AtmTransport {
    public void communicateWithBank(final byte[] datapacket) {
        System.out.println("Spring transport - yey!");
    }
}

Tada! Det är allt som behövs för att kunna ”bootstrappa” applikationen via ett AnnotationConfigApplicationContext enligt exemplet nedan:

final Atm springAtm = new AnnotationConfigApplicationContext(SpringConfig.class).getBean(Atm.class);
springAtm.deposit(new BigDecimal(3000));

Programmets fantastiska output blir:

Spring: deposit called
Spring transport - yey!

Guice

För att åstadkomma ovanstående via Guice kan man göra enligt följande. Först så skapas en implementation av Atm och AtmTransport med Guice-annotationer. Därefter skapas en Module som används för att konfigurera applikationen (också utan XML). Vår Module heter GuiceConfig.

Uttagsautomaten ser ut så här:

package se.cygni.stacktrace.di.guice;
 
import java.math.BigDecimal;
import se.cygni.stacktrace.di.Atm;
import se.cygni.stacktrace.di.AtmTransport;
import com.google.inject.Inject;
 
public class GuiceAtm implements Atm {
    private final AtmTransport transport;
 
    @Inject
    public GuiceAtm(final AtmTransport transport) {
        this.transport = transport;
    }
 
    private byte[] createData() {
        return "testdata".getBytes();
    }
 
    public void deposit(final BigDecimal bd) {
        System.out.println("Guice: deposit called");
        transport.communicateWithBank(createData());
    }
 
    public void withdraw(final BigDecimal bd) {
        System.out.println("Guice: withdraw called");
        transport.communicateWithBank(createData());
    }
}

@Inject-annotationen deklarerar att injicering är möjlig via konstruktorn. Observera att inga annotationer á la @Component krävs utan detta kan definieras i Module-klassen som visas senare.

Transportobjektet är helt vanlig Java utan annotationer:

package se.cygni.stacktrace.di.guice;
 
import se.cygni.stacktrace.di.AtmTransport;
 
public class GuiceTransport implements AtmTransport {
    public void communicateWithBank(final byte[] datapacket) {
        System.out.println("Guice transport - googly!");
    }
}

Guice-magin sker i Module-klassen GuiceConfig som ser ut så här:

package se.cygni.stacktrace.di.config;
 
import se.cygni.stacktrace.di.Atm;
import se.cygni.stacktrace.di.AtmTransport;
import se.cygni.stacktrace.di.guice.GuiceAtm;
import se.cygni.stacktrace.di.guice.GuiceTransport;
import com.google.inject.AbstractModule;
 
/** Simple Guice-configuration */
public class GuiceConfig extends AbstractModule {
    @Override
    protected void configure() {
        bind(Atm.class).to(GuiceAtm.class);
        bind(AtmTransport.class).to(GuiceTransport.class);
    }
}

Här binder vi exempelvis AtmTransport till att vara just av typen GuiceTransport. Modulen ärver från AbstractModule som tillhandahåller metoder som förenklar bindningen.

Uppstart av det hela sker enligt nedan. En Injector skapas som läser den Module-konfiguration vi specificerat.

final Atm guiceAtm = Guice.createInjector(new GuiceConfig()).getInstance(Atm.class);
guiceAtm.deposit(new BigDecimal(2000));

Programmets output blir då:

Guice: deposit called
Guice transport - googly!

CDI

För att köra ovanstående exempel med CDI krävs egentligen en Java EE 6-miljö. Spring erbjuder dock möjligheten att kunna köra vissa CDI-annotationer via Spring-kontextet. Detta innebär alltså att CDI-annoterad kod kan köras antingen via Spring eller via en Java EE-container. I dagsläget stöds dock inte alla CDI-annotationer (ex @Default).

Vi börjar med Atm-implementationen:

package se.cygni.stacktrace.di.cdi;
 
import java.math.BigDecimal;
import javax.inject.Inject;
import javax.inject.Named;
import se.cygni.stacktrace.di.Atm;
import se.cygni.stacktrace.di.AtmTransport;
 
@Named("Atm")
public class CdiAtm implements Atm {
    private final AtmTransport transport;
 
    @Inject
    public CdiAtm(final AtmTransport transport) {
        this.transport = transport;
    }
 
    private byte[] createData() {
        return "testdata".getBytes();
    }
 
    public void deposit(final BigDecimal bd) {
        System.out.println("CDI: deposit called");
        transport.communicateWithBank(createData());
    }
 
    public void withdraw(final BigDecimal bd) {
        System.out.println("CDI: withdraw called");
        transport.communicateWithBank(createData());
    }
}

Liksom för både Spring och Guice annoteras konstruktorn för injicering. CDI använder annotationen javax.inject.Inject för detta och Spring tolkar alltså detta ungefär på samma sätt som @Autowired. Själva klassen annoteras med @Named("Atm") och blir då en komponent i Springs kontext. Namnet på komponenten, i detta fall Atm kan sedan användas i applikationen. Om inget namn anges så blir namnet cdiAtm.

CDI-versionen av AtmTransport ser ut så här:

package se.cygni.stacktrace.di.cdi;
 
import javax.inject.Named;
import se.cygni.stacktrace.di.AtmTransport;
 
@Named
public class CdiTransport implements AtmTransport {
    public void communicateWithBank(final byte[] datapacket) {
        System.out.println("CDI transport - wow!");
    }
}

Den påminner mycket om Spring-versionen med dess @Component-annotation. Bootstrap av CDI-exemplet är i stort sett identiskt med Spring-versionens via @Configuration och @ComponentScan. Den enda egentliga skillnaden är vilket paket som ska scannas.

package se.cygni.stacktrace.di.config;
 
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import se.cygni.stacktrace.di.cdi.CdiAtm;
 
/** Spring 3.1 configuration with component scanning */
@Configuration
@ComponentScan(basePackageClasses = CdiAtm.class)
public class CdiConfig {
    // No impl. needed - only annotations are used
}

Ovanstående kan köras med hjälp av följande:

final Atm cdiAtm = new AnnotationConfigApplicationContext(CdiConfig.class).getBean(Atm.class);
cdiAtm.deposit(new BigDecimal(1000));

Output blir då:

CDI: deposit called
CDI transport - wow!

Summering

Spring, Guice och CDI påminner starkt om varandra – i alla fall för de enklaste fallen. Fördelen med CDI är att det är standardiserat och kan både köras i en Spring-container och i en Java EE 6-container. Guice är byggt för att vara ”XML-fritt” men sedan Spring 3.1 finns den möjligheten även för Spring.