Programowanie aspektowe w języku Java, czyli AspectJ w praktyce. Co to jest?

Czas czytania: 6 minut

Język Java pod wieloma względami jest naprawdę uniwersalny i pozwala zastosować wiele różnorodnych technik, które w znacznym stopniu pozwalają uprościć proces tworzenia aplikacji. Tematem dzisiejszego wpisu jest biblioteka AspectJ i związane z nią programowanie aspektowe. Dowiemy się np.: jak w przezroczysty sposób mierzyć czas wywołania metod? Jak zaimplementować prosty, przezroczysty dla programisty system uprawnień? Jeśli chciałbyś poznać odpowiedzi na powyższe pytania, przeczytaj 🙂

Co to jest programowanie aspektowe?

Już w pierwszych linijkach tego wpisu pojawiło się nowe pojęcie: „programowanie aspektowe”. Co to u licha jest? Spójrzmy na definicję:

Programowanie aspektowe (aspect-oriented programming, AOP) to paradygmat tworzenia programów komputerowych wspomagający separację zagadnień i rozdzielenie programu na części w jak największym stopniu niezwiązane funkcjonalnie.

Definicja definicją, ale co ona w sumie oznacza? Wyobraźmy sobie prosty program, który realizuje zapis informacji do bazy. Cała operacja musi odbyć się w jednej transakcji bazodanowej, gdyż dane muszą zachować spójność. Jak taka operacja mogłaby wyglądać?

Spójrz na powyższy listing. Właściwa logika zapisu do bazy danych znajdowałaby się między linijkami 4 a 8. Cała reszta to tzw. boilerplate, który w każdym wypadku będzie taki sam.

Jak wyglądała obsługa transakcji gdy używamy frameworka Spring? Bajecznie prosto.

Nad metodą, która ma być realizowana wewnątrz transakcji dodajemy adnotację @Transactional. To właśnie ta adnotacja dba o całościowy proces obsługi transakcji.

Obsługa adnotacji @Transactional, została stworzona za pomocą aspektów. Kod źródłowy takiego aspektu możesz obejrzeć przechodząc pod ten adres. Poniżej znajduje się fragment listingu, który zawiera tzw. Pointcuty o których powiemy sobie co nieco w dalszej części artykułu.

W jaki sposób działają aspekty? Podczas uruchamiania aplikacji, aspekt jest „doczepiany” do konkretnych metod. O tym, do których metod oraz w którym momencie aspekt zostanie przypięty, decyduje tzw Pointcut oraz rodzaj aspektu. Procedura obsługi aspektu może być wywoływana przed, w trakcie lub po wywołaniu danej metody. Za pomocą aspektów możemy wykonać wiele naprawdę ciekawych rzeczy, które w istotny sposób uproszczą i uprzyjemnią proces tworzenia oprogramowania.

W runtime, wykonanie aspektu można zobrazować w następujący sposób:

Wymagane zależności – Maven

W celu dodania mechanizmu aspektów do naszego programu, musimy dodać kilka zależności. Ich lista znajduje się na listingu poniżej:

Warto także nad główną klasą naszej aplikacji springowej umieścić adnotację @EnableAspectJAutoProxy.

Najprostszy aspekt – pomiar czasu wykonania metody

Wyobraźmy sobie, że mamy jakąś aplikację, która ma problemy natury wydajnościowej. Aby móc dowiedzieć się, co jest „wąskim gardłem” takiej aplikacji, musimy wiedzieć ile czasu zajmuje wywołanie poszczególnych metod. W jaki sposób możemy to zrobić?

Najprostszy sposób to dodanie pomiaru czasu przed i po wywołaniu metod. Lecz ręczne i jawne zakodowanie takiego pomiaru nie zawsze jest najbardziej czytelne i najszybsze. Bo co zrobić w sytuacji, kiedy chcemy dodać taki pomiar do kilkuset różnych metod? No właśnie. W tym miejscu z pomocą przychodzą aspekty. Projekt, na bazie którego będziemy pracowali znajduje się tutaj.

Najpierw spójrzmy, jak wygląda nasza implementacja serwisów.

Nie ma tu nic skomplikowanego. Mamy cztery serwisy, które realizują jakąś bliżej nieznaną logikę. Chcemy, aby nasz aspekt logował czas wykonania każdej z tych metod. Na samym końcu znajduje się metoda addUser(UserData userData). Dla niej napiszemy aspekt, który pozwoli zalogować argument wejściowy.

Zdefiniujmy, w którym momencie powinien wywoływać się aspekt. Chcemy dokonać pomiaru czasu wywołania każdej publicznej metody. Aspekt powinien wykonywać się tylko dla naszych serwisów. (Nie potrzebujemy pomiaru czasu wykonania metod pochodzących z używanych przez nas bibliotek. Nie potrzebujemy także pomiaru czasu wykonania repozytoriów lub innych klas które nie są serwisem). Spójrz na poniższy listing.

W linijce 6 zdefiniowaliśmy Pointcut. Pointcut informuje o tym, jakie warunki musi spełniać metoda aby aspekt mógł zostać wywołany. Przyjrzyjmy się jemu dokładniej.

„execution” oznacza, że aspekt zostanie uruchomiony dla każdej metody, której sygnatura spełnia odpowiednie warunki. Argument dla „execution” ma następującą postać:

execution(rodzaj_metody zwracany_argument pakiet.nazwa_klasy.nazwa_metody(argumenty)

Wobec powyższego, zapisany w powyższym listingu Pointcut możemy przetłumaczyć w następujący sposób: „Uruchom aspekt dla każdej publicznej metody zwracającej dowolne dane, która znajduje się w pakiecie pl.kompikownia.blog.aspect.timemeasurement.service i przyjmuje dowolne argumenty.

Metoda runMeasure zawiera logikę aspektu. Zwróć uwagę, że ma ona specyficzną sygnaturę i oznaczona jest adnotacją @Around. Adnotacja @Around oznacza, że możemy dodać dodatkowy kod zarówno przed jak i po wywołaniu metody. Metoda oznaczona adnotacją @Around musi zwracać Object oraz przyjmować argument typu ProceedingJoinPoint. W ProceedingJoinPoint znajduje się wszystko co potrzebne do ustalenia, jaka metoda została wywołana, z jakimi argumentami itd.

W aspekcie wykonujemy pomiar czasu. Poza pomiarem czasu zapisujemy moment wywołania i zakończenia wykonania metody. Zwróć uwagę na linijkę 13, w której znajduje się kod pjp.proceed(); Ta linijka wywołuje metodę, której wywołanie przechwyciliśmy za pomocą aspektu. Wszystko to, co znajduje się przed tą linijką dzieje się przed wywołaniem metody. Wszystko to co za – po zakończeniu wykonywania przechwyconej metody.

Jeśli używasz środowiska Intellij IDEA, możesz w bardzo prosty sposób sprawdzić, czy prawidłowo napisałeś aspekt. Obok każdego utworzonego aspektu pojawia się specjalna ikona. Jeśli na nią klikniesz, wyświetlona zostanie lista wszystkich metod do których aspekt zostanie doczepiony.

Wynik działania aspektu przedstawiony został poniżej.

Jak widzisz, został zalogowany moment wywołania metody (dzień, czas), moment zakończenia wykonania metody oraz łączny czas, jaki poświęcono na jej wykonanie. (6292 ms).

Możesz uruchomić ten przykład na swoim komputerze. Projekt znajduje się w tym miejscu.

Aspekt logujący argumenty metody

Zwiększymy nieco poziom trudności. Naszym następnym celem jest stworzenie aspektu, który zaloguje argumenty, z jakimi została wywołana metoda. Będziemy chcieli też zalogować rezultat wykonania metody. Aspekt będziemy chcieli wywołać dla wszystkich metod, które zawierają co najmniej jeden argument*.

Spójrz na poniższy listing, który zawiera rzeczony aspekt.

Zacznijmy od początku. Przyjrzyj się linijkom 22-23. Stworzyliśmy nowy Pointcut. Zmieniliśmy fragment odpowiedzialny za argumenty metody. Zamiast (..) wstawiliśmy (Object+,..). Co to oznacza? Pierwszym argumentem metody musi być klasa Object lub klasa dziedzicząca po Object (+). Kolejne argumenty mogą być dowolne. Dzięki temu aspekt zostanie uruchomiony dla dowolnych metod, które zawierają co najmniej jeden argument.

W ciele metody korzystamy z ProceedingJoinPoint w taki sposób, aby odczytać argumenty oraz rezultat wywołania metody.

Przykład działania programu możesz zobaczyć poniżej.

Zagadka: Jaką sygnaturę musi mieć metoda, która przyjmuje co najmniej jeden argument ale nie zostanie dla niej wywołany powyższy aspekt?

Rozwiązanie zagadki
Powyższy aspekt nie zostanie uruchomiony dla metod, które jako pierwszy argument przyjmują typ prosty (int, char itd). Wynika to z faktu, że typy proste nie dziedziczą po klasie Object.

Pełny kod źródłowy programu, który został przedstawiony powyżej znajduje się tutaj.

Realne zastosowanie aspektów – weryfikacja uprawnień

Spróbujmy teraz napisać sobie aspekt, który obsłuży stworzoną przez nas adnotację. Nazwijmy ją @CheckPermission. Jako argument będzie przyjmowała nazwę uprawnienia. Będziemy chcieli stworzyć aspekt, który będzie uruchamiał się dla każdej metody oznaczonej tą adnotacją. Jeśli użytkownik ma uprawnienia do uruchomienia metody – uruchomimy ją. W przeciwnym wypadku zwrócimy wyjątek. Najpierw napiszmy definicję adnotacji.

Jest to standardowa adnotacja, uruchamiana w Runtime (RetentionPolicy.RUNTIME). Adnotacja może być umieszczona tylko nad metodami (ElementType.METHOD). Zajmijmy się teraz aspektem.

Najpierw spójrzmy na pointcut znajdujący się w linijce 12. Oprócz execution użyliśmy nowego typu: @annotation. W nawiasie podaliśmy, dla jakiego rodzaju adnotacji pointcut ma się wykonywać. Spójrz teraz na linijkę 15. Przede wszystkim, zamiast @Around użyliśmy adnotacji @Before. Oznacza ona, że aspekt zostanie uruchomiony przez wywołaniem metody spełniającej warunki. Zerknij teraz na argument tej adnotacji. Możesz zobaczyć, jak można łączyć różne Pointcuty ze sobą. W tym wypadku aspekt zostanie uruchomiony dla każdej publicznej metody, która została oznaczona adnotacją @CheckPermission.

W treści metody obsługującej pointcut możesz zobaczyć, jak można z obiektu JointPoint wyciągnąć informację o metodzie, dla której został wywołany aspekt. W tym wypadku odczytujemy adnotację i sprawdzamy jej argumenty. Na ich podstawie decydujemy, czy należy wyrzucić wyjątek czy nic nie robić. (W przypadku @Before oznacza to, że metoda zostanie normalnie wykonana).

Jak wygląda użycie naszej stworzonej przed chwilą adnotacji? Bardzo prosto! Spójrz na poniższy listing.

Kod powyższej aplikacji dostępny jest w tym miejscu.

Podsumowując

Gdzie warto używać aspektów?

Aspektów warto używać przede wszystkim w zastosowaniach poruszonych w tym artykule. Aspektów używa się do realizacji typowo „technicznego” kodu, takiego jak transakcyjność, który mógłby zmniejszać czytelność kodu realizującego biznesową funkcjonalność. Mechanizmu tego warto także używać w celu obsługi logowania różnych informacji podczas działania aplikacji. Ostatnim elementem, gdzie często można spotkać aspekty jest szeroko pojęte Security – obsługa uprawnień, kontekstu użytkownika itd.

Gdzie nie warto używać aspektów?

Przede wszystkim – nie powinniśmy ich nadużywać 🙂 Jeśli wewnątrz aspektów będziemy umieszczali kod, który mógłby zostać zrealizowany w postaci zwykłej metody, nasz kod stanie się nie bardziej, a mniej czytelny. Aspekty jakby nie patrzeć nieco zaburzają tradycyjny przepływ sterowania w programie, o czym należy pamiętać.

Aspekty są naprawdę ciekawym mechanizmem, który warto bliżej poznać. Przydają się one do wykonania operacji które wykonują się „obok” lub „pomiędzy” kodem biznesowym.

Poniżej znajduje się lista linków, pod którymi możesz dowiedzieć się nieco więcej o tym mechaniźmie:

Jeśli artykuł ci się podobał, okaż to 🙂 Polub mój fanpage na FB oraz zapisz się na newsletter (wszystko znajduje się po prawej stronie). Dzięki newsletterowi co jakiś czas otrzymasz informacje o nowościach na blogu, a także pewne unikalne treści, których nie zdobędziesz w inny sposób 🙂 Zachęcam także do włączenia powiadomień. Możesz to zrobić, klikając czerwono-biały dzwoneczek w prawej dolnej części ekranu.

Jeśli zauważysz jakiś błąd, będziesz miał problem – nie bój się, napisz komentarz. Na pewno znajdziemy jakieś rozwiązanie.

Do zobaczenia 🙂

2 komentarze do “Programowanie aspektowe w języku Java, czyli AspectJ w praktyce. Co to jest?

  1. laptopy Odpowiedz

    Jeśli chodzi o tego typu programowanie, to jest to naprawę skomplikowana sprawa, jeśli nie wiemy, jak się tego nauczyć.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.