Spring - IOC Kontajner
Spring je veľmi rozšírený Java framework, ktorý obsahuje niekoľko rôznych projektov. Dalo by sa povedať, že všetky projekty spája jeden spoločný kontajner.
Inversion of Control
Spring kontajner využíva návrhový vzor Inversion of Control (IOC). Ten uvoľňuje pevné väzby medzi objektmi. Pevná väzba znamená, že trieda si sama inicializuje svoje vlastnosti (inej triedy, s ktorými má vzťah) a nedostane je z vonku.
Pevným väzbám sa snažíme vyhýbať!
public interface CarDao {} public class CarDaoImpl implements CarDao {} // zde mezi CarDao a CarService je pevná vazba public class CarServiceImpl { private CarDao carDao = new CarDaoImpl(); }
Prečo? Vaša architektúra je príliš úzko zviazaná (podobne ako u dedičnosti) a málo flexibilné. Zmena kódu môže byť veľmi obtiažna. Trieda bez pevných väzieb sa lepšie testuje.
Ako? Princíp IOC presúva zodpovednosť za vznik väzieb na niekoho iného. V našom prípade je presunutý z programátora na framework.
Dependency injection (DI)
Tento návrhový vzor súvisí priamo s IOC. Ide o mechanizmus, kedy je do našej triedy vložená (injektovaná) inštancie inej triedy. O túto injekciu sa stará sám Framework podľa konfigurácie. Pre prehľadnosť a väčšiu flexibilitu je dobré mať oddelenú konfiguráciu od implementácie. Existujú tri typy injekcie:
Property Inject
Framework si sám nájde danú property pomocou reflexie.
@Autowired
private CarDao carDao;
Constructor Inject
Pri vytváraní pošle cez konštruktor inštancie potrebných component.
private CarDao carDao; @Autowired public CarServiceImpl(CarDao carDao) { this.carDao = carDao; }
Setter Inject
Pri vytváraní je nahraná inštancie pomocou setter-u.
private CarDao carDao; @Autowired public void setCarDao(CarDao carDao) { this.carDao = carDao; }
Spring
Keď už poznáme základné pojmy, poďme sa pozrieť, ako ich využíva Spring. Tu je nutné poznať dva pojmy:
Bean
Objekt, ktorý vykonáva nejakú funkčnosť (napr. Pridáva dáta do databázy, vyhľadáva ...). Bean žijú v kontajneri po celý beh aplikácie. Možno s ním pracovať v celej aplikácii. Existujú dva typy bean (scope).
- Singleton objekt je v celej aplikácii len raz. Zakaždým, keď si povieme o daný objekt aplikačnému kontextu, dostaneme rovnakú inštanciu.
- Prototype je podobný ako Singleton. Rozdiel je v tom, že ak si povieme o daný objekt aplikačnému kontextu, dostaneme vždy novú inštanciu.
Kontajner
V kontajneri, čiže aplikačným kontextu, žijú objekty (BEAN), ktoré tvoria funkčné jadro vašej aplikácie. Kontajner sa zavádza pri štarte aplikácie a reprezentuje ho trieda ApplicationContext. V celej aplikácii je len jeden a dá sa injektovať odkiaľkoľvek.
Konfigurácia aplikačného kontextu
Konfigurovať kontext môžeme pomocou XML súboru alebo pomocou Java class. Obe konfigurácie si funkčne zodpovedajú. Preferovaná cesta je Java class.
Java konfigurácia
Je realizovaná obyčajnú Java triedou, v ktorej sú použité anotácie pre tvorbu aplikačného kontextu. Je dobré, si pre konfiguračný triedy urobiť špeciálny package (configuration).
- @Configuration vytvorí z danej triedy konfiguračný triedu
- @Import spojí dve konfigurácie (importuje inú konfiguráciu)
- @Bean vytvorí BEAN; typ je návratová hodnota a názov je názov metódy (ak sa nepoužije name). Možno aj zmeniť defaultnú Singleton scope (scope = DefaultScopes.PROTOTYPE)
- @Autowired injektuje inštanciu iné Bean
- @ComponentScan - preskenuje zadanej package. Ak narazí na špeciálne anotácie (@Controller: prezentačná vrstva; @Service: aplikačná vrstva; @Repository: dátová vrstva) vytvorí z daných tried Beana. Užitočná vec pre rýchlu tvorbu bean.
// jedná se o konfiguraci @Configuration // naimportuje konfiguraci z třídy StorageConfig @Import({StorageConfig.class}) // skenuje cz.itnetwork a tvoří beany (@Component, @Service...) @ComponentScan("cz.itnetwork") public class ContextConfig { // vytvoří beanu typu CarDao a názvem carRepository @Bean(name="carRepository") public CarDao carDao() { return new CarDaoImpl(); } // vytvoří beanu CarService a injektuje ji CarDao (CarRepository) @Bean @Autowired public CarService carService(CarDao carDao) { return new CarServiceImpl(carDao); } }
XML konfigurácie
Je reprezentovaná XML súborom. Konfigurácia sa musí nachádzať v Resources a byť na CLASSPATH.
- <Bean id = "..." class = "..."> vytvorí Bean
- <Import resource = "..." /> import iné konfigurácie
- <Context: component-scan base-package = "..." /> skenovanie package
Použitia kontajnera
Pre prácu s aplikačným kontextom slúži Beana ApplicationContext. Táto bean má metódu getBean (), pomocou ktorej získate akúkoľvek Beana z kontajnera.
@Configuration public class ContextConfig { @Bean public NameStrategy nameStrategy() { return new NameStrategyImpl(); } ... } public class UpdateFactoryImpl implements UpdateFactory { // zisk pristupu ke kontejneru @Autowired private ApplicationContext applicationContext; public Strategy getStrategy(Change change) { if (change.isChangeName()) { // vytažení beany NameStrategy return applicationContext.getBean(NameStrategy.class); } ... return null; } }
Príklad je výťažok kódu, kde je využitý návrhový vzor Factory. Podľa zmeny (change) sa rozhoduje ktorou stratégiu má factory vytvoriť (vytiahnuť z aplikačného kontextu). V našom prípade sa jedná o NameStrategy.
Testovanie s mockito
Ak nepoznáte mockito pozrite sa na tento článok. Z článku sa dozviete, že môžete injektované Bean namockovat (@Mock) a sledovať, či boli v teste použité.
Je dobré otestovať tiež, že sa vám správne zostaví aplikačný kontext (inicializuje sa kontajner). Tu je možné využiť metóda ApplicationContext.getBean ();
Rady
Je čitateľnejší a flexibilnejšie pokiaľ oddelíte konfiguráciu aplikačného kontexte od implementácie jednotlivých tried.
Výrazná výhoda je v tom, že ak budete chcieť vymeniť framework (napr. Spring za java EE), nie je to tak bolestivé. Stačí zahodiť starú konfiguráciu a vytvoriť novú.
class CarDaoImpl implement CarDao {} public class CarServiceImpl implements CarService { private CarDao carDao; CarServiceImpl(CarDao carDao) { this.carDao = carDao; } } @Configuration public class ContextConfiguration { @Bean public CarDao carDao() { return new CarDaoImpl(); } @Bean @Autowired public CarService carService(CarDao carDao) { return new CarServiceImpl(carDao); } }
Ako je zrejmé z príkladu, trieda CarServiceImpl využíva CarDao. Neobsahuje však žiadnu konfiguráciu, žiadne anotácie @Autowired) a ani pevnú väzbu.
Konfigurácia je vykonávaná v konfiguračnej triede (ContextConfiguration), kedy pri vytváraní Bean carService sa injektuje CarDao.
S constructor Inject zaobchádzajte opatrne, môžete sa dostať do problémov s cyklickými závislosťami medzi Beana.