poniedziałek, 8 listopada 2010

Spring AOP - definiowanie poincut-ów cz. 2

Post ten nawiązuje do dwóch poprzednich w tematyce spring aop. Będę opierał się o ten sam projekt.
Definiując pointcut możemy uzyskać dostęp do kontekstu join point-a. Do tego kontekstu należą:
  • this - oznacza rzeczywisty obiekt, na którym została wywołana metoda (proxy)
  • target - oznacza obiekt zawierający join point (bean springa, którego metodę 'aspektujemy')
  • argumenty wywołania metody
  • adnotacje związane z metodą.
Są to informacje, które mogą się okazać użyteczne podczas implementacji kodu advice.

Podział ten odnosi się do podziału ze względu na typ :
  • execution - ten już znamy :-), dla przypomnienia - używamy jak chcemy napisać pointcut po paternie na nazwę metody.
  • within - służy do określenia pakietu w którym będą szukane join point-y. Zwykle w spring aop używany wraz z execution aby odfiltrować wyniki do danej paczki.
  • this - możemy odfiltrować się do typu obiektu this(spring aop proxy).
  • target - możemy odfiltrować się do typu obiektu target.
  • args - odfiltrowujemy się do join point-ów z pasującymi typami argumentów (za chwilę pokażę na przykładzie)
  • @args - możemy odfiltrować join point-y podając typ adnotacji jaką musi być zaadnotowany parametr metody. Możemy podać po przecinku kilka typów adnotacji - będą się odnosić do kolejnych parametrów metody.
  • @annotation - obiekt target musi być posiadać adnotacje typów podanych w pointcut-ie

Po pełną listę polecam zajrzeć tu. Ja skupię się wyłącznie na tych wyżej wymienionych.

Załóżmy, że logując wywołanie metody getMessage(String) chcemy dodatkowo zalogować z jaką wartością parametru została ta metoda wywołana.
Aspekt może wyglądać tak :
@Aspect
public class LoggerAspect {

    @Before("oneParameter() && args(message)")                            // 3
    public void trackLog(JoinPoint joinPoint, String message) {           // 4
        StringBuilder logText = createJoinPointTraceName(joinPoint);
        logText.append(" (").append(message).append(")");
        System.out.println(logText);
    }

    @Pointcut("execution(* pl..Service+.get*(*))")                        // 2
    private void oneParameter() {}                                        // 1

... 
}
Kolejno, co się dzieje:
  1. Zdefiniowałem poincut 'oneParameter' wyciągający nam z jakiegokolwiek podpakietu 'pl' interfejs Service (i każdy po nim dziedziczący).
  2. Na tym interfejsie szukamy metod o nazwie zaczynającej się od 'get' i dokładnie jednym parametrze.
  3. Nad advice (metoda trackLog) napisałem pointcut, który korzysta z pointcut-a 'oneParameter' oraz dokłada warunek args(message).
  4. Ten drugi warunek odnosi się do parametru metody trackLog - szuka parametru o nazwie 'message'.
  5. Następnie sprawdza jakiego typu jest ten argument. Widzi, że typem jest String.
  6. Znając już ten typ pointuct odfiltrowuje się tylko do metod mających jeden parametr typu String.
Łącząc nazwany pointcut (oneParameter) oraz warunek 'args(message)' otrzymaliśmy oczekiwany pointcut.
Podsumowując: dzięki użyciu 'args' otrzymaliśmy tutaj dwie rzeczy :
  • odfiltrowaliśmy join point-y po typie argumentu.
  • w kodzie metody trackLog mamy referencję do obiektu przekazanego jako parametr wywołania join point-a.
    Posiadając referencję do obiektu JoinPoint możemy z niego wyciągnąć tablicę argumentów, rzutować, korzystać z refleksji i otrzymać to samo. Wydaje mi się jednak, że pokazany przezemnie wyżej sposób jest 'czystszy' i bardziej czytelny, zobaczmy jak to wygląda:
    private String getStringArgs (JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        if (args.length != 1) {
            throw new RuntimeException("Niepoprawna ilość parametrów metody");
        }
        String stringParam = (String) args[0];
        return stringParam;
    }
    

    Wyobraźmy sobie, że same parametry wywołania join point-a nam nie wystarczają, chcemy mieć referencję do obiektu target, zmodyfikujmy nasz pointcut i advice :
    @Before("args(message) && target(service)")
    public void trackLog(JoinPoint joinPoint, String message, Service service) {
        StringBuilder logText = createJoinPointTraceName(joinPoint);
        logText.append(" (").append(message).append(")");
        System.out.println("Obiekt target : " + service.getClass());
        System.out.println(logText);
    }
    
    Podobnie jak z 'args' - target(service) wskazuje na parametr w argumentach metody trackLog, spring sprawdza typ tego parametru - na tej podstawie nastąpi odfiltrowanie join point-ów. Obiekt ten to bean springa, który 'aspektujemy' (na nim wołaliśmy metodę join ponit). Innymi słowy w tym pointcut-cie wyciągamy join point-y tylko z bean-ów implementujących interfejs Service, metody publiczne z jednym parametrem typu String.

    A co jeśli chcielibyśmy zmodyfikować nazwane poincut-y tak, aby to one wyciągały nam interesujący nas kontekst :
    @Before("oneParameter(message, service)")
    public void trackLog(JoinPoint joinPoint, String message, Service service) {
        StringBuilder logText = createJoinPointTraceName(joinPoint);
        logText.append(" (").append(message).append(")");
        System.out.println("Obiekt target : " + service.getClass());
        System.out.println(logText);
    }
    
    @Pointcut("execution(* pl..*.get*(*))  && args(message) && target(service)")
    private void oneParameter(String message, Service service) {}
    
    Działa identycznie jak poprzedni przykład.

    A teraz: chciałbym mieć kontekst obiektu this (na tym obiekcie wywoływany jest advice - jest to obiekt proxy). Spring tworzy jeden obiekt proxy dla wszystkich wywołań, co można łatwo sprawdzić.
    Modyfikujemy nazwany pointcut-y:
    @Pointcut("execution(* pl..Service+.get*(*))  && args(message) && target(service) && this(proxyObject)")
    private void oneParameter(String message, Service service, Service proxyObject) {}
    
    @Before("oneParameter(message, service, proxyObject)")
    public void trackLog(JoinPoint joinPoint, String message, Service service, Service proxyObject) {
        StringBuilder logText = createJoinPointTraceName(joinPoint);
        System.out.println(proxyObject);
        logText.append(" (").append(message).append(")");
        System.out.println(logText);
    }
    
    Wywołajmy trzykrotnie metodę getMessage(String) na naszym beanie :
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("/META-INF/spring/app-context.xml");
        Service service = (Service) context.getBean("service");
        service.getMessage("raz");
        service.getMessage("dwa");
        service.getMessage("trzy");
    }
    
    Na konsoli widzę :
    pl.turo.spring.aop.service.ExampleService@9a9b65
    Service.getMessage (raz)
    ^ string
    
    pl.turo.spring.aop.service.ExampleService@9a9b65
    Service.getMessage (dwa)
    ^ string
    
    pl.turo.spring.aop.service.ExampleService@9a9b65
    Service.getMessage (trzy)
    ^ string
    
    Widać, że za każdym razem jak wywoływana była metoda getMessage(String) uruchomiony został kod advice i obiekt this to ten sam obiekt w JVM.
    W metodach ExampleServie dodałem sysout aby na konsoli łatwo móc zweryfikować którą metodę zawołaliśmy.
    @Override
    public String getMessage(String name) {
        System.out.println("^ string\n");
        return name;
    }
    
    @Override
    public String getMessage(Double a) {
        System.out.println("^ double\n");
        return null;
    }
    

    Napisałem wyżej że można odfiltrowywać się do metod oznaczonych odpowiednią adnotacją. Załóżmy, że mamy adnotację Log i chcemy złapać się na wszystkie wywołania metod za-adnotowanych tą adnotacją:
    @Pointcut("execution(@pl.turo.spring.aop.utils.Log * *(..))")
    private void logAnnotation() {}
    
    @Before("logAnnotation()")
    public void trackLog(JoinPoint joinPoint) {
        StringBuilder logText = createJoinPointTraceName(joinPoint);
        System.out.println(logText);
    }
    
    Utworzyliśmy nazwany pointcut 'logAnnotation' który wyciąga metodę zaadnotowaną adnotacją pl.turo.spring.aop.utils.Log z dowonlego pakietu, o dowolnej nazwie, dowolnej liczbie i typach parametrów, dowolnym zwracanym typie.
    Jeśli chcielibyśmy z tej adnotacji wyciągnąć jakieś wartości to możemy zrobić tak:
    @Pointcut("@annotation(log)")
    private void logAnnotationValue(Log log) {}
    
    @Before("logAnnotationValue(log)")
    public void trackLog(JoinPoint joinPoint, Log log) {
        System.out.println(log.logText());
    }
    
    Mając referencję do adnotacji możemy z niej wyciagnąć całą konfigurację metody. Myślę, że może to być użyteczne np. w obsłudze security (@Secured(allowedRole={"admin"}) czy transakcyjności.

    Pokażę jeszcze jak skorzystać z @args. Weźmy dwie różne markerowe adnotacje Anno i Anno2 (wybacznie finezję nazewnictwa). Tworzę dwie klasy ParamClass i ParamClass2 odpowiednio za-adnotowane. Spójrzmy na pointcut:
    @Before("@args(pl.turo.spring.aop.agrsanno.Anno, pl.turo.spring.aop.agrsanno.Anno2)")
    public void trackLog(JoinPoint joinPoint) {
    ...
    }
    
    Odfiltruje wywołania wszystkich metod publicznych, na wszystkich beanach, które mają dokładnie dwa parametry oraz
    • klasa pierwszego z nich jest zaadnotowana adnotacją Anno
    • klasa drugiego z nich jest zaadnotowana adnotacją Anno2.
    Spójrzmy na deklaracje metod :
    void getMessage(ParamClass paramClass, ParamClass2 paramClass2);
    void getMessage(ParamClass paramClass, int i, ParamClass2 paramClass2);
    void getMessage(ParamClass paramClass, ParamClass2 paramClass2, int i);
    
    Tylko pierwsza deklaracja pasuje do naszego pointcut-a.

    Możemy również chcieć ograniczyć się do szukania join point-ów tylko w określonych pakietach lub określonej klasie, robimy to tak:
    @Pointcut("within(pl.turo.spring.aop.service.ExampleService)")
    private void withinPointcut(){}
    
    @Before("logAnnotationValue(log) && withinPointcut()")
    public void trackLog(JoinPoint joinPoint, Log log) {
     System.out.println(log.logText());
    }
    
    W ten sposób ograniczyliśmy się do klasy ExampleService i tylko w niej szukamy publicznych metod oznaczonych adnotacją Log.
    Wniklimy Czytelnik zapewne zauważy, że jest to bardzo podobne do paterna z execution, przecież moglibyśmy mieć tak:
    @Pointcut("execution(* pl.turo.spring.aop.service.ExampleService.*(..))")
    
    I osiągamy to samo. Ok, ale zauważmy że within jest dedykowane do ograniczania się do jakiegoś pakietu/klasy. Korzystając z nazawnych pointcut-ów możemy ograniczać się do poszczególnych warstw aplikacji w przejrzysty sposób.
    @Pointcut("within(com.companyname.dao..*)")
    public void daoLayer(){}
    
    @Pointcut("within(com.companyname.service..*)")
    public void serviceLayer(){}
    
    @Pointcut("within(com.companyname.gui..*)")
    public void guiLayer(){}
    
    I teraz możemy pisać tak tego użyć :
    @Before(logAnnotatioValue(log) && serviceLayer())
    
    Jak na moje oko czytelne : chcemy publiczne metody z warstwy service z adnotacją Log.

    To tyle w tematyce definiowania pointcut-ów. Temat jest o wiele szerszy, niż mój wpis, ale wydaje mi się, że wiedza zebrana w tym poście wystarczy do rozpoczęcia pracy ze spring aop i porywa sporą część standardowych problemów. Stąd można pobrać źródła projektu, na którym testowałem opisywane pointcut-y.
    W następnym wpisie opiszę różne typy advice-ów.

    Brak komentarzy:

    Prześlij komentarz