Wraz z powstaniem Spring Framework proces tworzenia aplikacji klasy enterprise w Javie został znaczenie uproszczony. Stało się tak dzięki kontenerowi zależności, który odseparował kreacje obiektów od ich użycia. Nie musimy się martwić o zarządzenie zależnościami, Spring za nas je wykryje i uporządkuje. Dokonuje on tego automatycznie lub na podstawie naszej własnoręcznie zdefiniowanej konfiguracji. Zacznijmy od krótkiego opisu działania kontenera zależności Springa.
Działanie kontenera zależności Springa
Jeden z najbardziej rozpoznawalnych frameworków Javowych
Kontener zależności opiera się na mechanizmie wstrzykiwania zależności, czyli Dependency Injection. Dzięki niemu jesteśmy w stanie oddać kontrolę frameworkowi, który za nas będzie zarządzał cyklem życia obiektów w aplikacji. Będzie odpowiedzialny za ich stworzenie oraz odpowiednie połączenie. Przed tym podejściem to programista miał za zadanie kreować obiekty poprzez słowo kluczowe new i przekazywać je do innych obiektów, które ich wymagają.
1
2
3
ItemRepository itemRepository = new ItemRepository();
EventPublisher eventPublisher = new EventPublisher();
OrderService orderService = new OrderService(itemRepository, eventPublisher);
Dzięki użyciu kontenera zależności Springa nie trzeba się już dłużej martwić, aby zapisywać tego typu kod. Operacja tworzenia zostanie oddelegowana do frameworka, a my będziemy mogli skupić się na implementowaniu przypadków biznesowych. No dobrze, ale skąd Spring będzie wiedział, które obiekty za nas stworzyć oraz w jaki sposób? Musimy mu w tym pomóc poprzez użycie odpowiednich adnotacji (jeśli nie wiesz czym są adnotacje to zapraszam na stronę devcave.pl): @Component lub @Bean.
Adnotacja @Component
W dokumentacji Springa można przeczytać, że adnotacja @Component jest odpowiedzialna za:
Indicates that an annotated class is a “component”. Such classes are considered as candidates for auto-detection when using annotation-based configuration and classpath scanning.
Oznaczając daną klasę taką adnotacją zostanie ona potencjalnym kandydatem do automatycznego wykrycia przez Springa. Oczywiście stanie się tak jeśli znajdzie się w pakiecie, który podlega skanowaniu (zachęcam do zapoznania się z @ComponentScan). Jest to naprawdę wygodne rozwiązanie, przez dosłownie jedną linijkę kodu jesteśmy zwolnieni z myślenia o sposobie tworzenia danego obiektu. Jednak ma to minus w postaci wnikania mechanizmów frameworka do naszego kodu, chodzi tu głównie o domenę biznesową, która powinna być niezależna od “świata zewnętrznego”. Jeśli będziemy zmuszeni do zmiany infrastruktury to nie chcielibyśmy, aby nasza logika biznesowa na tym ucierpiała. Pragnęlibyśmy, żeby pozostała ona niezależna od jakichkolwiek zmian technicznych. Z tego powodu warto przyjrzeć się konfiguracji przy pomocy adnotacji @Bean.
Na koniec krótki przykład jak może wyglądać kod wraz z adnotacją @Component, oczywiście każdą z poniższych klas należy rozdzielić na osobne pliki. Warto dodać, że od Springa 4.3 adnotacja @Autowired nie jest konieczna nad konstruktorem jeśli klasa posiada tylko jeden konstruktor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class ItemRepository {
...
}
@Component
public class EventPublisher {
...
}
@Component
public class OrderService {
private final ItemRepository itemRepository;
private final OrderService orderService;
@Autowired // można pominąć tą adnotację
public OrderService(ItemRepository itemRepository, OrderService orderService) {
this.itemRepository = itemRepository;
this.orderService = orderService;
}
...
}
Adnotacja @Bean
Wszyscy mamy nadzieję, że czasy konfiguracji poprzez XML w Springu już dawno minęły. Teraz możemy wszystko konfigurować na poziomie klasy. Wystarczy stworzyć klasę konfiguracyjna, oznaczyć ją za pomocą adnotacji @Configuration i zadeklarować w niej odpowiednie metody kreacyjne tworzące instancje odpowiednich klas. Oczywiście jeśli dana klasa wymaga do jej stworzenia zależności to możemy wstrzyknąć potrzebny obiekt jako parametr metody. Wygląda to mniej więcej w sposób przekazany poniżej.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration(proxyBeanMethods = false)
public class AppConfiguration {
@Bean
ItemRepository itemRepository() {
return new ItemRepository();
}
@Bean
EventPublisher eventPublisher() {
return new EventPublisher();
}
@Bean
OrderService orderService(
ItemRepository itemRepository,
EventPublisher eventPublisher
) {
return new OrderService(itemRepository, eventPublisher);
}
}
Tutaj mała dygresja do adnotacji @Configuration, dodałem w niej własność ‘proxyBeanMethods = false’, której użycie zostało dobrze uzasadnione na StackOverflow. W skrócie chodzi o to, aby nie tworzyć niepotrzebnie proxy i lepiej kontrolować ile zależności jest wymagane przez daną klasę (kierując się zasadą Single Responsible Principle). Wracając do głównego wątku, dzięki klasie konfiguracyjnej nasza logika biznesowa może pozostać niezależna. Jeśli chcielibyśmy przejść na inny framework posiadający kontener zależności to nie dotykamy w ogóle serca naszej aplikacji. Wyrzucamy tylko klasy konfiguracyjne Springa i korzystamy z innych możliwości nowego frameworka, przez to zmiana infrastruktury staje się naprawdę prosta. Wszystko ma minusy, więc i to rozwiązanie nie jest idealne. Nie możemy oddelegować sposobu tworzenia obiektów do Springa, sami musimy napisać konfigurację kreującą obiekty. Moim zdaniem ma to sporą korzyść w postaci tego, że możemy weryfikować czy jakaś klasa nie ma zbyt wielu odpowiedzialności kosztem kilkunastu linijek kodu więcej.
Oczywiście w przykładzie powyżej korzystamy ze słówka kluczowego new jednak wykorzystywane jest ono tylko w celu napisania przepisu tworzenia obiektów. W kodzie odpowiedzialnym za logię biznesową nie musimy się już tym w ogóle martwić!
Bonus korzystania z **@Bean w testach
Podejście z użyciem adnotacji @Bean ma też korzystny efekt uboczny przy pisaniu testów. Wykorzystując produkcyjny schemat konstruowania obiektów możemy nadpisać metodę tworzącą połączenie np. z bazą danych na taką korzystającą z danych w pamięci. Dzięki temu jesteśmy w stanie przygotować szybki test, który nie stawia całego kontenera zależności Springa. Jest on w stanie przetestować integrację obiektów naszej głównej logiki biznesowej bez kosztownych połączeń z zewnętrznymi komponentami.
Konfiguracja w AppConfiguration.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration(proxyBeanMethods = false)
public class AppConfiguration {
@Bean
ItemRepository itemRepository() {
return new ItemRepository();
}
@Bean
OrderVerification orderVerification(ItemRepository itemRepository) {
return new OrderVerification(itemRepository);
}
@Bean
OrderService orderService(OrderVerification orderVerification) {
return new OrderService(orderVerification);
}
}
Zmodyfikowana konfiguracja dla testów w klasie TestConfiguration.
1
2
3
4
5
6
7
8
9
10
public class TestConfiguration extends AppConfiguration {
OrderVerification testOrderVerification() {
return new OrderVerification(new InMemoryItemRepository());
}
OrderService testOrderService() {
return orderService(testOrderVerification());
}
}
Klasa testowa TestClass.
1
2
3
4
5
6
7
8
9
10
11
12
public class TestClass {
@Test
void manualConfigurationForTest() {
OrderService orderService = new TestConfiguration().testOrderService();
OrderResult result = orderService.verify(sameData());
assertThat(result.getStatus()).isEqualTo(OrderStatus.ORDERED);
}
}
Ważne, żeby nie stosować w klasie TestConfiguration adnotacji @Bean oraz @Configuration. Korzystamy z niej jak ze zwykłej klasy, w której tylko zdefiniowaliśmy sposób tworzenia naszych obiektów.
Podsumowanie
Frameworki w programowaniu są naprawdę świetnym rozwiązaniem, istnieją po to, aby ułatwić nam pracę. Nie ma sensu tworzyć własnego kontenera zależności zwłaszcza jak na rynku jest wiele dostępnych rozwiązań napisanych przez doświadczonych programistów. Jednak potrafią one wręcz “zawładnąć” naszą aplikacją, wniknąć w jej bebechy. Dlatego polecam korzystać z takich półśrodków jak @Bean i @Configuration zamiast uzależniać się od Springa przez adnotację @Component. Chyba, że to jest mała aplikacja, która będzie miała krótki czas życia i nie będzie jej przeszkadzało wymieszanie logiki biznesowej z infrastrukturą. Oczywiście wszystko zależy od Ciebie i to Ty powinieneś decydować o najlepszym rozwiązaniu dla Twojego projektu!