Spring som DI-ramverk

Detta inlägg ingår i serien Spring från början och kommer att förklara hur Spring kan användas som DI-ramverk.

Spring som DI-ramverk

Spring är ett Dependency Injection-ramverk (se tidigare artikel gällande Dependency Injection). Det är också så många andra saker, men i sin enklaste form är Spring ändå i första hand ett DI-ramverk.

Om du designar din applikation med hjälp av Dependency Injection kan du konfigurera Spring att hantera hur varje enskild komponent ska skapas, konfigureras och bindas ihop med övriga komponenter. En ”komponent” i Spring är en vanlig POJO, komponenter som hanteras av Spring kallas för ”Spring Beans” (eller på svenska ”bönor”) även om de inte nödvändigtvis måste uppfylla JavaBeans-specifikationen.

Det centrala objektet i Springs DI-hantering är en BeanFactory. En BeanFactory ansvarar bland annat för att skapa bönor, livscykelhantering och att binda ihop bönorna enligt den konfiguration du tillhandahåller. Det vanligaste sättet att konfigurera dina bönor är att använda XML-konfiguration men det finns även andra konfigurationssätt exempelvis via property-filer eller via annotations. En BeanFactory kan skapas manuellt, det vanligaste är dock att man skapar en BeanFactory via någon av det hjälpklasser som tillhandahålls av Spring exempelvis genom användning av ContextServletListener för webbapplikationer. En specialiserad form av BeanFactory är ett ApplicationContext som är det man typiskt använder, förklaringen till detta hittar du här.

Låt oss se på ett enkelt exempel. Anta att vi ska skriva en applikation som hämtar reklamtexter (strängar) från någon typ av datakälla, för att sedan publicera dem på en ljusskylt. Vi bortser från allt det riktigt komplicerade, som att kommunicera med hårdvaran och sånt, och tittar på det förenklade fallet. Först har vi en datakälla med vissa egenskaper, den fungerar ungefär som en Iterator<String>, så vi modellerar gränssnittet på liknande sätt:

public interface AdTextDataSource {
    boolean hasNext();
    String next();
}

Vi vet inte så mycket mer om datakällan för närvarande än att den kommer att klara av att uppfylla gränssnittet ovan. Dags att titta på konsumenten, dvs den komponent som ska hämta textsträngarna och visa upp dem på enklaste sätt:

public class AdTextConsumer {
    private AdTextDataSource dataSource;

    public void setDataSource(AdTextDataSource ds) {
         this.dataSource = ds;
    }

    public AdTextDataSource getDataSource() {
        return this.dataSource;
    }

    public void loopUntilDone() {
        while (getDataSource().hasNext()) {
            System.out.println(getDataSource().next());
        }
    }
}

Så, var kommer textsträngarna ifrån? Ja det vet vi ju inte riktigt ännu — det kan vara från en textfil på det lokala filsystemet, en SQL-databas, en extern webbsida, ett pollande webservice-anrop eller nåt helt annat. Men för att testa vår kod bygger vi en liten test-datakälla som lagrar strängarna i en vanlig lista i minnet.

public class InMemoryAdTextDataSource implements AdTextDataSource {
    private List<String> texts = new ArrayList<String>();
    private int index;
    public List<String> getTexts() {
        return this.texts;
    }

    public void setTexts(List<String> texts) {
        this.texts = texts;
    }

    public String next() {
        return getTexts().get(index++);
    }

    public boolean hasNext() {
        return index < getTexts().size();
    }
}

Ovan ser vi att AdTextConsumer inte har något beroende till InMemoryAdTextDataSource utan bara till dess interface. Vi ser också att AdTextConsumer inte instansierar eller slår upp InMemoryAdTextDataSource, det ska vi låta Spring göra. Nedan följer ett ett exempel på hur dessa klasser kan bindas ihop med Spring. Vi skapar en fil som vi kallar för applicationContext.xml med följande innehåll.

<?xml version="1.0" encoding="UTF-8"?>
<beans
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://www.springframework.org/schema/beans"
  xmlns:util="http://www.springframework.org/schema/util"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    http://www.springframework.org/schema/util
    http://www.springframework.org/schema/util/spring-util-2.5.xsd">

    <bean id="dataSource" class="InMemoryAdTextDataSource">
	<property name="texts" ref="texts"/>
    </bean>

    <bean id="consumer"
            class="AdTextConsumer"
            init-method="loopUntilDone">

        <property name="dataSource" ref="dataSource"/>
    </bean>

    <util:list id="texts">
	<value>text1</value>
	<value>text2</value>
	<value>text3</value>
    </util:list>
</beans>

Vi har nu en färdig XML-konfiguration för Spring. Den första bönan vi definierar är dataSource, när vi anger class berättar vi för Spring vilken klass som ska instansieras.

Genom att ange en property (med namnet texts) låter vi Spring köra setTexts-metoden på bönan dataSource, vi har här vårt första exempel på DI.

Bönan texts utgör här initialiseringsdata till dataSource. Hade vi haft vår initialiseringsdata i en propertyfil hade vi exempel kunnat använda Springs PropertiesFactoryBean eller liknande. Bönan texts är lite speciell, Spring skapar ett vanligt List-objekt med värdena vi anger. Vi skulle kunna göra det på annat sätt men Spring har ett util-paket som låter oss skapa bönan på detta kortare sätt.

Nästa böna är consumer som ska kopplas ihop med bönan dataSource. Med init-method har vi infört livscykelshantering. Spring kommer automatiskt att köra metoden loopUntilDone så snart bönan är skapad och alla properties är satta. Motsatsen till init-method kan anges med destroy-method. Livscykelhantering kan man även uppnå genom att implementera interfacen InitializingBean och DisposableBean.

För att få igång ett program behöver vi en main-metod som sparkar igång Spring, det kan göras som följande

public static void main(String[] args) {
    ApplicationContext ctx =
        new ClassPathXmlApplicationContext("applicationContext.xml");
}

Vi har nu designat våra klasser för DI, inga förekomster av new eller fabriker. Vi har låtit Spring ta hand om skapande av objekt, livscykelhantering och initialiseringsdata. I och med att vi inte har några beroenden till Spring, någon speciell fabrik eller implementationsklass för AdTextDataSource är det enkelt att skapa ett unit-test för att testa vår consumer.

public void testSimple() {
    String[] texts = {"text1", "text2", "text3"};
    AdTextDataSource dataSource = new InMemoryAdTextDataSource();
    dataSource.setTexts(Arrays.asList(texts)); //DI

    AdTextConsumer consumer = new AdTextConsumer();
    consumer.setDataSource(dataSource); // DI
    consumer.loopUntilDone(); // Egentligen hanterat via "init-method"
    assertValidOutput(); // assert på output (inte relevant)...
}

(Ok, jag fuskade lite på slutet och antog att det fanns en assertValidOutput() som kollade att det som skrevs ut stämde — det är inte relevant för artikeln!)

Här kan man säga att vi manuellt binder ihop dataSource och consumer genom metoden setDataSource. Om man vill testa genom att använda Spring istället så kan man starta ett context via ett JUnit-test genom att ärva någon av subklasserna till AbstractSpringContextTests där metoden getConfigLocations kan överlagras och ex returnera en referens till ovan nämnda contextfil (applicationContext.xml).

Av: Robert Burén, Johan Risén, Tommy Wassgren