Mateusz Mazurek – programista z pasją

Blog o Pythonie i kilku innych technologiach. Od developera dla wszystkich.

AlgorytmikaInżynieria oprogramowaniaProgramowanieProgramowanie webowe

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:Schemat warstw

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")
publicclass UserController extends AbstractController implements CommonController{

    privatestaticfinal 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);
        returnnew Response(u);
    }
    @Override
    public Response get(@PathVariable long id){
        User u = userService.get(id);
        returnnew Response(u);
    }
    @Override
    public Response list(){
        List<User> u = userService.list();
        returnnew Response(u);
    }
    @Override
    public Response delete(@PathVariable long id){
        userService.delete(id);
        returnnew Response(mapForDelete(id));
    }
    @Override
    public Response update(@PathVariable long id, @RequestBody Map<String, Object> values){
        User u = userService.update(id, values);
        returnnew 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
publicinterface 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
publicclass UserService extends AbstractService<User>{
   
    privatestaticfinal Logger logger = Logger.getLogger(UserService.class.getName());
   
    public UserService(){
    }

    @Transactional
    @Override
    public User create(Map<String, Object> values){
        returnsuper.create(values);
    }
   
    @Transactional
    @Override
    publicboolean delete(long entity){
        returnsuper.delete(entity);
    }

    @Transactional
    @Override
    public User update(long id, Map<String, Object> values){
        returnsuper.update(id, values);
    }

    @Transactional
    @Override
    public User get(long id){
        returnsuper.get(id);
    }

    @Override
    @Transactional
    public List<User> list(){
        returnsuper.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 @interfaceSynchronized{
   
}

a teraz nasz aspekt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Aspect
@Component
publicclass SynchronizingAspect {
    @Pointcut("execution(* *.*(..))")
    protectedvoid start(){
    }
    privatestaticfinal Logger logger = Logger.getLogger(SynchronizingAspect.class.getName());
    @Around("start() && @annotation(Synchronized)")
    publicObject synchronize(ProceedingJoinPoint joinPoint)throwsThrowable{
        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
publicclass LoggingAspect {

    privatestaticfinal Logger logger = Logger.getLogger("GlobalLogger");
    @Before("@annotation(Logged)")
    publicvoid 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 =newThread(newRunnable(){
               
                privateint i;
                publicRunnable set(int i){
                    this.i=i;
                    returnthis;
                }

                publicvoid run(){
                    User u = userService.create(values, i);
                }
            }.set(i));
           
            t.start();
        }
       
        returnnew 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);
        returnnull;
    }

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.

Dzięki za wizytę,
Mateusz Mazurek

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

1 Komentarz
Inline Feedbacks
View all comments