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żą:
Podział ten odnosi się do podziału ze względu na typ :
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 :
Podsumowując: dzięki użyciu 'args' otrzymaliśmy tutaj dwie rzeczy :
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 :
A co jeśli chcielibyśmy zmodyfikować nazwane poincut-y tak, aby to one wyciągały nam interesujący nas kontekst :
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:
W metodach ExampleServie dodałem sysout aby na konsoli łatwo móc zweryfikować którą metodę zawołaliśmy.
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ą:
Jeśli chcielibyśmy z tej adnotacji wyciągnąć jakieś wartości to możemy zrobić tak:
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:
Możemy również chcieć ograniczyć się do szukania join point-ów tylko w określonych pakietach lub określonej klasie, robimy to tak:
Wniklimy Czytelnik zapewne zauważy, że jest to bardzo podobne do paterna z execution, przecież moglibyśmy mieć tak:
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.
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ą.
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:
- Zdefiniowałem poincut 'oneParameter' wyciągający nam z jakiegokolwiek podpakietu 'pl' interfejs Service (i każdy po nim dziedziczący).
- Na tym interfejsie szukamy metod o nazwie zaczynającej się od 'get' i dokładnie jednym parametrze.
- Nad advice (metoda trackLog) napisałem pointcut, który korzysta z pointcut-a 'oneParameter' oraz dokłada warunek args(message).
- Ten drugi warunek odnosi się do parametru metody trackLog - szuka parametru o nazwie 'message'.
- Następnie sprawdza jakiego typu jest ten argument. Widzi, że typem jest String.
- Znając już ten typ pointuct odfiltrowuje się tylko do metod mających jeden parametr typu String.
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.
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) ^ stringWidać, ż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.
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