Automatyczne wstrzykiwanie bean’ów w Springu
Jednym z najważniejszych modułów frameworka Spring jest jego kontener odwrócenia sterowania (ang. Inversion of Control, IoC), który dostarcza nam możliwości wstrzykiwania zależności (ang. Dependency Injection, DI). Jest to miejsce gdzie dzieje się magia :-)
Taki kontener należy odpowiednio skonfigurować, aby bean‘y zarządzane przez framework były odpowiednio zainicjowane oraz ze sobą połączone. Nie będę opisywał wszystkich aspektów konfiguracji kontenera IoC, gdyż to zadanie na niekrótką książkę. Skupię się tylko na definiowaniu powiązań między bean‘ami za pomocą automatycznego wstrzykiwania (ang. autowire).
Tytułem krótkiego wprowadzenia zaimplementujmy dwa bardzo proste bean‘y:
package pl.michalmech.autoexample.bean;
public class Parent {
private Integer simpleNumber;
private Property propertyObject;
public Parent() {}
/* setters&getters */
}
package pl.michalmech.autoexample.bean;
public class Property {
private String simpleText;
public Property() {}
/* setters&getters */
}
Jak widać to bardzo niewyszukane byty pozostające w banalnej zależności. Swoją mini aplikację uruchamiam przez przeglądarkę. W związku z tym dołożyłem sobie prosty kontroler:
package pl.michalmech.autoexample.web;
public class HelloController implements Controller {
protected final Log logger = LogFactory.getLog(getClass());
private Parent parentObject;
@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
logger.info(parentObject.getSimpleNumber());
logger.info(parentObject.getPropertyObject().getSimpleText());
return new ModelAndView("index.jsp");
}
/* setters&getters */
}
Tytle tytułem wstępu. Czas zabrać się za definicję bean‘ów i ich właściwości w deskryptorze. Oto one:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean name="/index.html" class="pl.michalmech.autoexample.web.HelloController"/>
<bean name="parentObject" class="pl.michalmech.autoexample.bean.Parent">
<property name="simpleNumber" value="123"/>
</bean>
<bean name="propertyObject" class="pl.michalmech.autoexample.bean.Property">
<property name="simpleText" value="Lorem ipsum ..."/>
</bean>
</beans>
I tu pierwsze pytanie: Co się stanie kiedy uruchomimy teraz kontroler? Oczywiście, dostaniemy po oczach wyjątkiem NullPointerException z bardzo prostej przyczyny. Poza definicją bean‘ów należy je jeszcze wstrzyknąć sobie nawzajem czyli określić relację między nimi.
Automatyczne wstrzykiwanie na podstawie nazwy.
Powyższy deskryptor wymaga dwóch poprawek. Pierwsza to wstrzyknięcie obiektu klasy Parent do kontrolera HelloController:
<bean name="/index.html" class="pl.michalmech.autoexample.web.HelloController">
<property name="parentObject" ref="parentObject"/>
</bean>
Jest to najczęściej stosowany sposób definiowania zależnościami pomiędzy bean‘ami. W jawny sposób mówimy, że bean o nazwie parentObject powinien zostać wstrzyknięty w pole kontrolera o tej samej nazwie.
Koleją zależność zdefiniujemy nieco inaczej:
<bean name="parentObject" class="pl.michalmech.autoexample.bean.Parent" autowire="byName">
<property name="simpleNumber" value="123"/>
</bean>
Obiekt parentObject klasy Parent posiada jedno pole typu prostego i je tutaj pomijam bo nie ma znaczenia przy omawianiu automatycznego wstrzykiwania, które tyczy się tylko obiektów. Zadeklarowanie bean‘a jako autowire=”byName” sprawi, że Spring automatycznie wyszuka bean‘a o nazwie takiej jak nazwa inicjowanego pola. W tej sytuacji w pole propertyObject zostanie wstrzyknięty bean o nazwie propertyObject. Sprawa jest dość prosta i klarowna. Warto zwrócić jednak uwagę na trzy kwestie:
Niezgodność typów. W przypadku kiedy bean o pasującej nazwie próbuje być wstrzyknięty jako wartość pola ale nie jest zgodny z jego typem dostaniemy wyjątek BeanCreationException, spowodowany przez TypeMismatchException:
Caused by: org.springframework.beans.TypeMismatchException: Failed to convert property value of type [pl.michalmech.autoexample.bean.FakeProperty] to required type [pl.michalmech.autoexample.bean.Property] for property 'propertyObject'; nested exception is java.lang.IllegalArgumentException: Cannot convert value of type [pl.michalmech.autoexample.bean.FakeProperty] to required type [pl.michalmech.autoexample.bean.Property] for property 'propertyObject': no matching editors or conversion strategy found
Brak bean‘a o szukanej nazwie. Jeśli kontener nie znajdzie bean‘a wymaganej nazwie to, jak łatwo się domyślić, nic nie wstrzyknie. Pole nadal będzie miało wartość null;
Niejednoznaczność nazwy bean‘a. To problem, który może wystąpić i który nie jest związany bezpośrednio z automatycznym wstrzykiwaniem. Dwa bean‘y o tej samej nazwie to prosty błąd konfiguracji, który sprowadza się do BeanDefinitionParsingException.
Automatyczne wstrzykiwanie po typie.
Kontener IoC pozwala nam automatycznie wstrzyknąć zależności nie tylko bazując na nazwach bean‘ów ale również na podstawie ich typów. Zmodyfikujmy konfigurację automatycznego wstrzykiwania dla bean‘a parentObject następująco:
<bean name="parentObject" class="pl.michalmech.autoexample.bean.Parent" autowire="byType">
<property name="simpleNumber" value="123"/>
</bean>
Zmienił się typ automatycznego wstrzykiwania zależności. W obecnej sytuacji Spring natrafiając na pole propertyObject bean‘a parentObject będzie starał się wstrzyknąć bean‘a o typie Property. W moim prostym przykładzie mamy oczywiście takiego bean‘a więc wszystko uda się bez problemu.
W przypadku wstrzykiwania po typie nie musimy kompletnie martwić się o nazwę bean‘a. Podobnie nie musimy się martwić jeśli bean o żądanym typie nie zostanie odnaleziony. Po prostu nic nie zostanie wstrzyknięte.
Niestety całkowicie wbrew powiedzeniu “Od przybytku głowa nie boli” musimy się martwić o nadmiar bean‘ów o typie zgodnym z typem pola do którego Spring będzie chciał wstrzyknąć zależności.
Załóżmy, że mam dwa bean‘y o tym samym typie:
<bean name="propertyObject" class="pl.michalmech.autoexample.bean.Property">
<property name="simpleText" value="Lorem ipsum ..."/>
</bean>
<bean name="secondPropertyObject" class="pl.michalmech.autoexample.bean.Property">
<property name="simpleText" value="... muspi meroL"/>
</bean>
Próba uruchomienia tak skonfigurowanej aplikacji skończy się na BeanCreationException spowodowanym przez UnsatisfiedDependencyException:
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'parentObject' defined in ServletContext resource [/WEB-INF/AutoExample-servlet.xml]: Unsatisfied dependency expressed through bean property 'propertyObject': : No unique bean of type [pl.michalmech.autoexample.bean.Property] is defined: expected single matching bean but found 2: [propertyObject, secondPropertyObject]; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [pl.michalmech.autoexample.bean.Property] is defined: expected single matching bean but found 2: [propertyObject, secondPropertyObject]
Pomimo tego, że streszczenie wyjątku jest w jednej, długiej linii to dostarcza wielu informacji. Spring podpowiada, że znalazł aż dwa bean‘y pasujące typem do pola oraz podaje ich nazwy co pozwala na szybkie wytropienie błędu.
Automatyczne wstrzykiwanie za pomocą konstruktora.
Kolejnym sposobem na automatyczne wstrzyknięcie zależności do bean‘a jest użycie konstruktora. Zmodyfikujmy klasę Parent w następujący sposób:
package pl.michalmech.autoexample.bean;
public class Parent {
protected final Log logger = LogFactory.getLog(getClass());
private Integer simpleNumber;
private Property propertyObject;
public Parent(Property propertyObject) {
this.propertyObject = propertyObject;
logger.info("propertyObject field set by constructor");
}
public Property getPropertyObject() {
return propertyObject;
}
public void setPropertyObject(Property propertyObject) {
this.propertyObject = propertyObject;
logger.info("propertyObject field set by method");
}
/* setter&getter for simpleNumber */
}
W stosunku do oryginału zmieniły się dwie rzeczy:
- Usunięty został domyślny konstruktor;
- Dodany został konstruktor inicjujący pole propertyObject;
Zmieńmy jeszcze sposób automatycznego wstrzykiwania. Zmiana deskryptora wygląda następująco:
<bean name="parentObject" class="pl.michalmech.autoexample.bean.Parent" autowire="constructor">
<property name="simpleNumber" value="123"/>
</bean>
Próba uruchomienia aplikacji zakończy się sukcesem a w konsoli ujrzymy następujący napis:
propertyObject field set by constructor
Na podstawie tego przykładu widać doskonale, że automatyczne wstrzykiwanie za pomocą konstruktora niewiele różni się od wstrzykiwania na podstawie typu. Również w tym przypadku kontener wyszukuje bean‘a o odpowiednim typie lecz zamiast odpowiedniej metody do wstrzyknięcia używa konstruktora.
Sposób ten w odróżnieniu od byType niesie ze sobą więcej potencjalnych problemów. Pierwszym z nich jest sytuacja kiedy konstruktor posiada więcej argumentów. Załóżmy więc, że konstruktor przyjmuje nie tylko jeden argument o typie zgodnym z polem do którego będzie wstrzyknięta wartość (w tym przypadku Property) lecz posiada jeszcze jeden:
public Parent(Integer simpleNumber, Property propertyObject) {
this.simpleNumber = simpleNumber;
this.propertyObject = propertyObject;
}
W takiej sytuacji próba uruchomienia aplikacji zakończy się wyrzuceniem znanego już wyjątku UnsatisfiedDependencyException z komunikatem podobnym do:
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [java.lang.Integer] is defined: Unsatisfied dependency of type [java.lang.Integer]: expected at least 1 matching bean
Oznacza to, że aby kontener IoC mógł skorzystać z naszego konstruktora potrzebuje wszystkich argumentów. Co zresztą wcale nie dziwi. W związku z tym, że nie mamy bean‘a o typie Integer, dostajemy wyjątek.
Podobnie stałoby się gdybyśmy potrzebowali wstrzyknąć dwa lub więcej obiekty a żaden z konstruktorów nie posiadałby ich pełnej listy w swoich argumentach.
Jak już wcześniej się okazało przybytek nie zawsze jest dobry. Co się stanie w sytuacji kiedy będziemy mieli więcej konstruktorów, z których Spring mógłby skorzystać do wstrzykiwania zależności? Załóżmy więc taką sytuację:
public Parent(Integer simpleNumber, Property propertyObject) {
this.simpleNumber = simpleNumber;
this.propertyObject = propertyObject;
}
public Parent(String fakeString, Property propertyObject) {
this.propertyObject = propertyObject;
}
i
<bean name="parentObject" class="pl.michalmech.autoexample.bean.Parent" autowire="constructor">
<property name="simpleNumber" value="123"/>
</bean>
<bean name="someInt" class="java.lang.Integer">
<constructor-arg value="123"/>
</bean>
<bean name="someString" class="java.lang.String">
<constructor-arg value="Lorem ipsum ..."/>
</bean>
<bean name="propertyObject" class="pl.michalmech.autoexample.bean.Property">
<property name="simpleText" value="Lorem ipsum ..."/>
</bean>
Uruchomienie aplikacji na takich warunkach uda się bez problemu a do wstrzyknięcia zależności użyty zostanie pierwszy konstruktor. Dlaczego? No coż, nie mam pewności ale z moich obserwacji wynika, że użyty zostanie pierwszy pasujący konstruktor.
Automatyczny wybór sposobu wstrzykiwania.
Poza trzema omówionymi strategiami automatycznego wstrzykiwania zależności jest jeszcze jedna, polegająca na autodetekcji:
<bean name="parentObject" class="pl.michalmech.autoexample.bean.Parent" autowire="autodetect">
<property name="simpleNumber" value="123"/>
</bean>
Autodetekcja polega na tym, że kontener wybierze sam pomiędzy constructor a byType. Kryterium wyboru to obecność domyślnego konstruktora. Jeśli kontener IoC go znajdzie, to skorzysta ze wstrzykiwania na podstawie typu, jeśli nie, to spróbuje wstrzyknąć zależności za pomocą konstruktora.
Kolekcje a automatyczne wstrzykiwanie.
Zbliżając się ku końcowi wrócę jeszcze do tego przybytku od, którego głowa bolała. Mechanizm autowire radzi sobie bardzo dobrze z kolekcjami.
Kiedy wstrzykiwaliśmy na podstawie typu (autowire=”byType”) a bean‘ów o poszukiwanym typie było więcej niż jeden, framework raczył nas wyjątkiem UnsatisfiedDependencyException. Nie działoby się tak jeśli mielibyśmy do czynienia z tablicą, kolekcją (Collection) lub mapą:
package pl.michalmech.autoexample.bean;
public class Parent {
private Integer simpleNumber;
private Set<Property> propertyObjects;
public Set<Property> getPropertyObjects() {
return propertyObjects;
}
public void setPropertyObjects(Set<Property> propertyObjects) {
this.propertyObjects = propertyObjects;
}
/* setter&getter for simpleNumber */
}
W takiej sytuacji kontener wrzuci do kolekcji wszystkie znalezione bean‘y o pasującym typie. W powyższym przykładzie jest zbiór ale mechanizm zadziała również z tablicą oraz dowolną inną kolekcją (Collection). W przypadku mapy ważne jest by jej kluczem były obiekty typu String ponieważ to nazw bean‘ów Spring używa do wypełnienia mapy. W przeciwnym wypadku framework po prostu nic do niej nie wstrzyknie.
Kandydaci do wstrzyknięcia.
Użycie kolekcji zamiast pojedynczego bean‘a nie zawsze jest rozwiązaniem pozwalającym ominąć problem nadmiarowej ilości bean‘ów. Co w sytuacji kiedy po prostu chcemy wstrzyknąć jednego bean‘a i bardzo chcemy użyć automatycznego wstrzykiwania na podstawie typu? Otóż są jeszcze dwa rozwiązania.
Pierwsze z nich to wykluczenie części bean‘ów z kandydatury na bycie wstrzykniętym. Oto jak to się robi:
<bean name="propertyObject" class="pl.michalmech.autoexample.bean.Property" autowire-candidate="false">
<property name="simpleText" value="Lorem ipsum ..."/>
</bean>
Tak skonfigurowany bean nie będzie nam przeszkadzał w automatycznym wstrzykiwaniu, ponieważ nie będzie wzięty pod uwagę.
Innym sposobem jest określenie głównego bean‘a o danym typie:
<bean name="propertyObject" class="pl.michalmech.autoexample.bean.Property" primary="true">
<property name="simpleText" value="Lorem ipsum ..."/>
</bean>
Jeśli jestkilka bean‘ów o danym typie i tylko jeden z nich jest primary to on zostanie wstrzyknięty.
Podsumowanie.
Jak widać automatyczne wstrzykiwanie zależności to trochę jak pudełko czekoladek, nigdy nie wiadomo co się trafi (tak mawiał Forrest Gump). Nawet dokumentacja zaznacza, że należy się zastanowić czy warto stosować mechanizm autowire przy dużych wdrożeniach, ponieważ nie jest łatwo wtedy utrzymać całkowitą jednoznaczność i kontrolę nad bean‘ami. Mi się jednak ten echanizm podoba. Zobaczymy jak w przyszłości.
P.S.
Mechanizmem automatycznego wstrzykiwania zależności można również sterować za pomocą adnotacji dostępnych w Javie od wersji 1.5. Postanowiłem jednak pominąć je w tym wpisie ponieważ urósłby on do zbyt dużych rozmiarów.