Spring Framework to jeden z najpopularniejszych frameworków wykorzystywanych do tworzenia aplikacji w języku Java. Od ponad dwóch dekad stanowi fundament wielu systemów biznesowych, aplikacji webowych oraz architektur opartych na mikroserwisach. Jego głównym zadaniem jest uproszczenie procesu tworzenia oprogramowania poprzez dostarczenie mechanizmów zarządzania obiektami, ich zależnościami oraz konfiguracją aplikacji.
Jedną z największych zalet Springa jest możliwość oddzielenia logiki biznesowej od infrastruktury technicznej. Dzięki mechanizmom takim jak Inversion of Control (IoC) czy Dependency Injection (DI) programista może skupić się na implementacji funkcjonalności, pozostawiając frameworkowi zarządzanie zależnościami i cyklem życia obiektów.
Czym jest Spring Framework?
Spring Framework to rozbudowany, modułowy framework dla języka Java, który dostarcza narzędzi do budowania aplikacji o różnym stopniu złożoności – od prostych aplikacji webowych po rozbudowane systemy korporacyjne i mikroserwisy.
W przeciwieństwie do bibliotek, które rozwiązują pojedyncze problemy, Spring oferuje spójny ekosystem komponentów wspierających różne warstwy aplikacji. Programista może korzystać tylko z tych modułów, których rzeczywiście potrzebuje.
Jakie problemy rozwiązuje Spring?
W tradycyjnej aplikacji Java programista musi samodzielnie tworzyć obiekty, zarządzać ich zależnościami, konfigurować połączenia z bazą danych czy obsługiwać transakcje. Wraz ze wzrostem aplikacji prowadzi to do coraz większej ilości kodu infrastrukturalnego oraz silnego powiązania między komponentami.
Spring pomaga ograniczyć te problemy, oferując gotowe mechanizmy, które ułatwiają:
- Zarządzanie zależnościami – komponenty nie muszą samodzielnie tworzyć obiektów, z których korzystają.
- Konfigurację aplikacji – ustawienia mogą być definiowane za pomocą adnotacji, klas konfiguracyjnych i plików konfiguracyjnych.
- Integrację z innymi technologiami – Spring współpracuje między innymi z Hibernate, systemami komunikatów oraz bazami danych.
- Testowanie – komponenty można łatwiej izolować i zastępować atrapami podczas testów.
- Obsługę zagadnień technicznych – transakcje, bezpieczeństwo czy obsługa żądań HTTP mogą być realizowane za pomocą gotowych mechanizmów.
Spring Framework a Spring Boot
Spring Framework i Spring Boot to dwa pojęcia, które często są ze sobą utożsamiane, jednak nie oznaczają tego samego.
Spring Framework stanowi fundament całego ekosystemu. Dostarcza podstawowe mechanizmy zarządzania komponentami, obsługi transakcji czy tworzenia aplikacji webowych.
Krótka historia Spring Framework
Historia Spring Framework sięga 2002 roku, kiedy brytyjski programista Rod Johnson opublikował książkę Expert One-on-One J2EE Design and Development. Autor krytykował w niej nadmierną złożoność ówczesnych technologii J2EE (Java 2 Enterprise Edition), szczególnie Enterprise JavaBeans (EJB), które wymagały dużej ilości konfiguracji oraz kodu infrastrukturalnego.
Johnson zaproponował alternatywne podejście do tworzenia aplikacji, oparte na zwykłych obiektach Java, określanych jako POJO (Plain Old Java Object). Zamiast zmuszać programistów do implementowania specjalnych interfejsów czy dziedziczenia po klasach frameworka, zaproponował wykorzystanie lekkiego kontenera zarządzającego zależnościami między obiektami.
Kod dołączony do książki stał się fundamentem Spring Framework. Projekt został publicznie udostępniony w 2003 roku, a 24 marca 2004 roku ukazała się pierwsza stabilna wersja Spring Framework 1.0.
Ewolucja Spring Framework
Na przestrzeni kolejnych lat Spring był systematycznie rozwijany, dostosowując się do zmieniających się potrzeb programistów Java.
Najważniejsze etapy jego rozwoju to:
- 2004 – Spring Framework 1.0 – pierwsza stabilna wersja oferująca m.in. kontener IoC, Dependency Injection, AOP oraz obsługę transakcji.
- 2006 – Spring Framework 2.0 – wprowadzenie obsługi adnotacji takich jak
@Transactionaloraz rozszerzenie możliwości konfiguracji. - 2009 – Spring Framework 3.0 – wsparcie dla konfiguracji opartej na Javie (
@Configuration,@Bean) oraz pełniejsze wykorzystanie adnotacji. - 2013 – Spring Framework 4.0 – wsparcie dla Java 8 oraz rozwój mechanizmów wykorzystywanych w nowoczesnych aplikacjach.
- 2014 – Spring Boot 1.0 – pojawienie się projektu, który znacząco uprościł konfigurację i uruchamianie aplikacji Spring.
- 2017 – Spring Framework 5.0 – wprowadzenie programowania reaktywnego poprzez Spring WebFlux.
- 2022 – Spring Framework 6.0 – przejście na Java 17 jako minimalną wymaganą wersję oraz migracja z
javax.*dojakarta.*.
Szczególnie istotnym wydarzeniem było pojawienie się Spring Boot, który wyeliminował konieczność ręcznego konfigurowania wielu elementów aplikacji. Dzięki temu Spring stał się jeszcze bardziej przystępny i dobrze dopasowany do architektury mikroserwisowej.
Inversion of Control (IoC) i Dependency Injection (DI)
Dwa najważniejsze mechanizmy, na których opiera się Spring Framework, to Inversion of Control (IoC) oraz Dependency Injection (DI). Chociaż pojęcia te są ze sobą ściśle powiązane, nie oznaczają tego samego.
Czym jest Inversion of Control (IoC)?
Inversion of Control (IoC), czyli odwrócenie sterowania, to zasada projektowania oprogramowania, zgodnie z którą odpowiedzialność za określone aspekty działania aplikacji zostaje przeniesiona z kodu programisty do zewnętrznego mechanizmu, np. frameworka. IoC to realizacja słynnej reguły Dependency Inversion Principle z akronimu SOLID, którą możesz znać z książki Clean Code.
W tradycyjnym podejściu to programista decyduje, kiedy i w jaki sposób tworzone są obiekty oraz jak zarządzać ich zależnościami.
Przykładowo:
public class PaymentService {
private final PaymentGateway paymentGateway =
new StripePaymentGateway();
public void processPayment(BigDecimal amount) {
paymentGateway.pay(amount);
}
}W powyższym przykładzie klasa PaymentService samodzielnie tworzy obiekt StripePaymentGateway. Oznacza to, że jest bezpośrednio związana z jego konkretną implementacją.
Takie podejście ma kilka wad:
- Silne powiązanie pomiędzy komponentami (tight coupling).
- Trudniejsza wymiana implementacji, np. Stripe na PayPal.
- Utrudnione testowanie jednostkowe, ponieważ nie można łatwo podstawić mocka.
- Konieczność samodzielnego zarządzania tworzeniem obiektów.
Spring odwraca tę zależność. Zamiast samodzielnie tworzyć wszystkie potrzebne obiekty, klasa otrzymuje je od kontenera Springa.
To właśnie istota IoC – kontrola nad tworzeniem i dostarczaniem zależności zostaje przeniesiona z komponentów aplikacji do kontenera.
Czym jest Dependency Injection (DI)?
Dependency Injection (DI), czyli wstrzykiwanie zależności, to jedna z technik realizacji zasady IoC.
Polega na dostarczaniu obiektowi jego zależności z zewnątrz, zamiast tworzenia ich bezpośrednio wewnątrz klasy.
Wróćmy do poprzedniego przykładu i zmodyfikujmy klasę PaymentService:
public class PaymentService {
private final PaymentGateway paymentGateway;
public PaymentService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public void processPayment(BigDecimal amount) {
paymentGateway.pay(amount);
}
}Tym razem PaymentService nie tworzy samodzielnie obiektu StripePaymentGateway. Otrzymuje zależność poprzez konstruktor.
Co istotne, Dependency Injection nie wymaga korzystania ze Springa. Obiekt można utworzyć również samodzielnie:
PaymentGateway gateway = new StripePaymentGateway();
PaymentService service = new PaymentService(gateway);
service.processPayment(new BigDecimal("100.00"));W tym przypadku to programista odpowiada za utworzenie i przekazanie zależności.
Spring automatyzuje ten proces. Wystarczy zarejestrować odpowiednie komponenty w kontenerze:
@Component
public class StripePaymentGateway implements PaymentGateway {
@Override
public void pay(BigDecimal amount) {
System.out.println("Payment: " + amount);
}
}@Service
public class PaymentService {
private final PaymentGateway paymentGateway;
public PaymentService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public void processPayment(BigDecimal amount) {
paymentGateway.pay(amount);
}
}Podczas tworzenia PaymentService Spring wyszuka odpowiedni komponent implementujący interfejs PaymentGateway i przekaże go do konstruktora.
Warto zauważyć, że adnotacja @Autowired nie jest wymagana, jeżeli klasa posiada tylko jeden konstruktor. Spring automatycznie wykorzysta go do wstrzyknięcia zależności.
Rodzaje Dependency Injection
Spring obsługuje trzy najpopularniejsze sposoby wstrzykiwania zależności.
1. Constructor Injection – wstrzykiwanie przez konstruktor
Zależności są przekazywane podczas tworzenia obiektu.
@Service
public class PaymentService {
private final PaymentGateway paymentGateway;
public PaymentService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}Jest to zalecane rozwiązanie, ponieważ:
- Umożliwia stosowanie pól
final. - Zapewnia dostępność wymaganych zależności już podczas tworzenia obiektu.
- Ułatwia testowanie jednostkowe.
- Pozwala szybko wykrywać brakujące zależności.
2. Setter Injection – wstrzykiwanie przez setter
Zależność jest przekazywana poprzez metodę setter już po utworzeniu obiektu.
@Service
public class PaymentService {
private PaymentGateway paymentGateway;
@Autowired
public void setPaymentGateway(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}Takie podejście może być przydatne w przypadku zależności opcjonalnych lub takich, które powinny mieć możliwość późniejszej zmiany.
3. Field Injection – wstrzykiwanie bezpośrednio do pola
Spring umożliwia również wstrzykiwanie zależności bezpośrednio do pól klasy.
@Service
public class PaymentService {
@Autowired
private PaymentGateway paymentGateway;
}Choć rozwiązanie jest bardzo zwięzłe, nie jest zalecane w nowoczesnych aplikacjach.
Field Injection utrudnia testowanie bez kontenera Springa, ukrywa wymagane zależności klasy oraz uniemożliwia zadeklarowanie wstrzykiwanego pola jako final.
IoC a DI – najważniejsze różnice
| IoC | DI |
|---|---|
| Ogólna zasada projektowania oprogramowania | Konkretna technika realizacji IoC |
| Odwraca odpowiedzialność za sterowanie wybranymi mechanizmami aplikacji | Dostarcza obiektom ich zależności z zewnątrz |
| Może być realizowane na różne sposoby | Może działać bez wykorzystania frameworka |
| W Springu realizowane m.in. przez kontener IoC | W Springu realizowane przez wstrzykiwanie zależności |
Podsumowując, IoC określa zasadę odwrócenia sterowania, natomiast DI jest jednym ze sposobów jej realizacji.
W Spring Framework za automatyczne tworzenie obiektów, zarządzanie nimi oraz dostarczanie zależności odpowiada kontener IoC, którego działanie opiszę w kolejnym rozdziale.
Spring IoC Container i beany
W poprzednim rozdziale omówiłem mechanizmy Inversion of Control (IoC) oraz Dependency Injection (DI). Teraz przyjrzę się komponentowi, który odpowiada za ich realizację w Spring Framework, czyli kontenerowi IoC.
Kontener IoC stanowi centralny element Springa. Odpowiada za tworzenie obiektów, zarządzanie ich zależnościami, konfigurację oraz kontrolowanie ich cyklu życia.
Obiekty zarządzane przez kontener Springa nazywane są beanami.
Czym jest Spring Bean?
Bean to zwykły obiekt Java, który został zarejestrowany w kontenerze Springa i jest przez niego zarządzany.
Warto podkreślić, że nie każdy obiekt Java jest beanem. Podstawowa różnica polega na sposobie jego tworzenia i zarządzania nim.
Przykładowo:
PaymentService service = new PaymentService(paymentGateway);
W tym przypadku obiekt został utworzony bezpośrednio przez programistę. Spring nie zarządza automatycznie jego cyklem życia ani zależnościami.
Natomiast:
@Service
public class PaymentService {
private final PaymentGateway paymentGateway;
public PaymentService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}Jeżeli powyższa klasa zostanie wykryta podczas skanowania komponentów, Spring zarejestruje ją jako bean i będzie odpowiadał za jej utworzenie oraz dostarczenie wymaganych zależności.
Każdy bean posiada swoją definicję (Bean Definition), która zawiera informacje potrzebne kontenerowi, m.in.:
- Typ obiektu.
- Nazwę beana.
- Zakres działania (scope).
- Zależności wymagane do jego utworzenia.
- Sposób inicjalizacji i niszczenia obiektu.
BeanFactory i ApplicationContext
Spring udostępnia dwa podstawowe interfejsy związane z kontenerem IoC: BeanFactory oraz ApplicationContext.
BeanFactory to podstawowy interfejs kontenera Springa. Udostępnia mechanizmy tworzenia beanów, przechowywania ich definicji oraz rozwiązywania zależności. Większość programistów nie ma pojęcia o jego istnieniu i Ty też powinieneś tylko zapamiętać, że coś takiego istnieje.
ApplicationContext rozszerza BeanFactory, oferując dodatkowe funkcjonalności, takie jak:
- Automatyczne wykrywanie i rejestrowanie komponentów.
- Obsługa zdarzeń aplikacyjnych (Application Events).
- Wsparcie dla internacjonalizacji (i18n).
- Integracja z mechanizmami AOP.
- Obsługa środowisk i profili konfiguracyjnych.
W praktyce zdecydowana większość aplikacji Spring korzysta z ApplicationContext, ponieważ oferuje on znacznie więcej możliwości niż podstawowy BeanFactory.ji.
Rejestrowanie beanów w Spring
Spring pozwala rejestrować beany na kilka sposobów. W nowoczesnych aplikacjach najczęściej wykorzystywane są adnotacje @Component oraz @Bean.
1. Rejestrowanie za pomocą @Component
Najprostszym sposobem jest oznaczenie klasy adnotacją @Component:
@Component
public class EmailSender {
public void send(String message) {
System.out.println("Sending email: " + message);
}
}Podczas skanowania komponentów (Component Scanning) Spring wykryje klasę i zarejestruje ją jako bean.
Mechanizm ten jest wykorzystywany również przez adnotacje @Service, @Repository oraz @Controller, dziedziczą po @Component.
2. Rejestrowanie za pomocą @Bean
Drugim sposobem jest jawne zadeklarowanie beana w klasie konfiguracyjnej:
@Configuration
public class AppConfig {
@Bean
public EmailSender emailSender() {
return new EmailSender();
}
}Adnotacja @Configuration informuje Springa, że klasa zawiera konfigurację kontenera, natomiast @Bean wskazuje metodę, której wynik ma zostać zarejestrowany jako bean.
Rozwiązanie to jest szczególnie przydatne, gdy chcemy zarejestrować obiekt pochodzący z zewnętrznej biblioteki, którego klasy nie możemy zmodyfikować.
Oba przedstawione sposoby stanowią alternatywne metody rejestracji tego samego beana – antoacja @Component tworzy go na podstawie klasy, a adnotacja @Bean tworzy go na podstawie wyniku metody.
Cykl życia beana (Bean Lifecycle)
Jedną z istotnych funkcjonalności kontenera Spring jest zarządzanie cyklem życia beanów.
W uproszczeniu cykl życia standardowego beana obejmuje następujące etapy:
- Rejestracja definicji – Spring odczytuje informacje o beanie na podstawie konfiguracji.
- Instancjonowanie – kontener tworzy obiekt.
- Dependency Injection – Spring dostarcza wymagane zależności.
- Inicjalizacja – wykonywane są mechanizmy inicjalizacyjne, w tym metody oznaczone
@PostConstruct. - Gotowość do użycia – bean może być wykorzystywany przez pozostałe komponenty aplikacji.
- Zniszczenie – podczas zamykania kontenera wywoływane są odpowiednie metody porządkowe, np. oznaczone
@PreDestroy.
Spring pozwala programiście wykonywać dodatkowe operacje podczas inicjalizacji i niszczenia beanów.
Przykładowo:
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class CacheService {
@PostConstruct
public void init() {
System.out.println("Initializing cache...");
}
@PreDestroy
public void destroy() {
System.out.println("Clearing cache...");
}
}W powyższym przykładzie:
@PostConstruct– oznacza metodę wykonywaną po utworzeniu obiektu i wstrzyknięciu jego zależności, w trakcie inicjalizacji beana.@PreDestroy– oznacza metodę wykonywaną podczas niszczenia beana przez kontener, np. przy zamykaniu aplikacji.
Zrozumienie cyklu życia beanów jest szczególnie ważne podczas analizy problemów związanych z inicjalizacją aplikacji, zarządzaniem zasobami oraz działaniem mechanizmów AOP.
Zakresy beanów (Bean Scopes)
W Spring Framework każdy bean posiada określony zakres działania (scope), który definiuje, ile instancji danego obiektu może zostać utworzonych oraz jak długo będą one dostępne.
Odpowiedni dobór zakresu ma istotne znaczenie dla zarządzania pamięcią, współbieżności oraz przechowywania stanu aplikacji.
Spring udostępnia sześć podstawowych zakresów:
| Scope | Opis |
|---|---|
| Singleton | Jedna instancja beana w ramach Application Context |
| Prototype | Nowa instancja przy każdym pobraniu beana z kontenera |
| Request | Jedna instancja na żądanie HTTP |
| Session | Jedna instancja na sesję HTTP |
| Application | Jedna instancja na ServletContext (zyli współdzielona jeśli aplikacja ma kilka Application context) |
| WebSocket | Jedna instancja na sesję WebSocket |
Singleton
Singleton jest domyślnym zakresem beanów w Spring Framework. Oznacza to, że kontener tworzy jedną instancję dla danej definicji beana i udostępnia ją wszystkim komponentom, które z niej korzystają.
Przykładowo:
@Service
@Scope("singleton")
public class PaymentService {
public void processPayment() {
System.out.println("Processing payment...");
}
}Adnotacja @Scope("singleton") jest opcjonalna, ponieważ singleton to domyślny zakres.
Jeżeli dwie różne klasy korzystają z PaymentService, obie otrzymają referencję do tej samej instancji beana.
Warto jednak pamiętać, że singleton w Springu nie jest tym samym co klasyczny wzorzec projektowy Singleton.
W klasycznym wzorcu Singleton klasa samodzielnie kontroluje tworzenie swojej jedynej instancji, np. poprzez prywatny konstruktor i statyczną metodę getInstance(). Ta technika blokuje możliwość stworzenia więcej niż jednej instancji danej klasy. W Springu wygląda to trochę inaczej, bo singleton jest nałożony na konkretny bean, a nie na klasę. Oznacza to, że jeśli zarejestrujesz beany za pomocą np. adnotacji @Bean, które będą nałozone na dwie różne metody zwracające ten sam typ klasy, to Spring wciąż zarejestruje dwa różne singletony.
Istotną kwestią jest również bezpieczeństwo wielowątkowe (thread safety). Beany singleton są współdzielone przez wiele wątków, dlatego nie powinny przechowywać zmiennego stanu związanego z pojedynczym żądaniem użytkownika.
Prototype
W przypadku zakresu Prototype Spring tworzy nową instancję beana za każdym razem, gdy jest ona pobierana z kontenera.
@Component
@Scope("prototype")
public class ReportGenerator {
public ReportGenerator() {
System.out.println("Creating ReportGenerator");
}
}Można to sprawdzić, pobierając bean dwukrotnie:
ReportGenerator report1 =
context.getBean(ReportGenerator.class);
ReportGenerator report2 =
context.getBean(ReportGenerator.class);
System.out.println(report1 == report2); // falseKażde wywołanie getBean() powoduje utworzenie nowego obiektu.
Prototype sprawdza się w przypadku komponentów, które przechowują własny stan i powinny być tworzone niezależnie od siebie.
Warto jednak zwrócić uwagę na dwie istotne kwestie:
- Spring odpowiada za utworzenie i inicjalizację beana, ale nie zarządza automatycznie jego zniszczeniem. W konsekwencji metoda oznaczona
@PreDestroynie zostanie automatycznie wywołana. - Wstrzyknięcie beana Prototype do Singletona nie oznacza automatycznego tworzenia nowej instancji przy każdym wywołaniu metody Singletona.
Drugi przypadek może być szczególnie mylący.
Załóżmy, że ReportGenerator jest beanem Prototype:
@Service
public class ReportService {
private final ReportGenerator generator;
public ReportService(ReportGenerator generator) {
this.generator = generator;
}
}Ponieważ ReportService jest Singletonem, zależność ReportGenerator zostanie wstrzyknięta tylko raz – podczas tworzenia ReportService.
Jeśli potrzebuję nowej instancji ReportGenerator przy każdym wywołaniu metody, mogę wykorzystać ObjectProvider:
@Service
public class ReportService {
private final ObjectProvider<ReportGenerator> provider;
public ReportService(ObjectProvider<ReportGenerator> provider) {
this.provider = provider;
}
public void generateReport() {
ReportGenerator generator = provider.getObject();
// Generowanie raportu
}
}Teraz każde wywołanie provider.getObject() zwróci nową instancję ReportGenerator.
Request, Session, Application i WebSocket
Pozostałe cztery zakresy są związane z aplikacjami webowymi i wymagają odpowiednio skonfigurowanego kontekstu Spring obsługującego środowisko webowe.
Request Scope
Bean istnieje w ramach pojedynczego żądania HTTP. Każde nowe żądanie otrzymuje własną instancję obiektu.
@Component
@RequestScope
public class RequestContext {
private final String requestId = UUID.randomUUID().toString();
public String getRequestId() {
return requestId;
}
}Scope ten jest przydatny do przechowywania informacji specyficznych dla danego żądania, np. identyfikatora wykorzystywanego podczas logowania.
Session Scope
Bean jest tworzony dla konkretnej sesji HTTP i pozostaje dostępny przez cały czas jej trwania.
@Component
@SessionScope
public class ShoppingCart {
private final List<String> products = new ArrayList<>();
public void addProduct(String product) {
products.add(product);
}
}Przykładowym zastosowaniem jest koszyk zakupowy, którego zawartość powinna być niezależna dla poszczególnych użytkowników.
Application Scope
Bean jest współdzielony w ramach jednego ServletContext, czyli kontekstu aplikacji webowej.
@Component
@ApplicationScope
public class ApplicationStatistics {
}W przeciwieństwie do Singletona, którego zasięg obejmuje pojedynczy kontener Spring, Application Scope jest powiązany z ServletContext. Różnica ta ma znaczenie zwłaszcza wtedy, gdy jedna aplikacja webowa wykorzystuje kilka kontenerów Spring.
WebSocket Scope
Bean jest powiązany z konkretną sesją WebSocket i pozostaje dostępny przez czas jej trwania.
Może być wykorzystywany do przechowywania stanu połączenia, np. informacji o użytkowniku uczestniczącym w komunikacji w czasie rzeczywistym.
Który zakres wybrać?
W większości typowych aplikacji Spring Boot dominują beany Singleton. Dotyczy to przede wszystkim serwisów, repozytoriów oraz komponentów implementujących logikę biznesową.
Pozostałe zakresy powinny być stosowane wtedy, gdy wymagają tego konkretne potrzeby aplikacji.
Najważniejsze adnotacje Spring Framework
Jedną z największych zalet Spring Framework jest możliwość konfigurowania aplikacji za pomocą adnotacji. Dzięki nim programista może deklarować komponenty, definiować zależności, zarządzać ich cyklem życia oraz kontrolować sposób działania aplikacji bez konieczności tworzenia rozbudowanych plików konfiguracyjnych XML (tak jak kiedyś).
W nowoczesnych aplikacjach Spring Boot konfiguracja oparta na adnotacjach jest standardowym rozwiązaniem.
Adnotacje służące do definiowania komponentów
Spring udostępnia kilka adnotacji pozwalających rejestrować klasy jako beany. Każda z nich wskazuje również rolę, jaką dany komponent pełni w aplikacji.
| Adnotacja | Zastosowanie |
|---|---|
@Component | Ogólny komponent zarządzany przez Spring |
@Service | Komponent zawierający logikę biznesową |
@Repository | Komponent odpowiedzialny za dostęp do danych |
@Controller | Kontroler obsługujący żądania HTTP w Spring MVC |
@RestController | Kontroler REST API, którego metody domyślnie zwracają dane w treści odpowiedzi |
Wszystkie powyższe adnotacje są bezpośrednio lub pośrednio oparte na @Component, dzięki czemu Spring może wykrywać oznaczone nimi klasy podczas skanowania komponentów (Component Scanning).
Przykładowo:
@Repository
public class UserRepository {
public User findById(Long id) {
// Pobranie użytkownika z bazy danych
}
}@Service
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
public User getUser(Long id) {
return repository.findById(id);
}
}@RestController
@RequestMapping("/users")
public class UserController {
private final UserService service;
public UserController(UserService service) {
this.service = service;
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return service.getUser(id);
}
}W przedstawionym przykładzie Spring automatycznie wykryje wszystkie trzy komponenty i utworzy ich instancje. Następnie wstrzyknie UserRepository do UserService, a UserService do UserController.
Warto zaznaczyć, że @Service technicznie nie różni się od @Component. Stosuje się ją tylko po to aby kod był bardziej czytelny. Natomiast @Repository może również informować Springa, że wyjątki zgłaszane przez ten komponent powinny być tłumaczone na wyjątki Springa DataAccessException. Ułatwia to obslugę błędów.
@Configuration i @Bean
Konfiguracja beanu za pomocą tych adnotacji została opisana we wcześniejszym akapicie, więc nie będę się tu powtarzał. Są to adnotacje, które po prostu służą do alternatywnej rejestracji beanów, bez użycia pozostałych adnotacji typu @Component. Z reguły jest to konieczna technika, gdy integrujesz Spring z zewnętrzną biblioteką, bo wtedy nie może zarejestrować bean w klasyczny sposób.
@Autowired, @Qualifier i @Primary
Spring udostępnia również adnotacje umożliwiające kontrolowanie procesu wstrzykiwania zależności.
@Autowired informuje kontener o konieczności dostarczenia odpowiedniej zależności.
@Service
public class PaymentService {
private final PaymentGateway gateway;
@Autowired
public PaymentService(PaymentGateway gateway) {
this.gateway = gateway;
}
}Jeżeli klasa posiada tylko jeden konstruktor, adnotacja @Autowired nie jest wymagana. Spring automatycznie wykorzysta go do wstrzyknięcia zależności.
Problem pojawia się jednak wtedy, gdy w kontenerze istnieje kilka beanów implementujących ten sam interfejs.
Załóżmy, że mamy dwie implementacje PaymentGateway:
@Component
public class StripeGateway implements PaymentGateway {
// Implementacja
}@Component
public class PayPalGateway implements PaymentGateway {
// Implementacja
}W takiej sytuacji Spring nie może jednoznacznie określić, którą implementację należy wstrzyknąć do PaymentService, jeśli nie ma dodatkowych wskazówek.
Możemy rozwiązać ten problem na dwa sposoby.
1. Rozwiązanie: @Qualifier – wskazanie konkretnego beana
@Service
public class PaymentService {
private final PaymentGateway gateway;
public PaymentService(
@Qualifier("stripeGateway") PaymentGateway gateway) {
this.gateway = gateway;
}
}Adnotacja @Qualifier pozwala zawęzić wybór do określonego beana. Szczególne przydatne, gdy stosujesz wzorzec strategii w swojej aplikacji.
2. Rozwiązanie @Primary – wskazanie domyślnej implementacji
@Component
@Primary
public class StripeGateway implements PaymentGateway {
// Implementacja
}Adnotacja @Primary nadaje beanowi pierwszeństwo podczas automatycznego wstrzykiwania pojedynczej zależności, jeżeli istnieje kilka pasujących kandydatów.
@Scope, @Lazy i @Value
Kolejne adnotacje pozwalają kontrolować sposób tworzenia beanów oraz ich konfigurację.
@Scope
Definiuje zakres działania beana. Jak opisałem we wcześniejszym rozdziale, domyślnym zakresem jest Singleton.
@Component
@Scope("prototype")
public class ReportGenerator {
}W tym przypadku Spring utworzy nową instancję ReportGenerator za każdym razem, gdy bean zostanie pobrany z kontenera.
@Lazy
Domyślnie Spring tworzy większość singletonów podczas inicjalizacji kontekstu aplikacji.
Adnotacja @Lazy pozwala opóźnić inicjalizację beana do momentu, gdy będzie rzeczywiście potrzebny.
@Component
@Lazy
public class ExpensiveService {
public ExpensiveService() {
System.out.println("Initializing service...");
}
}Mechanizm ten może być przydatny w przypadku komponentów, których inicjalizacja jest kosztowna.
Warto jednak pamiętać, że jeśli taki bean zostanie wstrzyknięty jako wymagana zależność do innego, inicjalizowanego od razu singletona, również może zostać utworzony podczas uruchamiania aplikacji.
@Value
Umożliwia wstrzykiwanie wartości konfiguracyjnych, np. z pliku application.properties.
payment.currency=USD
@Service
public class PaymentService {
@Value("${payment.currency}")
private String currency;
}Spring odczyta właściwość payment.currency i przypisze jej wartość do pola currency.
Adnotacja @Value obsługuje również wartości domyślne oraz wyrażenia SpEL (Spring Expression Language).
@Profile – konfiguracja zależna od środowiska
Adnotacja @Profile pozwala określić, w jakich środowiskach dany bean powinien zostać zarejestrowany.
Jest to szczególnie przydatne w aplikacjach posiadających osobne konfiguracje dla środowisk developerskich, testowych i produkcyjnych.
Przykładowo:
@Component
@Profile("dev")
public class MockEmailSender implements EmailSender {
@Override
public void send(String message) {
System.out.println(message);
}
}@Component
@Profile("prod")
public class SmtpEmailSender implements EmailSender {
@Override
public void send(String message) {
// Wysyłanie wiadomości przez SMTP
}
}Jeżeli uruchomimy aplikację z aktywnym profilem dev, Spring zarejestruje MockEmailSender. Natomiast dla profilu prod zostanie zarejestrowany SmtpEmailSender.
Profil można aktywować np. za pomocą właściwości:
spring.profiles.active=dev
Dzięki temu możliwe jest dostosowanie zachowania aplikacji do środowiska bez konieczności modyfikowania kodu źródłowego.
Podsumowanie reszty adnotacji
Przedstawione adnotacje stanowią podstawę codziennej pracy ze Spring Framework. Pozwalają zarządzać strukturą aplikacji oraz konfiguracją komponentów w deklaratywny sposób.
Spring wykorzystuje jednak adnotacje również do implementowania bardziej zaawansowanych mechanizmów, takich jak programowanie aspektowe (AOP), które omówię w kolejnych rozdziałach.
| Adnotacja | Główne zastosowanie |
|---|---|
@Configuration | Definiowanie konfiguracji Springa |
@Bean | Jawne rejestrowanie beanów, nakładane na metodę a nie klasę. |
@Autowired | Wstrzykiwanie zależności za pomocą pól lub setterów |
@Qualifier | Wybór konkretnego beana |
@Primary | Wskazanie preferowanego beana |
@Scope | Określenie zakresu działania beana |
@Lazy | Opóźnienie inicjalizacji beana |
@Value | Wstrzykiwanie wartości z pliku konfiguracyjnego aplikacji |
@Profile | Warunkowa rejestracja beanów zależnie od aktywnego profilu |
Aspect-Oriented Programming (AOP)
Aspect-Oriented Programming (AOP), czyli programowanie aspektowe, to paradygmat programowania pozwalający oddzielić logikę biznesową aplikacji od funkcjonalności technicznych, które powtarzają się w wielu jej komponentach.
Wyobraźmy sobie aplikację, w której chcemy mierzyć czas wykonania metod serwisowych. W tradycyjnym podejściu musielibyśmy dodać odpowiedni kod do każdej metody:
@Service
public class OrderService {
public void processOrder() {
long start = System.nanoTime();
// Logika biznesowa
long duration = System.nanoTime() - start;
System.out.println("Execution time: " + duration);
}
}Jeśli aplikacja zawiera kilkadziesiąt serwisów, powielanie takiego kodu staje się problematyczne. Co więcej, logika odpowiedzialna za pomiar czasu nie jest bezpośrednio związana z przetwarzaniem zamówień.
Takie funkcjonalności określa się jako Cross-Cutting Concerns, czyli zagadnienia przekrojowe. Do najczęstszych należą:
- Logowanie i monitorowanie działania aplikacji.
- Pomiar czasu wykonywania metod.
- Obsługa transakcji.
- Kontrola dostępu i autoryzacja.
- Mechanizmy cache’owania.
Wzorzec projektowy Proxy
Aby zrozumieć sposób działania Spring AOP, warto najpierw przyjrzeć się wzorcowi projektowemu Proxy (pełnomocnik), który stanowi podstawę jego implementacji.
Proxy to strukturalny wzorzec projektowy polegający na utworzeniu obiektu pośredniczącego pomiędzy klientem a właściwym obiektem. Zamiast wywoływać metodę bezpośrednio, klient komunikuje się z obiektem proxy, który może wykonać dodatkowe operacje przed przekazaniem wywołania do właściwego komponentu lub po jego zakończeniu.
Wyobraźmy sobie serwis odpowiedzialny za przetwarzanie zamówień:
public interface OrderService {
void processOrder();
}public class OrderServiceImpl implements OrderService {
@Override
public void processOrder() {
System.out.println("Processing order...");
}
}Załóżmy, że chcemy mierzyć czas wykonywania metody processOrder(), ale bez modyfikowania jej implementacji.
Możemy w tym celu utworzyć klasę proxy:
public class OrderServiceProxy implements OrderService {
private final OrderService target;
public OrderServiceProxy(OrderService target) {
this.target = target;
}
@Override
public void processOrder() {
long start = System.nanoTime();
try {
target.processOrder();
} finally {
long duration = System.nanoTime() - start;
System.out.println("Execution time: " + duration);
}
}
}Teraz zamiast korzystać bezpośrednio z OrderServiceImpl, możemy posłużyć się obiektem pośredniczącym:
OrderService service = new OrderServiceProxy(
new OrderServiceImpl()
);
service.processOrder();Co to tak nam naprawdę dało? W ten sposób udało się wyrzucić powtarzalną logikę, odpowiadającą za logowanie czasu, do innej klasy (proxy). Tak naprawdę możnaby napisać nawet bardziej ogólne proxy, które działałoby nie tylko dla Orderu.
Jak Spring wykorzystuje wzorzec Proxy?
Spring AOP automatyzuje przedstawiony mechanizm. Zamiast samodzielnie tworzyć klasy takie jak OrderServiceProxy, programista definiuje aspekt określający, jakie dodatkowe operacje powinny zostać wykonane i które metody mają zostać nimi objęte.
Podczas tworzenia beanów Spring może wygenerować odpowiednie obiekty proxy, które przechwytują wywołania metod i uruchamiają zdefiniowaną logikę aspektową.
Podstawowe pojęcia AOP
Spring AOP wykorzystuje kilka podstawowych pojęć, które opisują sposób definiowania aspektów oraz przechwytywania wywołań metod.
Aspect (aspekt)
Aspekt to klasa zawierająca logikę przekrojową, która ma być wykonywana niezależnie od głównej logiki biznesowej aplikacji. W Springu definiuje się ją za pomocą adnotacji @Aspect, a do jej automatycznego wykrycia i zarejestrowania jako beana można wykorzystać @Component.
Pointcut (punkt przecięcia)
Pointcut to definicja określająca, które wykonania metod mają zostać objęte działaniem aspektu.
W Spring AOP najczęściej definiuje się go za pomocą wyrażenia Pointcut Expression, które pozwala wskazać metody na podstawie ich nazw, typów argumentów, pakietów lub adnotacji.
Pointcut można umieścić bezpośrednio w adnotacji określającej rodzaj Advice albo zdefiniować jako osobną metodę oznaczoną @Pointcut, dzięki czemu można wykorzystać tę samą definicję w wielu miejscach.
Advice (porada)
Advice to metoda wewnątrz klasy aspektu, która zawiera kod wykonywany w związku z przechwyconym wywołaniem.
Metoda ta jest oznaczana odpowiednią adnotacją, np. @Before, @After lub @Around, która określa moment wykonania dodatkowej logiki.
Advice może zostać wykonane przed metodą biznesową, po jej zakończeniu, w przypadku wystąpienia wyjątku lub wokół całego wywołania. W przypadku @Around może również kontrolować, czy metoda docelowa zostanie w ogóle uruchomiona.
Join Point (punkt połączenia)
Join Point to określony punkt podczas wykonywania programu, w którym może zostać uruchomiona logika aspektowa.
Target Object (obiekt docelowy)
Target Object to właściwy obiekt, którego metody są objęte działaniem aspektu. Najczęściej jest to bean Springa zawierający logikę biznesową, np. serwis lub repozytorium.
Obiekt docelowy nie musi wiedzieć o istnieniu aspektu ani zawierać kodu odpowiedzialnego za jego obsługę. Dodatkowa logika jest wykonywana przez mechanizm AOP podczas przechwytywania wywołań.
AOP Proxy (obiekt pośredniczący)
AOP Proxy to obiekt tworzony przez Springa, który pośredniczy pomiędzy komponentem wywołującym metodę a właściwym obiektem docelowym.
Proxy odpowiada za przechwycenie wywołania, uruchomienie odpowiednich Advice oraz przekazanie sterowania do metody docelowej zgodnie z ustalonym łańcuchem wywołań.
Praktyczny przykład AOP w Spring
Załóżmy, że chcemy automatycznie mierzyć czas wykonywania wybranych metod kontrolera REST API. W tym celu wykorzystam własną adnotację @TrackExecutionTime, która będzie wskazywać metody objęte pomiarem czasu.
1. Definicja własnej adnotacji
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackExecutionTime {
}Adnotacja @Target(ElementType.METHOD) określa, że może być stosowana wyłącznie na metodach, natomiast @Retention(RetentionPolicy.RUNTIME) sprawia, że będzie dostępna podczas działania aplikacji.
2. Implementacja aspektu
@Aspect
@Component
public class ExecutionTimeAspect {
private static final Logger log =
LoggerFactory.getLogger(ExecutionTimeAspect.class);
@Pointcut("@annotation(com.example.aop.TrackExecutionTime)")
public void trackedMethods() {
}
@Around("trackedMethods()")
public Object measureExecutionTime(
ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
try {
return joinPoint.proceed();
} finally {
long duration = System.nanoTime() - start;
log.info("Method {} executed in {} ms",
joinPoint.getSignature().toShortString(),
duration / 1_000_000.0);
}
}
}W powyższym przykładzie:
@Aspect– oznacza klasę jako aspekt.@Component– rejestruje aspekt jako bean Springa.@Pointcut– definiuje, które metody mają zostać przechwycone. W tym przypadku są to metody oznaczone@TrackExecutionTime.@Around– określa Advice, czyli metodę zawierającą logikę pomiaru czasu.ProceedingJoinPoint– reprezentuje przechwycone wywołanie metody i pozwala uzyskać informacje na jego temat.joinPoint.proceed()– kontynuuje wykonanie przechwyconej metody.
3. Wykorzystanie adnotacji w kontrolerze
@RestController
@RequestMapping("/orders")
public class OrderController {
@TrackExecutionTime
@GetMapping("/{id}")
public String getOrder(@PathVariable Long id) {
return "Order ID: " + id;
}
@GetMapping("/status")
public String getStatus() {
return "OK";
}
}Zwróćmy uwagę, że tylko metoda getOrder() została oznaczona adnotacją @TrackExecutionTime.
Po wykonaniu żądania:
GET /orders/123
Spring AOP przechwyci wykonanie metody getOrder(), a w logach aplikacji pojawi się przykładowo:
Method OrderController.getOrder(..) executed in 0.5 ms
Natomiast wywołanie:
GET /orders/status
nie uruchomi naszego aspektu, ponieważ metoda getStatus() nie posiada adnotacji @TrackExecutionTime.
4. Jak działa cały mechanizm?
Podczas inicjalizacji aplikacji Spring analizuje zdefiniowany aspekt i tworzy odpowiednie proxy dla beana OrderController.
Gdy nadchodzi żądanie HTTP, Spring MVC wywołuje metodę kontrolera poprzez proxy, które uruchamia nasz Advice.
Metoda measureExecutionTime() zapisuje czas rozpoczęcia operacji, następnie wywołuje joinPoint.proceed(), a po zakończeniu metody oblicza czas jej wykonania i zapisuje wynik w logach.
Warto zaznaczyć, że aspekt mierzy czas wykonania metody kontrolera, a nie całkowity czas obsługi żądania HTTP. Nie uwzględnia więc np. późniejszej serializacji odpowiedzi do JSON.
Dzięki wykorzystaniu własnej adnotacji możemy w prosty sposób rozszerzać zachowanie dowolnych metod zarządzanych przez Springa, bez konieczności modyfikowania ich logiki biznesowej.
Podsumowanie
Spring Framework to jeden z najważniejszych elementów ekosystemu Java, który znacząco upraszcza tworzenie i rozwijanie aplikacji. Jego fundament stanowią mechanizmy Inversion of Control (IoC) oraz Dependency Injection (DI), dzięki którym zarządzanie obiektami i ich zależnościami zostaje przeniesione do kontenera Springa.
W artykule omówiłem również pojęcie beanów, ich cykl życia, dostępne zakresy działania oraz najważniejsze adnotacje wykorzystywane podczas konfiguracji aplikacji. Przyjrzałem się także mechanizmowi Aspect-Oriented Programming (AOP), który pozwala oddzielić logikę przekrojową od biznesowej dzięki wykorzystaniu wzorca projektowego Proxy.
Zrozumienie tych mechanizmów pozwala nie tylko sprawniej korzystać ze Springa, ale również łatwiej diagnozować problemy związane z konfiguracją, wstrzykiwaniem zależności czy działaniem komponentów aplikacji. Stanowi także solidną podstawę do poznawania kolejnych modułów ekosystemu Spring, takich jak Spring Data, Spring MVC czy Spring Security.
