Jest to ostatni z wpisów, który przenoszę z bloga SpringDeveloper.pl. Wcześniejsze artykuły w tematyce Springa były raczej teoretyczne. Teraz skupimy się na praktyce. Dowiemy się w jaki sposób stworzyć konfigurację beanów Springa wykorzystując do tego celu Javę. Nie poruszymy w ogóle tematu plików konfiguracyjnych XML z racji ich coraz rzadszego wykorzystywania. Zabierajmy się więc do pracy.
Konfiguracja przy pomocy adnotacji
Bardzo popularnym rozwiązaniem do rejestrowania obiektów w kontenerze zależności jest stosowanie adnotacji. Możemy do tego celu skorzystać z @Component
oraz jego rozszerzeń w postaci @Repository
, @Service
czy @Controller
. Chociaż mają różne nazwy to z perspektywy rejestrowania beanów niczym się od siebie nie różnią. Najważniejsze dla Springa jest to, aby klasa pośrednio lub bezpośrednio miała adnotację @Component
. Zajrzyjmy więc do opisu adnotacji @Service
w dokumentacji, aby sprawdzić czy tak faktycznie jest.
1
2
3
4
5
@Target(value=TYPE)
@Retention(value=RUNTIME)
@Documented
@Component
public @interface Service
Może nasunąć Ci się pytanie - “dlaczego wprowadzono aż cztery koncepty o identycznym znaczeniu?”. Śpieszę z wyjaśnieniem. Pierwszym powodem jest możliwość wyróżnienia klas ze względu na ich odpowiedzialność. Jeżeli klasa jest oznaczona przez adnotację @Repository
to można przypuszczać, że służy ona do komunikacji z bazą danych. Natomiast adnotacja @Controller
sugeruje, że dana klasa jest przeznaczona do wymiany informacji ze światem zewnętrznym przy pomocy np. protokołu HTTP. Drugim motywem jest fakt, że wcześniej przedstawione adnotacje mogą służyć nam jako znaczniki dla programowania aspektowego. W ten sposób możemy określić dla jakich metod klas oznaczonych daną adnotacją ma zostać wykonany dany kawałek kodu przed lub po ich wywołaniu.
Po tym krótkim wprowadzeniu przejdźmy w końcu do tej obiecanej praktyki.
Weryfikacja działania adnotacji
W przykładach po raz kolejny posłużymy się Spring Bootem. Dzięki temu nie zaszumimy całego rozwiązania rozbudowaną konfiguracją. O samym Spring Boot porozmawiamy sobie kiedy indziej. Na ten moment załóż, że z jego pomocą łatwiej jest mi pokazać podstawowe koncepcje stojące za Spring Framework.
Poniżej w kodzie mamy zarejestrowane dwa beany klas: AnimalService
i AnimalRepository
. Udało nam się to zrobić przez wykorzystanie adnotacji @Service
oraz @Repository
. Następnie wykorzystując kontener zależności możemy wyciągnąć interesujący nas bean.
Skupmy się na instancji klasy AnimalService
. Możemy ją wyciągnąć z kontenera zależności wywołując metodę getBean
. Aby tego dokonać należy przekazać do niej jako argumenty nazwę beana oraz jego typ. Skąd wziąć nazwę? Spring domyślnie nasze beany nazywa wykorzystując nazwę klasy w konwencji ‘camelCase’. Dla przykładu, dla AnimalService
będzie to właśnie animalService
. Jeśli chcemy zmienić nazwę beana możemy to uczynić podając ją w adnotacji np. @Component("foo")
. Teraz wyciągnięcie beana jest możliwe tylko poprzez nazwę ‘foo’.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package pl.springdeveloper.containerioc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@SpringBootApplication
public class ContainerIocApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(
ContainerIocApplication.class, args);
// it is not the best idea...
AnimalService animalService = context.getBean(
"animalService", AnimalService.class);
Long animalId = animalService.createAnimal();
animalService.callAnimal(animalId);
}
}
@Service
class AnimalService {
private static final AtomicLong ID_SEQUENCE = new AtomicLong(1);
private final AnimalRepository animalRepository;
public AnimalService(AnimalRepository animalRepository) {
this.animalRepository = animalRepository;
}
public Long createAnimal() {
long generatedId = ID_SEQUENCE.getAndIncrement();
animalRepository.save(generatedId, new Animal());
return generatedId;
}
public void callAnimal(Long id) {
animalRepository.findBy(id)
.ifPresent(Animal::makeNoise);
}
}
@Repository
class AnimalRepository {
private final Map<Long, Animal> DATABASE = new ConcurrentHashMap<>();
public void save(Long id, Animal animal) {
DATABASE.put(id, animal);
}
public Optional<Animal> findBy(Long id) {
return Optional.ofNullable(DATABASE.get(id));
}
}
class Animal {
public void makeNoise() {
System.out.println("Muuu!");
}
}
Po uruchomieniu naszej prostej aplikacji powinniśmy dostać napis ‘Muuu!’ w konsoli. Wychodzi na to, że wszystko zadziałało prawidłowo.
💡 Wyciąganie beanów w kodzie bezpośrednio z kontenera nie jest najlepszym pomysłem. Można wtedy napotkać problemy związane z antywzorcem Service Locator. Ja jednak na potrzeby prezentacji pozwoliłem sobie na takie nadużycie.
Konfiguracja przy pomocy kodu Javy
Poznaliśmy sposób polegający na wykorzystaniu adnotacji. Przejdźmy teraz do drugiej opcji dostępnej w ramach języka Java. Polega ona na tworzeniu klas konfiguracyjnych łączących nasze beany. W tym sposobie musimy napisać więcej kodu, ale nasze klasy biznesowe nie będą oznaczone żadnymi adnotacjami Springa. To rozwiązanie pozwala nam na łatwiejsze pisanie testów, nie martwiąc się o tworzenie dodatkowej konfiguracji tylko na potrzeby testów. Nasz kod produkcyjny i kod testowy korzystają z tego samego źródła konfiguracyjnego.
W jaki sposób to uczynić? Musimy zaprzyjaźnić się z dwoma dodatkowymi adnotacjami: @Configuration
oraz @Bean
. Adnotację @Configuration
umieszamy nad klasą, która ma stać się klasą konfiguracyjną. W jej wnętrzu definiujemy metody zwracające obiekty klas będących naszymi beanami. Jednak aby tak się stało to taka metoda musi zostać oznaczona adnotacją @Bean
. Wróćmy teraz do naszego przykładu i zobaczymy jak będzie się on prezentował w nowym podejściu.
💡 Nie musiałem w tym kodzie korzystać z adnotacji @Configuration
. Jak zajrzymy do opisu adnotacji @SpringBootApplication
w dokumentacji to zobaczymy, że korzysta ona z innej adnotacji @SpringBootConfiguration
. Ta natomiast ma nad sobą właśnie @Configuration
. Z tego powodu mogłem sobie pozwolić na zdefiniowanie beanów w klasie ContainerIocApplication
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package pl.springdeveloper.containerioc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@SpringBootApplication
public class ContainerIocApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(
ContainerIocApplication.class, args);
// it is not the best idea...
AnimalService animalService = context.getBean(
"animalService", AnimalService.class);
Long animalId = animalService.createAnimal();
animalService.callAnimal(animalId);
}
@Bean
AnimalService animalService(AnimalRepository animalRepository) {
return new AnimalService(animalRepository);
}
@Bean
AnimalRepository animalRepository() {
return new AnimalRepository();
}
}
class AnimalService {
private static final AtomicLong ID_SEQUENCE = new AtomicLong(1);
private final AnimalRepository animalRepository;
public AnimalService(AnimalRepository animalRepository) {
this.animalRepository = animalRepository;
}
public Long createAnimal() {
long generatedId = ID_SEQUENCE.getAndIncrement();
animalRepository.save(generatedId, new Animal());
return generatedId;
}
public void callAnimal(Long id) {
animalRepository.findBy(id)
.ifPresent(Animal::makeNoise);
}
}
class AnimalRepository {
private final Map<Long, Animal> DATABASE = new ConcurrentHashMap<>();
public void save(Long id, Animal animal) {
DATABASE.put(id, animal);
}
public Optional<Animal> findBy(Long id) {
return Optional.ofNullable(DATABASE.get(id));
}
}
class Animal {
public void makeNoise() {
System.out.println("Muuu!");
}
}
W konsoli znowu pojawił się tekst ‘Muuu!’, więc wszystko zostało poprawnie skonfigurowane. Tutaj małe wtrącenie. Jak wspomniałem wyżej, wykorzystałem w tym przykładzie znajomość adnotacji @SpringBootApplication
. Jednak lepszą praktyką w kodzie produkcyjnym jest trzymanie konfiguracji w osobnych klasach jak w kodzie poniżej.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
@Configuration
class AnimalConfiguration {
@Bean
AnimalService animalService(AnimalRepository animalRepository) {
return new AnimalService(animalRepository);
}
@Bean
AnimalRepository animalRepository() {
return new AnimalRepository();
}
}
// ...
Rączki są bardziej pobrudzone, prawda? Zamiast pozwolić Springowi na całą magię, przez jego adnotacje, sami musieliśmy użyć operatora new
do tworzenia naszych beanów. Jednak jak wspomniałem wcześniej, to podejście ma swoje zalety. Oczywiście okupione kilkoma wadami, ale tak jest ze wszystkim…
💡 Dzięki Spring Boot nie musieliśmy zajmować się adnotacją @ComponentScan
. Dostaliśmy ją w prezencie od @SpringBootApplication
. Korzystając z niej Spring wie gdzie ma szukać kandydatów na beany. Zaczyna on od pakietu, w którym znajduje się klasa z oznaczeniem @ComponentScan
i następnie przegląda jej podpakiety. Co ciekawe, klasa oznaczona adnotacją @Configuration
też jest beanem. Umiesz powiedzieć dlaczego?
Podsumowanie
W tym artykule nauczyliśmy się jak rejestrować nasze beany w kontekście Springa. Wykorzystaliśmy do tego adnotacje, jak i klasy konfiguracyjne. Przekażę Ci na koniec pewną heurystykę co do wyboru sposobu konfiguracji. Moją metodą kciuka jest to, że gdy potrzebuje coś zrobić szybko i ten fragment kodu nie potrzebuje testów to korzystam z @Component
. Natomiast, gdy chcę coś zrobić zgodnie ze sztuką i dobrze to otestować to zabieram się za tworzenie klas konfiguracyjnych wykorzystując @Configuration
. Daj znać w komentarzu jakie jest Twoje zdanie na ten temat.