Na samom početku jedna mala napomena: izvorni kod za primjere opisane u ovom tekstu se može pregledati i preuzeti sa ove lokacije. U ovom članku će biti prikazani i analizirani samo oni fragmenti koda koji su neophodni za opisivanje i razumjevanje specifičnih Spring koncepata.

Primjer kojeg ćemo koristiti opisuje jedan jednostavan domen koji se sastoji od čuvanja detalja o filmovima i glumcima. Aplikacija se sastoji od tri sloja: domenski sloj, sloj repozitorija i servisni sloj.

 

 

Domenski objekti Movie i Actor predstavljaju apstrakcije filmova i glumaca. Interfejsi MovieRepository i ActorRepository definišu CRUD operacije nad domenskim objektima, a njihove InMemory implementacije omogućavaju spašavanje i čitanje domenskih objekata iz memorije. CinemaService interfejs i njegova implementacija omogućavaju izvršavanje aplikacione logike, korištenjem navedenih repozitorija. Tipična java aplikacija treba da, u okviru setup koda, instancira navedene klase i izvrši njihovo povezivanje prije izvršavanja bilo kakve poslovne logike. Npr. slijedeći primjer pokazuje jedan od mogućih načina za definisanje aplikacione logike koja popunjava i ispisuje bazu filmova i glumaca:

public class App {
    public static void main(String[] args) {
        ActorRepository actorRepository = new InMemoryActorRepository();
        MovieRepository movieRepository = new InMemoryMovieRepository();
        CinemaService cinemaService = new CinemaServiceImpl(movieRepository, actorRepository);
        populateDatabase(cinemaService);
        printDatabase(cinemaService);
    }
    private static void populateDatabase(CinemaService cinemaService) {
        Actor actor1 = new Actor(1, "Brad", "Pit");
        Actor actor2 = new Actor(2, "Edward", "Norton");
        Movie movie1 = new Movie(1, "Zodiac", 157, new HashSet>());
        Movie movie2 = new Movie(2, "Fight Club", 139,  new HashSet>());
        movie1.getActors().add(actor1);
        movie2.getActors().add(actor1);
        movie2.getActors().add(actor2);
        cinemaService.saveMovie(movie1);
        cinemaService.saveMovie(movie2);
        cinemaService.saveActor(actor1);
        cinemaService.saveActor(actor2);
    }
    private static void printDatabase(CinemaService cinemaService) {
        cinemaService.findAllMovies().forEach(System.out::println);
    }
}


Spring Framework se, sa druge strane, oslanja na izuzetno moćan koncept koji se naziva inverzija kontrole ili Dependency Injection (DI). U ovom pristupu developer ne mora pisati kod za instanciranje i povezivanje jer je to zadatak Spring kontejnera. Ono što developer mora obezbjediti jeste postojanje "recepta" po kojem će kontejner kreirati navedene objekte. Tradicionalni način za definirianje ovog recepta je bila odgovarajuća xml struktura. Npr. te recepte možemo kreirati unutar application-config.xml datoteke:

Svaki recept za kreiranje java objekta mora biti sadržan unutar taga bean. Npr:

Ova definicija znači da će Spring kontejner kreirati actorRepository bean instanciranjem klase InMemoryActorRepository. Po defaultu, navedeni bean je singleton što znači da će kontejner svim svojim klijentima vraćati uvijek jedan te isti objekat. Ukoliko želimo da kontejner uvijek kreira novi objekat, trebamo promijeniti recept tako da scope beana definišemo kao prototype:

Ukoliko bean treba da bude kreiran uz pomoć drugih beana, recept treba da sadrži reference tih objekata:

Prikazani primjer predstavlja instrukciju kontejneru da kreira cinemaService objekat koristeći konstruktor u kojeg će biti ubačene dvije reference, pri čemu če kontejner preuzeti brigu o redoslijedu kreiranja objekata.

Recept za kreiranje objekta može definisati i korake koji se dešavaju nakon kreiranja instance. Tako npr. možemo kreirati instrukciju kojom će kontejner nakon kreiranja instance postaviti atribute objekta: 

Kontejner će kreirati objekat actor1 instanciranjem klase Actor (pozivom default konstruktora), nakon čega će postavljati atribute objekta pozivom odgovarajućih set metoda (npr. setFirstName). Treba napomenuti da recept može definisati upotrebu konstruktora i postavku atributa zajedno.

Dakle, xml datoteka definiše objekte koje će kontejner kreirati, odnosno kontejnersku konfiguraciju. Kreiranje samog kontejnera se oslanja na instanciranje neke od implementacija ApplicationContext interfejsa. Ukoliko se naša xml datoteka nalazi na classpath-u aplikacije, možemo koristiti ClassPathXmlApplicationContext klasu:

ApplicationContext ctx = new ClassPathXmlApplicationContext("application-config.xml");

 

Ovim iskazom se kreira kontejner a istovremeno i svi singleton objekti u kontejneru. Nakon ovoga moguće je dohvatiti kreirane objekte, ili preko njihovog tipa ili preko njihovog imena:

CinemaService cinemaService = ctx.getBean(CinemaService.class);

 

Metod getBean() će potražiti objekat tipa CinemaService u kontejneru i vratiti ga pozivaocu. U slučaju da navedeni bean nije singleton (prototype), u ovom koraku će taj objekat biti kreiran.

Da bi sve ovo radilo, aplikacija mora importovati odgovarajuće Spring biblioteke, u ovom slučaju spring-context biblioteku. Navedenu biblioteku možemo ručno ubaciti u projekat, ali preporučeni pristup je korištenje nekog od build alata koji omogućavaju dependency management, npr. maven čija konfiguracija (pom.xml) treba da definiše odgovarajući dependency: 

 

Struktura upravo opisanog projekta izgleda ovako:

 

Naravno, konfiguracija kontejnera ne mora biti zapisana u xml dokumentu. Veliki broj developera ne želi da oslanja na nezgrapnu xml strukturu te konfiguriše Spring kontejner unutar java klase. Pretpostavimo da se ta klasa naziva Config. Za kreiranje kontejnera sada moramo koristiti drugačiju ApplicationContext implementaciju:

ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);

 

Klasa Config predstavlja običnu java klasu obogaćenu određenim anotacijama:

@Configuration

public class Config {

   ...

}

 

Anotacija @Configuration znači da se ova klasa koristi za konfiguraciju kontejnera. Ova klasa će biti instancirana odmah nakon kreiranja ApplicationContext instance. Unutar Config klase možemo definisati bean objekat na slijedeći način: 

@Bean
public Actor actor1(){
    return new Actor(1, "Brad", "Pitt");
}


@Bean(name="actor2")
public Actor actor2(){
    return new Actor(1, "Edward", "Norton");
}

 

Kao što vidimo, kreirali smo metode koje kreiraju Actor instance i koje su označene @Bean anotacijom. Spring će automatski izvršiti ove metode i njihove rezultate spremiti u kontejner, kao bean objekte sa nazivima actor1 i actor2. Ukoliko ove beane želimo koristiti za konstrukciju drugih objekta, možemo npr. napisati slijedeći kod:

@Bean
public Movie movie1(){
    Set actors = new HashSet>();
    actors.add(actor1());
    return new Movie(1, "Zodiac", 157, actors);
}

@Bean
public Movie movie2(Set actors){
    return new Movie(2, "Fight Club", 139, actors);
}

Bean movie1 će biti kreiran korištenjem beana actor1, a bean movie2 će biti kreiran uz pomoć referenci na oba Actor objekta. Obratite pažnju da metod movie2() ima argument kolekcije Actor objekata. Kontejner će u ovu kolekciju ubaciti sve bean objekte tipa Actor, a to su upravo actor1 i actor2. Dakle, kontejner automatski poziva @Bean metode i ubacuje druge bean objekte kao argumente tih metoda. Navedeni proces se naziva autowiring.

Postoje i drugi načini da kontejner kreira bean objekte. Npr. naša konfiguracijska klasa može imati slijedeću anotaciju: 

@Configuration
@ComponentScan(basePackages = "org.infobip.spring.ann")
public class Config {
   ...
}


@ComponentScan anotacija predstavlja instrukciju kontejneru da skenira odgovarajući java paket (i njegove podpakete) te da kreira bean objekte instanciranjem kandidatskih klasa. Kandidatska klasa je svaka klasa koja je označena nekom od slijedećih anotacija: @Service, @Component, @Repository i @Controller. Razlika između svih ovih anotacija je čisto semantičke prirode, a svaka će rezultirati instanciranjem klase i registracijom objekta unutar kontejnera. Npr.

@Service
public class CinemaServiceImpl implements CinemaService{
    private MovieRepository movieRepository;
    private ActorRepository actorRepository;

    @Autowired
    public CinemaServiceImpl(MovieRepository movieRepository, ActorRepository actorRepository) {
       this.movieRepository = movieRepository;
       this.actorRepository = actorRepository;
    }
   ...
 }
 

Ukoliko kontejner skeniranjem naleti na ovu klasu, pokušat će je instancirati. Obratite pažnju na @Autowired anotaciju; ona predstavlja instrukciju kontejneru da potraži bean objekte i ubaci ih u konstruktor navedene klase. To znači da i navedene klase trebaju biti instancirane uz pomoć kontejnera, uz pomoć @Bean metoda ili skeniranja komponenti. Klasa CinemaServiceImpl može izgledati i ovako:

@Service
public class CinemaServiceImpl implements CinemaService{
    @Autowired
    private MovieRepository movieRepository;
    @Autowired
    private ActorRepository actorRepository;
     
    public CinemaServiceImpl() {}
   ...
 }


I u ovom slučaju @Autowired anotacija omogućava automatsko ubacivanje objekata iz kontejnera u instancu posmatrane klase, baš kao što bi bio slučaj i da smo istu anotaciju postavili za odgovarajuće setter metode. Naravno, u ovom slučaju nećemo koristiti kontstruktor jer je kontejner obezbjedio postojanje povezanih objekata!

Naravno, ovo nije sve što se tiče bean objekata. Spring nam omogućava da definišemo metode koje će automatski biti pozvane nakon konstrukcije objekta, ili metode koje će biti pozvane prije njegovog uništavanja. Također, bean objekte je moguće kreirati uz pomoć factory objekata umjesto instanciranja klase. Kontejner (ApplicationContext) nije zadužen samo za konstrukciju objekata već i za dohvatanje informacija o okruženju u kojem se aplikacija izvršava, registraciju i slušanje aplikacionih događaja itd. Ove mogućnosti ostavljamo čitaocu za istraživanje uz još jednu napomenu da se izvorni kod primjera navedenih u ovom članku može preuzeti sa linka na početku teksta. Do narednog čitanja!

 

Almir Pehratović, Infobip BH d.o.o.

 

1/1