Automatyczne wiązania @Autowired

Wstrzykiwanie zależności jest prawdopodobnie najważniejszym wzorcem projektowym wykorzystywanym do tworzenia aplikacji nie tylko w Javie, ale także w większości obiektowych języków programowania. W tym artykule dowiesz się, jak wykorzystać wstrzykiwanie zależności, mechanizm automatycznego wiązania i adnotacji @Autowired w Springu.

Czym jest wstrzykiwanie zależności

Zależnością nazywamy sytuację, w której jedna klasa korzysta z obiektu innej klasy.

Obiekt typu Bar potrzebuje do działania obiektu typu Foo. Obiekt Foo jest więc zależnością klasy Bar. Zależności najczęściej będą w klasie reprezentowane jako pola:

class Foo {
    //...
}
class Bar {
    private Foo foo;
    
    //...
}

Wstrzykiwanie zależności może być realizowane na trzy sposoby.

Wstrzykiwanie przez konstruktor

Podejście rekomendowane, jasno wskazuje, że dana zależność jest wymagana do utworzenia obiektu.

Foo foo = new Foo();
Bar bar = new Bar(foo);

Wstrzykiwanie przez setter

Przydatne w sytuacji, gdy chcemy wskazać zależności opcjonalne. Można łączyć wstrzykiwanie przez konstruktor ze wstrzykiwaniem przez settery. Zależności w konstruktorze będą wymagane, a w setterach opcjonalne.

Foo foo = new Foo();
Bar bar = new Bar();
bar.setFoo(foo);

Wstrzykiwanie bezpośrednio do pola

Najgorsze podejście, ponieważ łamie zasady enkapsulacji. Pola w takiej sytuacji nie mogą być prywatne, albo konieczne jest wykorzystanie refleksji.

Foo foo = new Foo();
Bar bar = new Bar();
bar.foo = foo; //pole nie może być prywatne

Kurs Spring

Automatyczne wiązania

W Springu wstrzykiwanie zależności między komponentami realizowane jest najczęściej, wykorzystując mechanizm automatycznego wiązania. Polega to na tym, że Spring podczas skanowania klas komponentów automatycznie rozpoznaje zależności danej klasy na podstawie parametrów konstruktorów, setterów, albo pól oznaczonych dodatkowymi adnotacjami i dostarcza do nich wymagane zależności podczas tworzenia ziaren.

Klasy komponentów należy oznaczyć adnotacją @Component, albo jedną z jej specjalizacji, czyli np. @Service, @Repository, czy @Controller.

@Service
class Foo {
    //...
}
@Service
class Bar {
    private Foo foo;

    public Bar(Foo foo) {
        this.foo = foo;
    }

    //...
}

Spring podczas tworzenia ziaren i umieszczania ich w kontenerze wstrzykiwania zależności wykryje, że klasa Bar ma wymaganą zależność Foo. Najpierw utworzy więc ziarno tego typu, a dopiero później ziarno typu Bar, do którego wstrzyknie przez konstruktor obiekt Foo.

Adnotacja @Autowired

Wstrzykiwanie zależności przez konstruktor odbywa się w Springu w wersji 4.2 i nowszych automatycznie. Wcześniej należało wskazać, że chcemy do konstruktora wstrzyknąć zależność przy pomocy adnotacji @Autowired. Kod z poprzedniego listingu jest więc równoważny z poniższym:

@Service
class Bar {
    private Foo foo;

    @Autowired
    public Bar(Foo foo) {
        this.foo = foo;
    }

    //...
}

Adnotacja ta pozwala wskazać, że któraś z zależności jest opcjonalna, umożliwia także wstrzykiwanie przez setter, albo bezpośrednio do pola.

@Controller
class Baz {
    private Foo foo;
    private Bar bar;

    @Autowired //zależność wymagana
    public Baz(Foo foo) {
        this.foo = foo;
    }

    @Autowired(required = false) //zależność opcjonalna
    public void setBar(Bar bar) {
        this.bar = bar;
    }

    //...
}

Adnotacja @Autowired umożliwia wstrzykiwanie do pola nawet wtedy, gdy jest ono prywatne:

@Service
class Bar {
    @Autowired
    private Foo foo;

    //...
}

Jest to jednak podejście odradzane, ponieważ powoduje ukrycie zależności danej klasy i utrudnia testowanie ze względu na trudność z dostarczeniem do klasy mocków.

Typowe błędy

Wyjątek NoSuchBeanDefinitionException

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type '...' available

Powodem są najczęściej:

  • brak adnotacji wskazujących, że klasy są komponentami Springa (@Component, @Service, itp.)
  • błędna struktura projektu, np. klasa startowa aplikacji, która jest jednocześnie klasą konfiguracji odpowiedzialną za wskazanie pakietów do przeskanowania komponentów, nie znajduje się w pakiecie nadrzędnym dla klas komponentów:
a
--b
----c
------SpringApplication.java
----Foo.java
----Bar.java

Zakładając, że klasa SpringApplication jest klasą startową aplikacji wykorzystującej Spring Boota, czyli jest oznaczona adnotacją @SpringBootApplication, to Spring domyślnie przeszuka wyłącznie pakiet a.b.c i pakiety w nim zagnieżdżone w poszukiwaniu komponentów. Komponenty znajdują się jednak jeden poziom wyżej w pakiecie a.b. Rozwiązaniem jest dodanie do klasy konfiguracji dodatkowej adnotacji @ComponentScan i wskazanie dodatkowych pakietów do skanowania, albo zmiana struktury projektu w taki sposób, aby klasy komponentów znajdowały się co najmniej na tym samym poziomie, co klasa startowa aplikacji:

a
--b
----c
----SpringApplication.java
----Foo.java
----Bar.java

Wyjątek NoUniqueBeanDefinitionException

Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name '...' defined in file [...]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type '...' available: expected single matching bean but found 2: X,Y

Wyjątek występuje w sytuacji, gdy korzystamy z warstwy abstrakcji, np. interfejsów i stosujemy zasadę odwrócenia zależności (programujemy "do interfejsu").

public interface Base { }
@Service
public class Foo implements Base {
    //...
}
@Service
public class Bar implements Base {
    //...
}
@Controller
public class Baz {
    private Base base;

    public Baz(Base base) {
        this.base = base;
    }

    //...
}

W powyższym przykładzie komponent Baz ma zależność typu Base, ale w projekcie istnieją jego dwie implementacjie - Foo i Bar. Spring nie wie obiekt którego typu powinien wstrzyknąć do konstruktora.

Rozwiązaniem jest wykorzystanie kwalifikatorów (@Qualifier), profili (@Profile), albo usunięcie jednej z implementacji z projektu.

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.