Spring - IoC Kontejner
Spring je velmi rozšířený Java framework, který obsahuje několik různých projektů. Dalo by se říci, že všechny projekty spojuje jeden společný kontejner.
Inversion of Control
Spring kontejner využívá návrhový vzor Inversion of Control (IoC). Ten uvolňuje pevné vazby mezi objekty. Pevná vazba znamená, že třída si sama inicializuje své vlastnosti (jiné třídy, se kterými má vztah) a nedostane je z venčí.
Pevným vazbám se snažíme vyhýbat!
public interface CarDao {} public class CarDaoImpl implements CarDao {} // zde mezi CarDao a CarService je pevná vazba public class CarServiceImpl { private CarDao carDao = new CarDaoImpl(); }
Proč? Vaše architektura je příliš úzce svázaná (podobně jako u dědičnosti) a málo flexibilní. Změna kódu může být velice obtížná. Třída bez pevných vazeb se lépe testuje.
Jak? Princip IoC přesouvá odpovědnost za vznik vazeb na někoho jiného. V našem případě je přesunut z programátora na framework.
Dependency injection (DI)
Tento návrhový vzor souvisí přímo s IoC. Jedná se o mechanismus, kdy je do naší třídy vložena (injektována) instance jiné třídy. O tuto injekci se stará sám Framework podle konfigurace. Pro přehlednost a větší flexibilitu je dobré mít oddělenu konfiguraci od implementace. Existují tři typy injekce:
Property inject
Framework si sám najde danou property pomocí reflexe.
@Autowired
private CarDao carDao;
Constructor inject
Při vytváření pošle přes konstruktor instance potřebných component.
private CarDao carDao; @Autowired public CarServiceImpl(CarDao carDao) { this.carDao = carDao; }
Setter inject
Při vytváření je nahrána instance pomocí setter-u.
private CarDao carDao; @Autowired public void setCarDao(CarDao carDao) { this.carDao = carDao; }
Spring
Když již známe základní pojmy, pojďme se podívat, jak je využívá Spring. Zde je nutné znát dva pojmy:
Bean
Objekt, který vykonává nějakou funkčnost (např. přidává data do databáze, vyhledává...). Beany žijí v kontejneru po celý běh aplikace. Lze s ním pracovat v celé aplikaci. Existují dva typy bean (scope).
- Singleton objekt je v celé aplikaci jen jednou. Pokaždé, pokud si řekneme o daný objekt aplikačnímu kontextu, dostaneme stejnou instanci.
- Prototype je podobný jako singleton. Rozdíl je v tom, že pokud si řekneme o daný objekt aplikačnímu kontextu, dostaneme vždy novou instanci.
Kontejner
V kontejneru, neboli aplikačním kontextu, žijí objekty (BEAN), které tvoří funkční jádro vaší aplikace. Kontejner se zavádí při startu aplikace a reprezentuje ho třída ApplicationContext. V celé aplikaci je jen jeden a dá se injektovat odkudkoli.
Konfigurace aplikačního kontextu
Konfigurovat kontext můžeme pomocí XML souboru nebo pomocí Java class. Obě konfigurace si funkčně odpovídají. Preferovaná cesta je Java class.
Java konfigurace
Je realizována obyčejnou Java třídou, ve které jsou použity anotace pro tvorbu aplikačního kontextu. Je dobré, si pro konfigurační třídy udělat speciální package (configuration).
- @Configuration vytvoří z dané třídy konfigurační třídu
- @Import spojí dvě konfigurace (naimportuje jinou konfiguraci)
- @Bean vytvoří beanu; typ je návratová hodnota a název je název metody (pokud se nepoužije name). Lze i změnit defaultní singleton scope (scope=DefaultScopes.PROTOTYPE)
- @Autowired injektuje instanci jiné beany
- @ComponentScan - proskenuje zadané package. Pokud narazí na speciální anotace (@Controller: prezentační vrstva; @Service: aplikační vrstva; @Repository: datová vrstva) vytvoří z daných tříd beany. Užitečná věc pro rychlou 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 konfigurace
Je reprezentována XML souborem. Konfigurace se musí nacházet v Resources a být na classpath.
- <bean id="..." class="..."> vytvoří beanu
- <import resource="..."/> import jiné konfigurace
- <context:component-scan base-package="..." /> skenování package
Použití kontejneru
Pro práci s aplikačním kontextem slouží beana ApplicationContext. Tato bean má metodu getBean(), pomocí níž získáte jakoukoli beanu z kontejneru.
@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; } }
Příklad je výtažek kódu, kde je využit návrhový vzor Factory. Podle změny (change) se rozhoduje kterou strategii má factory vytvořit (vytáhnout z aplikačního kontextu). V našem případě se jedná o NameStrategy.
Testování s mockito
Pokud neznáte mockito podívejte se na tento článek. Z článku se dovíte, že můžete injektované beany namockovat (@Mock) a sledovat, jestli byly v testu použity.
Je dobré otestovat také, že se vám správně sestaví aplikační kontext (inicializuje se kontejner). Zde lze využít metoda ApplicationContext.getBean();
Rady
Je čitelnější a flexibilnější pokud oddělíte konfiguraci aplikačního kontextu od implementace jednotlivých tříd.
Výrazná výhoda je v tom, že pokud budete chtít vyměnit framework (např. Spring za java EE), není to tak bolestivé. Stačí zahodit starou konfiguraci a vytvořit novou.
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); } }
Jak je patrné z příkladu, třída CarServiceImpl využívá CarDao. Neobsahuje ovšem žádnou konfiguraci, žádné anotace @Autowired) a ani pevnou vazbu.
Konfigurace je prováděna v konfigurační třídě (ContextConfiguration), kdy při vytváření beany carService se injektuje CarDao.
S constructor inject zacházejte opatrně, můžete se dostat do problémů s cyklickými závislostmi mezi beanami.