Programowanie aspektowe w Spring Boot


Programowanie aspektowe w Spring Boot

Java to technologia na której wyrosło programowanie aspektowe. Sztandarową implementacją tego podejścia jest biblioteka AspectJ – projekt tworzony przez zespół profesora z zakresu informatyki Gregora Kiczales. Artykuł ten bazuje jednak na module Spring AOP. Moduł ten wykorzystywany jest tylko dla obiektów zarządzanych przez kontener IoC Springa – w przeciwieństwie do projektu AspectJ który może być wykorzystywany dla wszystkich obiektów domenowych.

Jaki problem rozwiązuje programowanie aspektowe?

Aplikacja oprócz głównej logiki biznesowej zawiera szereg innych mechanizmów odpowiadających np. za bezpieczeństwo, obsługę transakcji czy wielowątkowość. Procesy te są często rozsiane po całym systemie. Przeplatanie implementacji logiki biznesowej z procesami pobocznymi jest jedną z największych wad programowania obiektowego ponieważ programista zamiast skupiać się na implementacji danego procesu biznesowego musi implementować poboczne (z ang. concern) mechanizmy które są niezbędne do jego realizacji.

Klasyczny przykład to operacja przelewu bankowego. Proces ten to nie tylko zmniejszenie kwoty na jednym koncie i zwiększenie jej na drugim. Z procesem tym wiążą się również wspomniane wcześniej inne zagadnienia – obsługa transakcji, logowania, wielowątkowości itp…

public void transferTo(Account bank, double x) {
    super.getLogger().info("start transfer transaction")
    UserTransaction ut = getUserTransaction();
    try {
            ut.begin();
            if (x <= this.balance) {
                withdraw(x);
                bank.deposit(x);
                System.out.println("Transfer transaction succesful. Tansfered: $" + x);
	        ut.commit();
                super.getLogger().info("commit transfer transaction")
            } else { 
               System.out.println("Transfer transaction failed, not enough balance!");
            }
    } catch (Excdption e) {
          super.getLogger().infofinishtransfer transaction")
    }
}

W powyższym przykładzie widać przeplatanie się logiki biznesowej z pobocznymi mechanizmami które są niezbędne do jej realizacji. Utrudnia to refaktoryzację kodu źródłowego i znacznie zmniejsza jego czytelność. Jeśli aplikacja zawiera więcej tego rodzaju metod to pojawia się poważny problem refaktoryzacyjny! Metoda ta powinna po refaktoryzacji wyglądać następująco:

public void transferTo(Account bank, double x) {
    if (x <= this.balance) {
        withdraw(x);
        bank.deposit(x);
        System.out.println("Transfer transaction succesful. Tansfered: $" + x);
    } else { 
         System.out.println("Transfer transaction failed, not enough balance!");
    }
}

Programowanie aspektowe rozwiązuje wyżej opisany problem przeplatania kodu rozszerzając programowanie obiektowe o użycie tzw. aspektów. Koncept ten dobrze przedstawia rysunek:

[źródło] https://www.slideshare.net/anjosc/aop-codebits2011

Proces “tkania” – (z ang. weaving) czyli “łączenia kodu” może być wykonany na etapie kompilacji bądź na etapie wykonania programu (z ang. runtime):

AspectJ:

  • Compile-time weaving (podczas kompilacji),
  • Post-compile weaving (po kompilacji),
  • Load-time weaving (podczas ładowania klas do JVM przez class loader).

Spring AOP:

  • runtime weaving (podczas wykonania programu):

    • JDK proxy,
    • CGLib proxy.

Przykładem wykorzystania programowania aspektowego w Springu jest adnotacja @Transactional:

@Transactional
public void businessLogic() {
}

co jest odpowiednikiem do:

UserTransaction utx = entityManager.getTransaction(); 
try { 
    utx.begin(); 
    businessLogic();
    utx.commit(); 
} catch(Exception ex) { 
    utx.rollback(); 
    throw ex; 
}

Poniżej kilka ważnych informacji uzupełniających odnośnie transakcji:

Aspekt transakcyjny działa, ale tylko dla publicznych metod klasy. Adnotacja @Transactional nie zadziała zatem na prywatnych metodach. Ponadto wywołanie metody oznaczonej adnotacją @Transactional z innej metody również oznaczonej adnotacją @Transactional znajdującej się w tej samej klasie nie da żadnego efektu. Wywołanie metody transakcyjnej musi pochodzić z zewnętrznego serwisu. Jeśli jednak wywołana zostanie metoda która nie jest oznaczona adnotacją @Transactional przez metodę która z kolei jest transakcyjna to metoda ta ma wpływ na transakcję i jeśli zostanie wygenerowany przez tą metodę wyjątek to transakcja zostanie odrzucona. Więcej w tym temacie tutaj – https://docs.spring.io/spring/docs/3.0.x/spring-framework-reference/html/transaction.html.

  • @Transactional(propagation=Propagation.REQUIRED)

Poniżej przedstawiona transakcja wykonuje transakcyjną metodę oznaczoną adnotacją @Transactional(propagation=Propagation.REQUIRED). Oznacza to, że metoda testRequiredInner() używa tej samej transakcji co metoda testRequiredOuter().

@Autowired
private InnerBean innerBean;
 
@Override
@Transactional(propagation=Propagation.REQUIRED)
public void testRequiredOuter(User user) {
  try{
    innerBean.testRequiredInner();
  } catch(RuntimeException e){
    // handle exception
  }
}
@Override
@Transactional(propagation=Propagation.REQUIRED)
public void testRequiredInner() {
  throw new RuntimeException("Rollback this transaction!");
}
  • @Transactional(propagation=Propagation.REQUIRED_NEW)

Poniżej przedstawiona transakcja wykonuje transakcyjną metodę oznaczoną adnotacją @Transactional(propagation=Propagation.REQUIRED_NEW). Oznacza to, że metoda testRequiredInner() wykonywana jest w oddzielnej transakcji.

@Autowired
private InnerBean innerBean;
 
@Override
@Transactional(propagation=Propagation.REQUIRED)
public void testRequiredOuter(User user) {
  try{
    innerBean.testRequiredInner();
  } catch(RuntimeException e){
    // handle exception
  }
}
@Override
@Transactional(propagation=Propagation.REQUIRED_NEW)
public void testRequiredInner() {
  throw new RuntimeException("Rollback this transaction!");
}

Zastosowanie w Spring Boot:

Zaczynamy od nowego projektu Spring Boot – plik pom.xml – niezbędne zależności:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Przykładowa klasa serwisu:

@Service
public class ServiceBean {
    public String getMsg() {
        return "service bean msg";
    }
}

RestController:

@RestController
public class ServiceRestController {
 
    private final ServiceBean serviceBean;
 
    public ServiceRestController(ServiceBean serviceBean) {
        this.serviceBean = serviceBean;
    }
 
    @GetMapping("/")
    public String getServiceMsg() {
      return serviceBean.getMsg();
    }
}

Przykładowy aspekt – przechwytujemy wszystkie metody zdefiniowane w klasach znajdujących się w pakiecie service. Mierzymy czas wykonania metody dodając odpowiedni kod przed wykonaniem metody i po jej wykonaniu:

@Aspect
@Component
public class LogAspect {
 
    private static final Logger LOG = LoggerFactory.getLogger(LogAspect.class);
 
    private final String SERVICE_POINTCUT = "execution(* pl.javaleader.aspectaop.service.*.*(..))";
 
    @Around(SERVICE_POINTCUT)
    public Object logAdvice(ProceedingJoinPoint jp) throws Throwable {
        LOG.info("[METHOD] -&gt; {}", jp.getSignature().toShortString());
        Instant startTime = Instant.now();
        Object obj = jp.proceed();
        Instant endTime = Instant.now();
        LOG.info("[METRICS] -&gt; {}, time: {} {} ", jp.getSignature().toShortString(), Duration.between(startTime, endTime).getSeconds(), "sec.");
        return obj;
    }
}

po wejściu na adres:

http://localhost:8080/

w logach aplikacji zauważyć można:

2019-11-25 11:53:50.928  INFO 117680 --- [nio-8080-exec-1] p.j.aspectaop.aspects.LogAspect : [METHOD] -&gt; ServiceBean.getMsg()
2019-11-25 11:53:50.941  INFO 117680 --- [nio-8080-exec-1] p.j.aspectaop.aspects.LogAspect : [METRICS] -&gt; ServiceBean.getMsg(), time: 0 sec

Powyższy przykład jest bardzo przydatny kiedy aplikacja wdrożona jest na środowisko produkcyjne ponieważ pozwala dowiedzieć się czy dana metoda nie wykonuje się za długo.

  • Porada (z ang. advice):
    • Moment działania aspektu, który określony jest przez punkt złączenia (around, before, after).
  • Punkt złączenia (z ang. Join point):
    • Punkt w programie w którym następuje wykonanie metody.
  • Punkt przecięcia (z ang. pointcut):
    • Określa miejsca w programie np. zestaw klas dla których porada ma zostać uruchomiona.

Zobacz kod na GitHubie i zapisz się na bezpłatny newsletter!

.


Leave a comment

Your email address will not be published.


*