Programowanie aspektowe w Spring 4 – synchronizacja
Programowanie aspektowe.. Czyli kolejny paradygmat tworzenia aplikacji. Polega on na definiowaniu warstw abstrakcji oraz nakładaniu ich na siebie tak aby tworzyły całość.
Na potrzeby tego wpisu zdefiniujmy sobie kilka takich warstw. Pisząc „warstwa” mam na myśli zbiór klas które wykonują operacje z konkretnego zakresu. A więc niech to wygląda np. tak:
- Warstwa logiki biznesowej
- Warstwa synchronizacji
- Warstwa logowania
Warstwa logiki biznesowej to zbiór serwisów które wykonują operacje na danych.
Warstwa synchronizacji będzie to warstwa dodająca do warstwy biznesowej synchronizację wielu wątków które za pośrednictwem serwisów wykonuje różne operacje. Warstwa logowania będzie nam logowała fakt wykonania się konkretnej metody.
A więc schemat może wyglądać tak:
Tutaj logicznym jest że w naszej aplikacji warstwy będą nakładane na warstwę logiki biznesowej. Warto tutaj abym dodał że idea warstw jest abstrakcyjna a więc nie ma dosłownego jej odwzorowania w kodzie. Warstwa to agregacja klas które wykonują swoje zadania na danych innych warstw.
Tyle słowem wstępu ;) przykład pokażę na frameworku Spring 4. Przykład będzie dotyczył aplikacji pisanej jako RESTful Web Service.
A więc napiszmy kontroler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | @RestController @RequestMapping(value = "users") public class UserController extends AbstractController implements CommonController{ private static final Logger logger = Logger.getLogger(UserController.class.getName()); @Autowired private UserService userService; @Override public Response create(@RequestBody Map<String, Object> values) { User u = userService.create(values); return new Response(u); } @Override public Response get(@PathVariable long id) { User u = userService.get(id); return new Response(u); } @Override public Response list() { List<User> u = userService.list(); return new Response(u); } @Override public Response delete(@PathVariable long id) { userService.delete(id); return new Response(mapForDelete(id)); } @Override public Response update(@PathVariable long id, @RequestBody Map<String, Object> values) { User u = userService.update(id, values); return new Response(u); } } |
Klasa z której dziedziczy ten kontroler jest abstrakcyjna i nie zawiera istotnych rzeczy a więc sobie ją pominiemy. Interfejs który implementuje nie jest niczym odkrywczym:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public interface CommonController { @RequestMapping(method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.CREATED) public Response create(@RequestBody final Map<String, Object> values); @RequestMapping(value="/{id}", method = RequestMethod.GET) @ResponseStatus(value = HttpStatus.OK) public Response get(@PathVariable long id); @ResponseStatus(value = HttpStatus.OK) @RequestMapping(value="/", method = RequestMethod.GET) public Response list(); @ResponseStatus(value = HttpStatus.OK) @RequestMapping(value="/{id}", method = RequestMethod.DELETE) public Response delete(@PathVariable long id); @ResponseStatus(value = HttpStatus.ACCEPTED) @RequestMapping(value="/{id}",method = RequestMethod.PUT) public Response update(@PathVariable long id, @RequestBody final Map<String, Object> values); } |
Ten interfejs można by poprawić.. Mam na myśli kody HTTP które są zwracane. Ale zostawiam to na razie.
Wertując ten strasznie skomplikowany kod widzimy że mamy mapowany link http://example.com/users/ (POST) na tworzenie użytkownika. Zerknijmy więc na serwis:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | public class UserService extends AbstractService<User>{ private static final Logger logger = Logger.getLogger(UserService.class.getName()); public UserService() { } @Transactional @Override public User create(Map<String, Object> values) { return super.create(values); } @Transactional @Override public boolean delete(long entity) { return super.delete(entity); } @Transactional @Override public User update(long id, Map<String, Object> values) { return super.update(id, values); } @Transactional @Override public User get(long id) { return super.get(id); } @Override @Transactional public List<User> list() { return super.list(); } } |
I taką konstrukcję możemy uznać za jakąś część warstwy logiki biznesowej.
Teraz może czas na wytłumaczenie bym są te aspekty. Aspekt to klasa która posiada odpowiednią adnotację. Klasa ta posiada metody które mogę zostać wykonane przed (@Before), po (@After), po poprawnym wykonaniu się (@AfterReturning), po wyrzuceniu wyjątku (@AfterThrowing), zamiast (@Around) innej metody dowolnej klasy. Realizowane jest to na dwa sposoby, albo poprzez tworzenie proxy do obiektu na którym działamy (aspectjweaver i aspectjrt w pomie + konfiguracja) albo modyfikacja bytecodu (CGLIB + konfiguracja). Aspekty możemy podpinać tylko na metody publiczne.
Chcemy teraz stworzyć warstwę która będzie synchronizowała wykonywanie się metod w pracy na wielu wątkach. Chcemy jakoś oznaczyć które metody mają być synchronizowane, wiec stwórzmy adnotację:
1 2 3 4 5 | @Target({ ElementType.METHOD }) @Retention(value = RetentionPolicy.RUNTIME) public @interface Synchronized { } |
a teraz nasz aspekt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @Aspect @Component public class SynchronizingAspect { @Pointcut("execution(* *.*(..))") protected void start(){ } private static final Logger logger = Logger.getLogger(SynchronizingAspect.class.getName()); @Around("start() && @annotation(Synchronized)") public Object synchronize(ProceedingJoinPoint joinPoint) throws Throwable { Object that = joinPoint.getThis(); synchronized(that){ return joinPoint.proceed(); } } } |
W ten sposób określiliśmy iż metoda synchronize ma się wykonać dla zamiast każdej metody oznaczonej adnotacją @Synchronized. Ciało tej metody to ręczne wywołane metody która powinna się wykonać w miejscu aspektu z tym że w bloku synchronized. I to tyle :)
Teraz czas na na warstwę logowania. Niech to będzie znów adnotacja:
1 2 3 4 5 | @Target({ ElementType.METHOD }) @Retention(value = RetentionPolicy.RUNTIME) public @interface Logged { } |
I aspekt:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Aspect @Component public class LoggingAspect { private static final Logger logger = Logger.getLogger("GlobalLogger"); @Before("@annotation(Logged)") public void log(JoinPoint joinPoint){ Signature methodSignature = joinPoint.getSignature(); String methodName = methodSignature.getName(); String className = joinPoint.getThis().getClass().getName(); logger.log(Level.INFO, className+methodName); } } |
Sprawdźmy jak to działa :)
Przepiszmy kontroler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @Override public Response create(@RequestBody final Map<String, Object> values) { for(int i=0;i<20;i++){ Thread t = new Thread(new Runnable() { private int i; public Runnable set(int i){ this.i=i; return this; } public void run() { User u = userService.create(values, i); } }.set(i)); t.start(); } return new Response(); } |
i serwis:
1 2 3 4 5 6 7 8 | @Transactional @Override public User create(Map<String, Object> values, int i) { logger.log(Level.INFO, "Started: "+i); User o = super.create(values, i); logger.log(Level.INFO, "Stopped: "+i); return null; } |
Kawałek kodu jest prosty. Uruchamiany tworzenie użytkownika w 20 wątkach bez @Synchronized. Zwróć uwagę jak przekazać do klasy anonimowej niefinalną zmienną bo to przydatny trick.
Po wykasowaniu hiper ilości logów Springa widzimy taki efekt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 07-Mar-2015 16:52:01.808 INFO [Thread-12] pl.flomedia.springtpl.services.UserService.create Started: 6 07-Mar-2015 16:52:01.812 INFO [Thread-24] pl.flomedia.springtpl.services.UserService.create Started: 18 07-Mar-2015 16:52:01.818 INFO [Thread-7] pl.flomedia.springtpl.services.UserService.create Started: 1 07-Mar-2015 16:52:01.833 INFO [Thread-19] pl.flomedia.springtpl.services.UserService.create Started: 13 07-Mar-2015 16:52:01.820 INFO [Thread-20] pl.flomedia.springtpl.services.UserService.create Started: 14 07-Mar-2015 16:52:01.884 INFO [Thread-17] pl.flomedia.springtpl.services.UserService.create Started: 11 07-Mar-2015 16:52:01.886 INFO [Thread-9] pl.flomedia.springtpl.services.UserService.create Started: 3 07-Mar-2015 16:52:01.901 INFO [Thread-23] pl.flomedia.springtpl.services.UserService.create Started: 17 07-Mar-2015 16:52:01.904 INFO [Thread-13] pl.flomedia.springtpl.services.UserService.create Started: 7 07-Mar-2015 16:52:01.909 INFO [Thread-15] pl.flomedia.springtpl.services.UserService.create Started: 9 07-Mar-2015 16:52:01.912 INFO [Thread-21] pl.flomedia.springtpl.services.UserService.create Started: 15 07-Mar-2015 16:52:01.950 INFO [Thread-6] pl.flomedia.springtpl.services.UserService.create Started: 0 07-Mar-2015 16:52:01.952 INFO [Thread-16] pl.flomedia.springtpl.services.UserService.create Started: 10 07-Mar-2015 16:52:01.953 INFO [Thread-11] pl.flomedia.springtpl.services.UserService.create Started: 5 07-Mar-2015 16:52:01.954 INFO [Thread-18] pl.flomedia.springtpl.services.UserService.create Started: 12 07-Mar-2015 16:52:01.957 INFO [Thread-25] pl.flomedia.springtpl.services.UserService.create Started: 19 07-Mar-2015 16:52:01.957 INFO [Thread-8] pl.flomedia.springtpl.services.UserService.create Started: 2 07-Mar-2015 16:52:01.962 INFO [Thread-22] pl.flomedia.springtpl.services.UserService.create Started: 16 07-Mar-2015 16:52:02.001 INFO [Thread-10] pl.flomedia.springtpl.services.UserService.create Started: 4 07-Mar-2015 16:52:02.005 INFO [Thread-14] pl.flomedia.springtpl.services.UserService.create Started: 8 .. |
A więc wszystko wykonuje się równolegle bez jakiejkolwiek synchronizacji. Dodajemy @Synchronized i uzyskujemy zsynchronizowane pary logów:
1 2 3 4 5 6 | 07-Mar-2015 16:54:07.737 INFO [Thread-41] pl.flomedia.springtpl.services.UserService.create Started: 15 07-Mar-2015 16:54:07.755 INFO [Thread-41] pl.flomedia.springtpl.services.UserService.create Stopped: 15 07-Mar-2015 16:54:07.941 INFO [Thread-37] pl.flomedia.springtpl.services.UserService.create Started: 11 07-Mar-2015 16:54:07.948 INFO [Thread-37] pl.flomedia.springtpl.services.UserService.create Stopped: 11 itp.. |
Zauważ że bez dużej ingerencji w kod, bez zbędnego powielania bloku synchronized, zaimplementowaliśmy synchronizację metod.
Podobnie można sprawdzić czy logowanie działa dodając @Logged do metod – nie będę wklejał kodu – działa :)
Sama idea aspektów jest generalnie nawet trochę genialna ;) warto się zainteresować nimi.
Matt.
Mateusz Mazurek