Wyślij zapytanie Dołącz do Sii

Często przed programistami, oprócz realizacji głównego zadania programistycznego, pojawia się konieczność implementacji dodatkowych wymagań lub założeń technicznych. Odpowiednim przykładem obrazującym taki problem może być konieczność dodania do naszego kodu fragmentu odpowiedzialnego za obsługę transakcji bazodanowej. W celu zachowania przejrzystości logiki biznesowej, podeprzeć możemy się programowaniem aspektowym.

Czym jest programowanie zorientowane aspektowo?

Programowanie zorientowane aspektowo, AOP (ang. Aspect Oriented Programming) jest paradygmatem programowania, który zwiększa modułowość, umożliwiając wydzielenie problemów przekrojowych. Taką możliwość uzyskujemy poprzez dodanie zachowania do istniejącego kodu bez jego modyfikacji (dodanie kodu w odrębnym miejscu i określenie miejsca wykonania).

Dzięki temu mechanizmowi jesteśmy w stanie dodać do programu zachowanie, które nie jest kluczowe dla logiki biznesowej (przykładowo: logowanie operacji, weryfikację uprawnień czy zarządzanie transakcjami), odseparowując ten kod od kodu bazowego, a dodatkowo móc go wykorzystywać globalnie dla wskazanych grup metod. Aspekty powinny realizować jedynie techniczne lub niefunkcjonalne wymagania.

Programowanie aspektowe pozwala przerwać wykonywanie metody i dołączyć logikę przed, po lub w zależności od wystąpienia specyficznego wydarzenia np. wystąpienia błędu. Ważne, by metoda znajdowała się w beanie springowym.

Wiele tego typu rozwiązań jest już zaimplementowane (np. @Transactional czy @Timed), jednak na potrzeby artykuły zademonstrowane zostanie kilka przykładowych implementacji podobnych problemów.

Mechanizm działania Proxy

Spring AOP jest jedną z głównych części frameworka Spring i odpowiada za realizację tematyki AOP. W Spring AOP istnieją dwa sposoby implementacji aspektów:

  • pierwszy z nich umożliwia implementację za pomocą „czystej” klasy, która jest konfigurowana jako aspekt w springu za pomocą konfiguracji XML,
  • drugi sposób umożliwia korzystanie z adnotacji pochodzących z AspectJ. W późniejszych przykładach wykorzystany zostanie drugi sposób.

Aby zinterpretować dodatkową logikę np. adnotacji, spring wykorzystuje mechanizm proxy (warstwy pośredniej, która obudowuje wstrzykiwaną klasę). Wykorzystywany do tego celu może być zarówno mechanizm proxy JDK jak i CGLIB (powszechna biblioteka definicji klas typu open source, zawarta w spring-core). Jeśli obiekt docelowy implementuje co najmniej jeden interfejs, używamy proxy JDK. W przeciwnym przypadku wykorzystana zostanie biblioteka CGLIB.

Wizualizacja działania mechanizmu proxy
Ryc. 1 Wizualizacja działania mechanizmu proxy

Szczególnym, wartym uwagi przypadkiem jest fakt, że nawet poprawnie zaimplementowany aspekt nie zadziała w przypadku wywołania metody w obrębie jednej klasy (wywołanie metody x() z metody y() w obrębie tej samej klasy Z). Pominięte zostanie w ten sposób wywołanie na klasy proxy, która odpowiada za działanie dodatkowych mechanizmów. Może to rodzić wiele potencjalnych problemów oraz zachowanie aplikacji niezgodne z naszymi oczekiwaniami.

Dodatkowo, warto pamiętać, że aspekty nie zadziałają na metodach prywatnych (brak jest możliwości ich wywołania spoza klasy, a więc i opakowania w dodatkową logikę w klasie proxy).

Omówienie kluczowych elementów Spring AOP

Aspect

Jest modularyzacją problemu, która może obejmować wiele klas. W Spring AOP @Aspect jest adnotacją, którą opatrujemy klasę zawierającą definicję naszych porad (w przypadku korzystania z konfiguracji przez adnotację). Wspomniane adnotacje pochodzą z AspectJ i nie są adnotacjami typowo Springowymi, natomiast Spring rozpoznaje je podczas uruchamiania aplikacji.

Uwaga: wymagane użycie dodatkowej adnotacji np. @Component, by klasa została dodana do kontekstu.

@Aspect
@Component
public class ExampleAspect {
// miejsce implementacji porad i punktów przecięcia
}

Join point

Punkt łączenia jest specyficznym punktem podczas wykonywania programu w postaci wywołania metody, zmiany stanu obiektu, zgłoszenia wyjątku itd. Spring AOP obsługuje tylko join pointy w postaci wywołania metod, co stanowi jedynie niewielką część możliwości AspectJ. Słowem kluczowym używanym w Spring AOP do określenia join pointu w postaci wywołania metody jest „execution”.

Pointcut

Precyzuje zestaw punktów przecięcia, w których należy uruchomić poradę. W spring AOP istnieją dwa sposoby na określenie takiego punktu:

  • możemy podać jego definicję bezpośrednio jako argument adnotacji typu porady np. @Before,
  • możemy również zdefiniować go poprzez oznaczenie metody adnotacją @JoinPoint, dzięki czemu raz zdefiniowany punkt przecięcia można użyć podczas implementacji wielu porad, a nawet łączyć je przy pomocy operatorów logicznych.
@Pointcut("execution(* pl.demo.spring.ExampleClass.exampleMethod(..))")
void examplePointcutMethod() {

}

@Before("examplePointcutMethod()")
void beforeSomeMethod(JoinPoint jp) {
    logger.info("Call exampleMethod with arguments: {}", jp.getArgs());

}

Powyższy kod można również zapisać w następujący sposób:

@Before("execution(* pl.demo.spring.ExampleClass.exampleMethod(..))")
void beforeSomeMethod(JoinPoint jp) {
    logger.info("Call exampleMethod with arguments: {}", jp.getArgs());

}

Warto w tym miejscu przybliżyć podstawowe możliwości konfiguracji punktu przecięcia. W celu poznania bardziej zaawansowanych technik zachęcam do zapoznania się z dokumentacją Spring AOP.

Execution

Podstawowy desygnator umożliwiający dopasowanie punktów łączenia wykonania metody. Możemy w nim określić kolejno:

  • modyfikator dostępu (opcjonalnie),
  • zwracany typ,
  • pakiet,
  • klasę,
  • parametry.

Dodatkowo możemy używać symbolu wieloznaczności „*”. Dowolną liczbę argumentów oraz ich typ, oznaczamy symbolem dwóch kropek „..”.

Annotation

Przykładem równie popularnego desygnatora jest desygnator @annotation. Pozwala on ograniczyć dopasowanie metod oznaczonych specjalną adnotacją. Dodając do tego możliwość tworzenia własnych adnotacji, zyskujemy łatwy sposób na uruchomienie porad dla wskazanych metod. Przykładem zastosowania może być oznaczenie metod, dla wywołań których chcemy mierzyć czas wykonania – taki też przykład zaprezentowano w dalszej części artykułu.

Advice

Porada jest działaniem podjętym dla określonego punktu łączenia (ang. Join point). Dla programisty są to metody wykonane w aplikacji w przypadku, gdy zostanie osiągnięty określony punkt łączenia z pasującym punktem przecięcia (ang. Point cut). Poniżej przedstawiłem rodzaje dostępnych porad wraz z krótkim opisem ich przeznaczenia.

Typy porad

Before advice – porada, która zostanie wykonana przed metodą punktu łączenia. Adnotacją służącą do oznaczenia takiej metody jest adnotacja @Before z pakietu org.aspectj.lang.annotation. Porada ta jako parametr może przyjmować obiekt typu JoinPoint. Nie jest on jednak obowiązkowy.

@Before("pointcut()")
public void beforeMethod(JoinPoint joinPoint)
{
//...
}

After advice – porada, która zostanie zrealizowana po wykonaniu metod punktu łączenia. Należy zwrócić uwagę, iż ten typ porady wykona się niezależnie od wyniku metody (zarówno w przypadku pomyślnego wywołania jak i w przypadku wystąpienia wyjątku). Jej działanie można porównać do bloku finally. Adnotacja, jaką należy ją oznaczyć, to @After. Porada ta jako parametr może przyjmować obiekt typu JoinPoint.

@After("pointcut()")
public void afterMethod(JoinPoint joinPoint)
{
//...
}

After returning – w przeciwieństwie do porad typu @After możemy użyć porady, która wykonana zostanie tylko w przypadku pomyślnego wywołania metody. Taką poradę należy oznaczyć adnotacją @AfterReturning. Jako parametr może przyjmować ona obiekt typu JoinPoint oraz obiekt zwracanej przez funkcję wartości (powiązanie następuje za pomocą parametru returning adnotacji).

@AfterReturning(pointcut = "pointcut()", returning = "response")
public void afterReturningMethod(JoinPoint joinPoint, ResponseEntity<?> response)
{
//...
}

After throwing – uzupełnienie stanowi porada, która wykona się tylko w przypadku, gdy metoda punktu łączenia zgłosi wyjątek. Tego typu porady należy odznaczyć adnotacją @AfterThrowing. Jako parametr może ona przyjmować obiektu typu JoinPoint oraz obiekt zgłaszanego wyjątku (powiązany za pomocą atrybutu throwing adnotacji). 

@AfterThrowing(pointcut = "pointcut()", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Exception ex) {
//...
}

Around – porada, która pozwala nam na otoczenie wywołania metody punktu przecięcia oraz dodanie logiki zarówno przed jej wykonaniem jak i po. Dodatkowo, sami jesteśmy odpowiedzialni za jej wykonanie i dzięki niej mamy możliwość pominięcia wykonania takiej metody. Do tworzenia tego typu porady wykorzystywana jest adnotacja @Around.

W przeciwieństwie do poprzednich typów, porada ta przyjmuje obiekt typu ProceedingJoinPoint, na którym mamy możliwość wywołać metodę proceed(), w celu wykonania metody docelowej. Dodatkowo należy zadbać o zwrócenie rezultatu wykonanej metody.

@Around("pointcut()")
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//...
    Object result = proceedingJoinPoint.proceed();
//...
    return result;
}

Dostęp do bieżącego punktu łączenia

Wszystkie porady mają możliwość zadeklarowania jako pierwszego parametru obiektu typu JoinPoint. Wyjątek stanowi porada Around, która wymaga zadeklarowania jako pierwszego parametru obiektu typu ProceedingJoinPoint, który rozszerza interfejs JoinPoint.

Parametr JoinPoint w poradzie zapewnia wiele metod, które mogą wzbogacić implementację naszej porady. Dzięki niemu możemy na przykład uzyskać dostęp do sygnatury wywoływanej metody JoinPoint::getSignature() oraz jej parametrów JoinPoint::getArgs(). ProceedingJoinPoint pozwala dodatkowo sterować wywołaniem metody ProceedingJoinPoint::proceed().

Dodanie i konfiguracja zależności Spring AOP w projekcie

Pierwszą czynnością, jaką należy wykonać, aby rozpocząć przygodę z programowaniem aspektowym w frameworku SpringBoot, jest dodanie zależności spring-boot-starter-aop w pliku konfiguracyjnym projektu (build.gradle dla projektu zarządzanego przez gradle lub pom.xml w przypadku projektu zarządzanego przez maven).

<dependencies>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
   </dependency>
   <!--   kolejne zależności    -->
</dependencies>

Następnie, w klasie konfiguracyjnej, przy użyciu adnotacji @EnableAspectJAutoProxy, należy odblokować wsparcie dla komponentów oznaczonych adnotacją @Aspect. Wspomnianą adnotację można dodać w głównym pliku konfiguracji aplikacji lub w dowolnym komponencie konfiguracyjnym:

@SpringBootApplication
@EnableAspectJAutoProxy
public class AspectApplication {

   public static void main(String[] args) {
      SpringApplication.run(AspectApplication.class, args);
   }

}

Implementacje przykładowych problemów

Na potrzeby artykułu zaprezentowane zostaną 3 przykładowe implementacje aspektów.

  • Pierwsza z nich dotyczyć będzie porady typu Before, gdzie dodana zostanie implementacja logująca wywołanie endpointów w przykładowym kontrolerze.
  • Następnie przedstawiona i omówiona zostanie implementacja aspektu typu @Around na przykładzie logowania czasu trwania wybranych operacji, dodatkowo wykorzystany zostanie desygnator @annotation.
  • Ostatni przykład przedstawiać będzie logowanie sytuacji nadzwyczajnych, gdzie zwrócony został nieobsłużony wyjątek. Logowanie przy pomocy aspektów nie jest ich jedynym przeznaczeniem. Jednak na potrzeby prezentacji poszczególnych typów aspektów, jest w zupełności wystarczające i w całości pozwoli skupić się na omawianej tematyce.

Parametry

Poniżej przedstawiony zostanie kod kontrolera, serwisu, modelu oraz adnotacji, na podstawie których stworzone zostaną aspekty.

TaxController – zawiera implementację przykładowego kontrolera restowego. Metoda calculateTax została oznaczona specjalnie stworzoną adnotacją @LogExecutionTime, która posłuży do prezentacji tworzenia pointcutu z użyciem desygnatora @annotation.

@RestController
@RequestMapping("/tax")
public class TaxController {

    private final TaxService taxService;

    public TaxController(TaxService taxService) {
        this.taxService = taxService;
    }

    @GetMapping("/calculate/{clientId}")
    @LogExecutionTime
    ResponseEntity<TaxResult> calculateTax(@PathVariable Long clientId) {
        TaxResult result = taxService.calculateTax(clientId);
        return ResponseEntity.ok(result);
    }

}

TaxService – kod przykładowej implementacji serwisu, który w zależności od przekazanego parametru clientId pozwoli zasymulować 3 rodzaje operacji (wywołanie operacji, operację trwająca dłużej oraz operację zakończoną zgłoszeniem wyjątku). Tak przygotowana metoda pozwoli w pełni przetestować wszystkie stworzone porady.

@Service
public class TaxService {

    TaxResult calculateTax(Long clientId) {
        if (clientId == 2) {
            throw new IllegalStateException("Calculating for this client is not allowed!");
        }
        if (clientId == 3) {
            sleepFor5Seconds();
        }
        return new TaxResult(clientId);
    }

    private void sleepFor5Seconds() {
        try {
            Thread.sleep(5000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

TaxResult – prosta klasa modelu wykorzystywanego w przykładzie.

public class TaxResult {

    private Long clientId;

    private BigDecimal tax;

    public TaxResult(Long clientId) {
        this.clientId = clientId;
    }

    public TaxResult(Long clientId, BigDecimal tax) {
        this.clientId = clientId;
        this.tax = tax;
    }

    public Long getClientId() {
        return clientId;
    }

    public void setClientId(Long clientId) {
        this.clientId = clientId;
    }

    public BigDecimal getTax() {
        return tax;
    }

    public void setTax(BigDecimal tax) {
        this.tax = tax;
    }
}

LogExecutionTime – kod stworzonej adnotacji, którą oznaczono metodę calculateTax z klasy TaxController.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}

Implementacja przykładowych porad

Porada typu @Before przedstawia logowanie wywołania każdej metody z klasy TaxController. W tym przypadku zaprezentowano przekazywanie definicji pointcutu jako nazwy zdefiniowanej metody. Wykorzystano również pobranie nazwy metody, dla której uruchomiona została porada za pośrednictwem metody JoinPoint::getSignature()::getName() oraz przekazanych argumentów JoinPoint::getArgs().

@Aspect
@Component
public class TaxAspectBefore {

    public static final Logger logger = LoggerFactory.getLogger(TaxAspectBefore.class);

    @Pointcut("execution(* pl.demo.spring.TaxController.*(..))")
    void anyMethodOfTaxController() {

    }

    @Before("anyMethodOfTaxController()")
    void logCallTaxEndpoint(JoinPoint joinPoint) {
        logger.info("Call TaxController endpoint: {} with arguments {}", joinPoint.getSignature().getName(), joinPoint.getArgs());
    }

}

Przy pomocy porady typu @Around zaimplementowane zostało logowanie czasu wykonania metody. Metoda ta została opatrzona adnotacją @LogExecutionTime. Wykorzystany do tego celu został desygnator typu @annotation, który jako parametr przyjmuje nazwę adnotacji wraz z pakietem. Dodatkowo, warto zwrócić uwagę na konieczność wykonania metody proceedingJoinPoint.proceed() oraz przypisania do zmiennej i zwrócenia jej wyniku.

@Aspect
@Component
public class TaxAspectAround {

    public static final Logger logger = LoggerFactory.getLogger(TaxAspectAround.class);

    @Around("@annotation(pl.demo.spring.LogExecutionTime)")
    Object logExecutionTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        long duration = (System.currentTimeMillis() - start) / 1000;
        logger.info("Execution of method {} took {} s", proceedingJoinPoint.getSignature().getName(), duration);
        return result;
    }

}

Jako ostatnia przedstawiona została implementacja dodatkowego logowania w przypadku zgłoszenia wyjątku przez metodę calculateTax z serwisu TaxService. Za pomocą atrybutu throwing adnotacji @AfterThrowing określamy mapowanie zgłoszonego wyjątku do parametru metody. W logowaniu wykorzystano wcześniej poznane metody z klasy JoinPoint, a dodatkowo informacje pochodzące ze zgłoszonego wyjątku.

@Aspect
@Component
public class TaxAspectAfterThrowing {

    public static final Logger logger = LoggerFactory.getLogger(TaxAspectAfterThrowing.class);

    @AfterThrowing(pointcut = "execution(TaxResult pl.demo.spring.TaxService.calculateTax(Long))", throwing = "exception")
    void logAfterThrowingInCalculateTaxMethod(JoinPoint joinPoint, Exception exception) {
        logger.error("TaxService::calculateTax throw error for clientId: {} with message: {}", joinPoint.getArgs()[0], exception.getMessage());
    }

}

Podsumowanie

Wykorzystanie podejścia zorientowanego na aspekty w tworzonej aplikacji może stanowić doskonałe rozwiązanie do oddzielenia niefunkcjonalnych/technicznych fragmentów kodu od logiki biznesowej.

Należy jednak mieć na uwadze, iż dodanie kolejnego poziomu abstrakcji może stanowić problem dla mniej doświadczonych programistów oraz utrudniać proces debugowania. Dlatego warto korzystać z nich w sposób przemyślany oraz umieszczać je w jednym miejscu, w celu szybkiej ich identyfikacji.

5/5 ( głosy: 3)
Ocena:
5/5 ( głosy: 3)
Autor
Avatar
Sebastian Piotrowski

Zawodowo z informatyką związany od ponad 10 lat, w roli programisty już 7. rok. W Sii od ponad roku pracuje jako Software Engineer. Jego głównym zainteresowaniem są technologie backendowe jak Java i Kotlin oraz ekosystem frameworku Spring. Pracuje również frontendowo z wykorzystaniem JavaScript i React.

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Może Cię również zainteresować

Pokaż więcej artykułów

Bądź na bieżąco

Zasubskrybuj naszego bloga i otrzymuj informacje o najnowszych wpisach.

Otrzymaj ofertę

Jeśli chcesz dowiedzieć się więcej na temat oferty Sii, skontaktuj się z nami.

Wyślij zapytanie Wyślij zapytanie

Natalia Competency Center Director

Get an offer

Dołącz do Sii

Znajdź idealną pracę – zapoznaj się z naszą ofertą rekrutacyjną i aplikuj.

Aplikuj Aplikuj

Paweł Process Owner

Join Sii

ZATWIERDŹ

This content is available only in one language version.
You will be redirected to home page.

Are you sure you want to leave this page?