Riippuvuuksien injektointi Spring-sovelluskehyksessä

Jatketaan viime viikolla käsittelemämme laskimen tarkastelua. Kertaa tarvittaessa yllä olevan linkin takana oleva teksti.

Alla oleva koodi löytyy gradle-muotoisina projekteina kurssin tehtävärepositoriosta hakemistosta koodi/viikko2/RiippuvuuksienInjektointiSpring

Päädyimme siis tilanteeseen, missä Laskin-luokasta on erotettu konkreettinen riippuvuus syötteen lukemiseen ja tulostamiseen. Laskin tuntee ainoastaan rajapinnan IO jonka kautta se hoitaa syötteen käsittelyn ja tulostamisen.

Ennen käynnistämistä refaktoroitu laskin pitää konfiguroida injektoimalla sille sopivat riippuvuudet:

// konfigurointivaihe
Laskin laskin = new Laskin( new KonsoliIO() );

// ja laskin valmiina käyttöön
laskin.suorita();

Esimerkkimme tapauksessa konfigurointi on helppoa. Isommissa ohjelmissa konfiguroitavalla oliolla voi olla suuri määrä riippuvuuksia ja konfigurointi voi olla monimutkaista.

Kurssilta Web-palvelinohjelmointi osalle tuttu Spring-sovelluskehys tarjoaa mekanismin, jonka avulla riippuvuuksien injektointi voidaan siirtää Springin vastuulle.

Spring on laaja ja monikäyttöinen sovelluskehys, jota käytetään yleisesti mm. Javalla tapahtuvassa web-sovelluskehityksessä. Tutustumme kurssilla oikeastaan ainoastaan Springin riippuvuuksien injektointiin.

Spring saadaan käyttöön lisäämällä se riippuvuudeksi gradle-projektin build.gradle-tiedostoon, katso tarkemmin täältä.

Springissä ideana on siirtää osa sovelluksen olioista ns. Inversion of Control container:in eli eräänlaisen oliosäiliönä toimivan sovelluskontekstin hallinnoitavaksi.

Springissä on kaksi tapaa määritellä sovelluskontekstin kontrolloimat oliot. Käytämme nykyään suositumpaa annotaatioihin perustuvaa määrittelytapaa.

Sovelluskontekstin huolehdittavaksi annettavien olioiden luokat merkitään annotaatiolla @Component. Luokka KonsoliIO annotoidaan seuraavasti:

@Component
public class KonsoliIO implements IO {
  // ...
}

Myös luokka Laskin merkataan annotaatiolla Component. Tämän lisäksi Springille kerrotaan annotaatiolla @Autowired, että sen täytyy injektoida laskimelle rajapinnan IO toteuttava olio konstruktoriparametrina:

@Component
public class Laskin {
    private IO io;

    @Autowired
    public Laskin(IO io) {
        this.io = io;
    }

  //...

}

Itseasiassa samaan lopputulokseen päästäisiin vieläkin helpommin merkitsemällä laskimen oliomuuttuja io annotaatiolla @Autowired, tällöin konstruktoria ei tarvita:

@Component
public class Laskin {
    @Autowired
    private IO io;
    
    // ...      
   
}

Sovellus on vielä konfiguroitava, jotta Spring tietää, että käytössä on annotaatioihin perustuva määrittely. Konfigurointi voidaan tehdä joko xml-tiedostona tai Java-luokkana.

Käytetään luokkapohjaista tapaa. Konfiguroinnin hoitava luokka on yksinkertainen:

@Configuration
@ComponentScan(basePackages = "ohtu.laskin")
public class AppConfig  {}

Käytännössä luokka siis konfiguroi, että käytetään annotaatioihin perustuvaa määrittelyä, ja etsitään annotoituja luokkia pakkauksen ohtu.laskin alta.

Pääohjelma on nyt seuraava:

public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);

        Laskin laskin = ctx.getBean(Laskin.class)
        laskin.suorita();
    }
}

Ensimmäinen rivi luo sovelluskontekstin. Tämän jälkeen kontekstilta pyydetään Laskin-olio ja kutsutaan laskimen käynnistävää metodia.

Spring siis luo konfiguraatioiden ansiosta automaattisesti laskimen ja injektoi sille KonsoliIO-olion.

Oletusarvoisesti Spring luo ainoastaan yhden olion kustakin luokasta, eli jos metodikutsu ctx.getBean(Laskin.class) suoritetaan toistuvasti, se palauttaa aina saman olion.

Jos tämä ei ole haluttu toimintatapa, voidaan Spring konfiguroida palauttamaan jokaisella kutsulla uusi olio.

Lisää tietoa Springin kontainereiden toiminnasta löytyy dokumentaatiosta.