sobota, 6 listopada 2010

Wstęp do Spring AOP.

W dzisiejszym wpisie wystąpi trochę anglojęzycznych zwrotów - ucząc się AOP nie próbowałem niektórych pojęć tłumaczyć i zapożyczyłem je, jeśli szanowny Czytelnik ma pomysł na tłumaczenia proszę o komentarz.

Aby zrozumieć czym jest, a może ważniejsze - po co jest AOP spójrzmy na dwa problemy występujące w systemach :
  1. "Tangling" - mówi o tym, że mamy pomieszany kod biznesowy z kodem obsługującym : transakcyjność, logowanie, obsługę wyjątków itp. - jakiś kod techniczny.
  2. "Scattering" - mamy podobny kod rozsiany po całym systemie - np kod obsługi transakcyjności, otwieranie sesji hibernate itp.
AOP mówi o tym, żeby takie kawałki kodu znaleźć, wyekstrahować do osobnego bytu i zaaplikować do kodu biznesowego. Oddzielamy kod biznesowy od kodu technicznego. Mówiąc po ludzku : z kodu biznesowego wyciągamy to, co nie jest kodem logiki biznesowej, następnie umieszczamy go w innej klasie, dalej AOP 'automagicznie' sprawia, że w czasie wykonania wyjęty przez nas kod mimo wszystko zostanie uruchomiony.



Aby zwiększyć precyzję myśli wprowadźmy pojęcia :
  • Join point - np wywołanie metody lub przypisanie wartości do pola. Spring AOP wspiera tylko wywołania publicznych metod na bean-ach.
  • Pointcut - wyrażenie określające miejsca, do których zostaną zaaplikowane aspekty. Możemy rozumieć jako 'wyrażenie regularne', które 'wyciąga' nam odpowiednie miejsca w systemie (join point-y) aby tam umieścić kod - advice.
  • Advice - kod, który wcześniej opisałem jako ten 'wyjęty', kod najczęściej nie biznesowy.
  • Aspect - byt (w spring aop będzie to klasa) zawierający pointcut-y oraz advice-y.

    Typy advice-ów :
    • before - advice uruchomi się przed wywołaniem join point-a.
    • afterReturning - advice uruchomi się po poprawnym wykonaniu join point-a.
    • afterThrowing - advice uruchomi się po rzuceniu wyjątku z join point-a.
    • after - advice obejmuje @AfterReturning oraz @AfterThrowing.
    • around - advice uruchomi się podobnie jak dla before, ale sami wołamy join point-a. Skoro sami go wołamy, to możemy również wywołać go kilkukrotnie bądź nie wywołać w ogóle (np skorzystać z cache-a).

      Kilka ograniczeń Spring AOP :
      • pointcut-y odnosić się będą tylko do beanów springa (np nie zadziałają np na encjach hibernate).
      • jeśli bean A posiada dwie publiczne metody 'm1' i 'm2', na każdą z nich nałożymy odpowiednio aspekt 'a1' i 'a2'. Dalej, jeśli metoda 'm1' woła w sobie 'm2' (lub na odwrót), to nie zostanie uruchomiony aspekt 'a2'. Dzieje się tak, dlatego że wołając metodę 'm1' na bean-ie wołamy ją tak naprawdę na obiekcie proxy. Advice się uruchomi, i następnie odpali metodę 'm1' na obiekcie A (a nie na proxy do A). Zatem aspekt 'a2' nie zostanie wywołany.
      • możemy nakładać aspekty tylko na metody publiczne.
      • jest wolniejszy od AspectJ (który to modyfikuje bytecode).

        Tutaj powiem od razu, że można tak skonfigurować spring aop, aby korzystał z modyfikacji bytecode za pomocą AspectJ, zamiast z dynamic proxy. Można to osiągnąć na dwa sposoby :
        1. Jeśli nakładamy aspekt na beana, który nie posiada interfejsu spring automatycznie skorzysta z AspectJ.
        2. W konfiguracji taga <aop:aspectj-autoproxy> podać atrybut proxy-target-class="true".
          Jeśli jeszcze masz ochotę czytać, to dalej będzie mniej teorii, a więcej kodu :). Stąd można pobrać źródła projektu.

          Załóżmy, że mamy adnotację, interfejs oraz jego implementację:
          @Target(ElementType.METHOD)
          @Retention(RetentionPolicy.RUNTIME)
          @Documented
          public @interface Log {
              String logText() default "";
          }
          
          public interface Service {
           String getMessage(String name);
          }
          
          public class ExampleService implements Service {
              @Log
              @Override
              public String getMessage(String name) {
                  return name;
              }
          }
          
          Powiedzmy, że chcemy zalogować wywołanie metody getMessage. Oznaczyliśmy ją naszą adnotacją @Log. Napiszmy teraz odpowiedni pointcut :
          @Aspect
          public class LoggerAspect {
           @Before(value = "@annotation(log)")
              public void trackLog(JoinPoint joinPoint, Log log) {
               String logText = extractLogText(joinPoint, log);
               System.out.println(logText);
              }
           . . .
          }
          
          Oczywiście trzeba jeszcze dodać kawałek xml-a (nasz ExampleService musi być beanem, tak samo, jak aspekt).
          <bean id="service" class="pl.turo.spring.aop.service.ExampleService" />
          
          <aop:aspectj-autoproxy />
          
          <bean id="loggerAspect" class="pl.turo.spring.aop.utils.LoggerAspect" />
          
          Korzystam z namespace 'aop' - trzeba pamiętać aby dodać jego definicję.
          Mamy :
          • bena z metodą którą chcemy 'aspektować' - 'service',
          • aspekt z definicją pointcut oraz z naszym kodem advice.
            Teraz odpalmy springa i sprawdźmy co się stanie :
            public static void main(String[] args) {
              ApplicationContext context = new ClassPathXmlApplicationContext("/META-INF/spring/app-context.xml");
              Service service = context.getBean(Service.class);
              service.getMessage("czy zadziała AOP ?");
             }
            
            i na konsoli widzimy :
            ExampleService.getMessage (czy zadziała AOP ?)
            
            Odpaliliśmy metodę na beanie 'service', aspekt 'przechwycił' to wywołanie i je zalogował. (Pełny kod aspektu udostępniam w źródłach projektu).

            Teraz kolejno co się stało :
            1. Stworzyliśmy interfejs z metodą którą chcielibyśmy aspektować.
            2. Napisaliśmy implementację naszego interfejsu.
            3. Stworzyliśmy adnotację, aby jakoś oznaczyć metodę, że chcemy ją logować/aspektować.
            4. Napisaliśmy aspekt z pointcutem typu before.
            5. W advice rzucamy sysout z logiem.

              Czy w tym przykładzie konieczna jest adnotacja ? Oczywiście nie. Moglibyśmy na kilka innych sposobów napisać nasz pointcut. W kolejnym wpisie temat poin-cutów opiszę dokładniej.

              Wyżej napisałem, że spring tworzy obiekt proxy i w ten sposób dzieje się magia aop. Sprawdźmy to :
              public static void main(String[] args) {
                ApplicationContext context = new ClassPathXmlApplicationContext("/META-INF/spring/app-context.xml");
                Service service = context.getBean(Service.class);
                System.out.println(service.getClass());
                service.getMessage("czy zadziała AOP ?");
               }
              
              Po uruchomieniu tego kodu widzę na konsoli :
              class $Proxy5
              Service.getMessage (czy zadziała AOP ?)
              
              Mamy proxy.

              Powiedziałem wyżej, że wystarczy trochę konfiguracji (oraz ku ścisłości : aspectjweaver i aspectjrt na classpath) aby spring nie tworzył proxy a zmodyfikował bytecode, spójrzmy :
              <aop:aspectj-autoproxy proxy-target-class="true" />
              
              ustawiliśmy proxy-target-class="true", uruchamiamy jeszcze raz naszego maina :
              class pl.turo.spring.aop.service.ExampleService$$EnhancerByCGLIB$$8993e64c
              ExampleService.getMessage (czy zadziała AOP ?)
              
              Widzimy, CGLIB-a - działa.

              Powiedziałem też, że jeśli bean nie implementuje interfejsu z metodą join point to spring automatycznie skorzysta z modyfikacji bytecodu, spójrzmy :
              public class NoInterfaceService {
               @Log
               public String getMessage(String name) {
                return name;
               }
              }
              
              <bean id="noInterfaceservice" class="pl.turo.spring.aop.service.NoInterfaceService" />
              
              <aop:aspectj-autoproxy proxy-target-class="false" />
              
              <bean id="loggerAspect" class="pl.turo.spring.aop.utils.LoggerAspect" />
              
              i main :
              public static void main(String[] args) {
                ApplicationContext context = new ClassPathXmlApplicationContext("/META-INF/spring/app-context.xml");
                NoInterfaceService noInterfaceService = context.getBean(NoInterfaceService.class);
                System.out.println(noInterfaceService.getClass());
                noInterfaceService.getMessage("bez interfejsu mamy zawsze AspectJ");
               }
              
              i konsola :
              class pl.turo.spring.aop.service.NoInterfaceService$$EnhancerByCGLIB$$1d51d172
              NoInterfaceService.getMessage (bez interfejsu mamy zawsze AspectJ)
              
              Widzimy, że działa.

              W powyższym przykładzie korzystałem z adnotacji aby skonfigurować aspekt. Jak to w springu bywa można to zrobić również w xml-u, ale tutaj osobiście myślę, że adnotacje są czytelniejsze - czytając klasę aspektu widzimy jego pointcuty.

              AOP na pierwszy rzut oka może przerażać, przyznam się, że mnie przeraziło. Mamy trochę konfiguracji, jakąś klasę aspektu i nagle nasz kod zaczyna inaczej się zachowywać. Jednak po 2 latach z wykorzystaniem aspektów myślę, że to świetne narzędzie.

              STS (springsource tool suite) potrafi przejrzyście pokazywać wszystkie advice-y i pointcuty w aplikacji. Da się zatem nad tym zapanować.

              AOP pozwala na fajne zaimplementowanie transakcyjności (dokładnie tak w springu działa moduł odpowiedzialny za trasakcyjność :)), strategii cache, prób ponowienia wywołania metody przy jakiś tam warunkach.

              Generalnie super sprawa.

              2 komentarze:

              1. Mógłbyś wyjaśnić dokładnej składnię:
                " String logText() default "";"
                Jest ona intuicyjna, jednak spotkałem się z nią po raz pierwszy.

                OdpowiedzUsuń
              2. Pewnie :-)
                Zauważ, że jest to kawałek kodu z deklaracji adnotacji Log. Dla tej adnotacji deklarujemy jeden atrybut o nazwie 'logText'. Atrybut jest typu String i jest opcjonalny - domyślnie przyjęta wartość to pusty napis.

                OdpowiedzUsuń