czwartek, 7 marca 2019

Odmagicznianie

Patrząc na aplikację na jednym końcu jest framework (tu i ówdzie nazywany przez starych ludzi zrębem) a na drugim assembler, kod maszynowy, elektrony czy inne kwarki w zalezności od tego jak głęboko chcesz wchodzić. Im wyższy poziom tym - w teorii - powinno się szybciej pracować, gdyż ukrywana przed nami jest skomplikowana drobnica, ale - w praktyce - i tak prędzej czy później dostaniesz plaskacza w twarz od warstw poniżej (tutaj można klasycznie zaliczkować do artykułu o "cieknących abstrakcjach" z 1413 roku)

No i miała miejsce taka konkretna sytuacja. Robię warsztaty z nowego API Kotlina na Springu. To API jest fajne bo tam nie ma żadnych adnotacji (dlaczego to jest fajne to jeszcze o tym w naszym artykule będzie) i wykorzystując wiele z bardziej zaawansowanych kotlinowych mechanizmów pozwala zbudować konfigurację beanów,restów itd tylko i wyłącznie przy pomocy czystego kodu.

Warsztaty były z Kotlina. To API springowe dosyć dobrze napisane to myślę sobie - będzie to dobra ilustracja mechanizmów. Jednakże po zanurkowaniu w głąb kodu kilku osobom poleciała para z mózgu i tam przy okazji smoltoków padła taka koncepcja, ze w sumie może lepiej było zostać na poziomie wywołań i nie wchodzić w szczegóły.

Otóż wydaje mi się, że jednak nie byłoby lepiej. I to nie tylko dlatego, że taka przygoda ma to samo w sobie wartość edukacyjną w kontekście nauki Kotlina. Jest jeszcze jeden aspekt całej sprawy...

#10yearsczelendż - JDD 2009

Rok 2009 - czyli w świeci informatyki jakieś dwa wieki temu - był rokiem ciekawym. Wyobraź sobie, że świat jest w czymś czego młodzież IT rzucająca 10K na wejście jeszcze nie zna a co się nazywa "Kryzys Światowy".Java 6 jest szczytem nowoczesności. Youtube ma rozdzielczość 320p a zamiast fejsbuka jest nasza-klasa.

I właśnie wtedy miałem przyjemność uczestniczyć w konferencji JDD 2009 gdzie przyjechał pewien typ od JMS. No i tenże człowiek miał między innymiwykład o tym "jak NIE życ" czyli o antywzorcach w programowaniu. Lub coś takiego, coś w ten deseń. Było kilka ciekawych antywzorców ale nas interesuje ten jeden gdzie było coś o "magi" i pamiętam, że pokazał kawałek kodu z adnotacją @Transactional.


@Transactional
public void metoda(){
..
} 

To był Spring 3.coś i kontekst użycia magi polegał na tym, że ludzie wrzucali te adnotacje i mało kto wgłębiał się jak to działa. No i okazało się, ze w zależności od typu wyjątku jaki poleci - checked czy unchecked - działa to inaczej. Czasem będzie rollback a czasem nie (przynajmniej tak to działało A.D. 2009 )

Separacja i edukacja

Pomocne nam teraz będą dwa kawałki kodu.

Pierwszy przedstawia konfigurację RESTa adnotacjami. I widząc ten kawałek kodu w zasadzie trudno jest stwierdzić co się stanie. Trudno jest stwierdzić kiedy to się stanie.Trudno odpowiedzieć na pytanie czy jakiś wrapper będzie generowany w trakcie kompilacji, a może coś będzie czytać przez refleksję te adnotacje i jakaś bliżej nieokreślona logika pojawi się naokoło naszej klasy?

Dokładne przestudiowanie dokumentacji jest potrzebne - co samo w sobie nie musi być złe - ale to wymaga mocy skupienia aby zadość uczynić warunkowi "dokładne". A jak życie pokazuje dokumentacja może mieć nieścisłości, może mieć braki i dziury. Być może jest jakiś specjalny plugin do IDE, który powiąże adnotacje z miejscem wywołania i jakoś zbadać ten kod - ale raz, że nie wnikałem czy w ogóle coś takiego istnieje - a dwa, że uzależnia nas to od kolejnej rzeczy, kolejnego narzędzia.

  1. @RestController
  2. @RequestMapping("/foos")
  3. class FooController {
  4.  
  5. @Autowired
  6. private IFooService service;
  7.  
  8. @GetMapping
  9. public List<Foo> findAll() {
  10. return service.findAll();
  11. }
  12.  
  13. @GetMapping(value = "/{id}")
  14. public Foo findOne(@PathVariable("id") Long id) {
  15. return RestPreconditions.checkFound( service.findOne( id ));
  16. }

Do tego dochodzi jeszcze jeden ważny aspekt czyli - "kiedy tak naprawdę dowiem się, że coś zrobiłem źle i ile czasu zmarnuje aby rozpocząć naprawę buga". Kompilacja następuje (czasem dużo) wcześniej niż testy/odpalenie aplikacji - także jeśli czas to pieniądz to jest widoczna oszczędność pieniędzy. I znowu mogą pomóc jakieś pluginy do IDE tylko,że wiecie - naprawdę te pluginy to rozwiązanie sztucznego problemu, który sami sobie zrobiliśmy...

Teraz patrz na to:

  1. fun helloRouterFunction(): RouterFunction<ServerResponse> {
  2. //This one uses official kotlin DSL for building Routerfunction and Handlerfunctions
  3. return router {
  4. GET("/hello") { _ ->
  5. //and below is HandlerFunction
  6. ok().body(just("Hello World!"), String::class.java)
  7. }
  8. }
  9. }

Ok, jest to jakaś konstrukcja, której działania muszę się domyśla. Dokumentacja może być a może jej nie być. A Może mam jakieś specyficzne scenariusze, których nie ma w dokumentacyjnych hello worldach? Tutaj mam opcję! To zwykły kod. Można pobrać źródła , ctrl+b i juz patrzę co to za router

  1. fun router(routes: RouterFunctionDsl.() -> Unit) =
  2. RouterFunctionDsl().apply(routes).router()

Jest to prosta linijka kodu - prosta kiedy znasz kotlina, ale po to robię warsztaty - i jak już tego kotlina się umie czytać to to jest bardzo oczywiste co tutaj się dzieje. Jest taki builderek nazwany tutaj DSLem , do którego wpakujemy ustawienia routera. Na końcu jak to w builderach bywa jest wywołanie metody build , która tutaj nazywa się router

I teraz taka zagadka. Dlaczego mogę sobie napisać GET w środku routera ale już poniższe się nie kompiluje?

  1. GET("/hello") { _ ->
  2. //and below is HandlerFunction
  3. // Handler function is explained in IntroSpring2
  4. //and Reactor api in IntroSpring2 and MonoDemo
  5. ok().body(just("Hello World!"), String::class.java)
  6. }
  7. return router {
  8. //for explanation how GET is build take a look at IntroSpring2
  9. }

By odmagicznić sytuację zwyczajnie włażę w implementację tej metody i widzę, ze jest to metoda instancji wcześniej widzianego buildera, która modyfikuje stan tejże instancji - NO RACZEJ ma to sens by wołać ja w kontekście jakiegoś obiektu, EJ HELOŁ!

  1. /**
  2. * Route to the given handler function if the given request predicate applies.
  3. * @see RouterFunctions.route
  4. */
  5. fun GET(pattern: String, f: (ServerRequest) -> Mono<ServerResponse>) {
  6. routes += RouterFunctions.route(RequestPredicates.GET(pattern), HandlerFunction { f(it) })
  7. }

Wędka zamiast adnotacji

No i morał z tego taki, że im to wszystko, całe te mechanizmy są mniej przekombinowane tym naprawdę łatwiej rozkminić co się dzieje bo dokumentacji czasami jest jej jak na lekarstwo. Np. to Nowe API kotlinowe w springu to na jakimś blogu znalazłem opisane a dalej to sam już drążyłem i wynikiem sa materiały na gitbooku :

No i też te całe adnotacje interpretowane w runtime to trochę taki PHP napisany w Javie, macie dwa poziomy języka. To jest złe.

Warsztat na segfault university

Ten temat w formie 4-wymiarowego warsztatu poruszę na Segfault University w gdańsku w następnym tygodniu. Zapisywać można się chyba gdzieś tutaj : http://segfault.events/sites/gdansk2019/speakers/pawel-wlodarski/

sobota, 19 stycznia 2019

Kotlin plus Spring minus Adnotacje

Spring 5 dodał nowy - bardzo wygodny - sposób konfiguracji naszych przy użyciu prostych funkcji STOP Owo podejście nie wymaga stosowania adnotacji ani manipulacji bajtkodem STOP Co więcej Spring dostarcza natywne wsparcie dla Kotlina wykorzystując jego specyficzne mechanizmy, które znacznie upraszczają kod STOP A ponieważ nie jednego flejma o technologiach widziałem to we filozofie brnąć nie będę STOP A Tutaj jak ktoś chce są opisane problemy z adnotacjami The case against annotations STOP

Skoro o kodzie mowa

Rozpoczynając prezentację - dla potrzeb naszego przykładu edukacyjnego stworzymy następująca kolekcję typów, która to kolekcja pozwoli nam zobrazować jak wygląda konfiguracja z użyciem Kotlinowego DSL-a. Jakies interfejsy, kilka 'binów' i działamy :

interface I1
interface I2
class Bean1 : I1
class Bean2(dependency:I1) : I2
class Bean3(dependency:I2)

Aby użyć DSLka wystarczy jeden import :

import org.springframework.context.support.beans

No i już możemy pracować z bardzo - w moim odczuciu - przyjemnym kodem. I w moim osobistym odczuciu to naprawdę jest dosyć czytelna konfiguracja, która jednocześnie jest silnie typowana, czyli kompilator pilnuje nam pleców ,wyłapuje wszelkie babole , troszczy się itd:

  1. beans {
  2. bean<Bean1>()
  3. bean{
  4. val i1:I1=ref<Bean1>()
  5. Bean2(i1)
  6. }
  7. bean("Bean3"){
  8. val i2:I2=ref<Bean2>()
  9. Bean3(i2)
  10. }
  11. }

Powyższy fragment jest standardowym kodem Kotlina - nie ma tam żadnej magii a jedynie wykorzystanie natywnych mechanizmów języka. Deklarujemy nasze trzy beany pobierając zależności poprzez wywołanie metody. Wszystko jest kodem. Wszystko jest w jednym miejscu. Można to ogarnąć łatwo. Chaosu nie ma.

A jeśli nie do końca jeszcze wiadomo to, by lepiej zrozumieć sytuację zerknijmy w głąb metod. Mamy tego MR. beans i wygląda to nawet przyjaźnie ale tenże argument init jakis taki dziwny. Jakieś nawiasy po kropce. Co... co się tutaj stanęło?

  1. fun beans(init: BeanDefinitionDsl.() -> Unit): BeanDefinitionDsl {
  2. val beans = BeanDefinitionDsl()
  3. beans.init()
  4. return beans
  5. }

Otóż cała zabawa z tym DSLem jest możliwa dzięki zapisowi “init: BeanDefinitionDsl.() -> Unit” , który definiuje BeanDefinitionDsl jako odbiorcę wywołań w lambdzie (z engielska 'receiver'). Czyli dla przykładu we fragmencie

  1. beans {
  2. bean<Bean1>()
  3. ...
  4. }

Mamy 'implicit this' (dokładnie tak!) i tak naprawdę, wywołujemy

  1. beans { dsl ->
  2. dsl.bean<Bean1>()
  3. ...
  4. }

Teraz pojedynczy 'bean'. Tutaj pojawiają się dziwne nowe słowa:

  1. inline fun <reified T : Any> bean(....){....}

W tej metodzie dzieje się trochę więcej. Generalnie para słów kluczowych “inline” i “reified” w sprytny sposób zachowa informacje o typie z generyka w 'Runtime', dzięki czemu możliwe będzie utworzenie nowej instancji klasy Bean1 podanej w generyku. Podobna mechanika kryje się za metodą “ref”, która zwraca nam referencję do już zadeklarowanych beanów.

  1. inline fun <reified T : Any> ref(name: String? = null) : T =
  2. when (name) {
  3. null -> context.getBean(T::class.java)
  4. else -> context.getBean(name, T::class.java)
  5. }

Zauważ drogi czytelniku/czytelniczko iż zapis “T::class.java” robi coś niesamowitego i wyciąga klasę z generyka. Magia, po prostu magia….

Chociaż nie. Nie magia a nauka.

Inicjalizacja

Caly mechanizm "beans" to tak naprawdę zwykły wzorzec builder i tak jak na klasycznym buiderze wołaliśmy “build()” lub coś podobnego na sam koniec by zapieczętować budowę - tak tutaj przekazujemy do budowy springowy kontekst. Trochę to wygląda na cykliczną zależność ale w zasadzie nie zauważyłem jeszcze z tym problemów.

  1. val ourInit: BeanDefinitionDsl =beans {...}
  2. GenericApplicationContext {
  3. ourInit.initialize(this)
  4. refresh()
  5. }

I kontekst springowy zainicjalizowany naszymi beanami. Żadnych adnotacji nie trzeba, żadnego CGLiba i majstrowania przy bajtkodzie też nie trzeba!!! Ale to nie koniec. Kotlin i Spring 5 dają nam jeszcze więcej udogodnień bo teraz
skupcie się weźta
wystawimy resta

konfiguracja REST - uważaj na przykłady Javy!

Przede wszystkim czytając tutoriale do nowego mechanizmu - jak na przykład ten https://spring.io/blog/2016/09/22/new-in-spring-5-functional-web-framework - uważaj na użytą składnie Javy, gdyż używając w identyczny sposób Kotlina możesz napotkać na pewne trudności. Otóż API dla Javy zaleca użycie funkcji “route”, która jest zdefiniowana w sposób następujący

  1. public static <T extends ServerResponse> RouterFunction<T> route(
  2. RequestPredicate predicate, HandlerFunction<T> handlerFunction) {
  3.  
  4. return new DefaultRouterFunction<>(predicate, handlerFunction);
  5. }
  6.  

Gdzie "HandlerFunction" to prosty interfejs funkcyjny:

  1. @FunctionalInterface
  2. public interface HandlerFunction<T extends ServerResponse> {
  3. Mono<T> handle(ServerRequest request);
  4. }

Używając tego API możemy wygodnie wpisać sobie lambdę tam gdzie oczekiwany jest HandlerFunction a to dlatego, iż Java z automatu konwertuje ową lambdę na tzw. SAM czyli klasę abstrakcyjną/interfejs z jedną metodą. Dzięki temu możemy sobie napisać route(GET(“/”),r->...) . Niestety w Kotlinie przy konwersji lambdy na typ Javy jest potrzebna dodatkowa podpowiedź dla kompilatora:

  1. //route(GET("/test"), { r -> ok().body(Mono.just("response")) }) <- to nie pyknie
  2.  
  3. //poniżej podajemy podpowiedź, że ta lambda to HandlerFunction i działa
  4. route(GET("/test"), HandlerFunction { r -> ok().body(Mono.just("response")) })

Dlatego też warto używać dedykowany DSL -wiecie taki natywny ze springa a nie żadny na boku robiony - napisany specjalnie dla Kotlina. Konstrukcja tam użyta jest podobna do tego co widzieliśmy w poprzednim odcinku dla definicji Beanów - czyli dla przypomnienia:

  1. fun beans(init: BeanDefinitionDsl.() -> Unit): BeanDefinitionDsl {
  2. val beans = BeanDefinitionDsl()
  3. beans.init()
  4. return beans
  5. }

W przypadku definiowania “routingu” wygląda to bardzo ale to bardzo podobnie :

  1. fun router(routes: RouterFunctionDsl.() -> Unit) = RouterFunctionDsl().apply(routes).router()
  2.  
  3. router {
  4. GET("/hello") { _ ->
  5. ok().body(just("Hello World!"), String::class.java)
  6. }
  7. }

Jak możecie chyba zgadnąć GET to nic innego jak wywołanie metody na naszym DSLu i aby było jawne, jasne i klarowne co tam się dzieje to dzieję coś takiego :

  1. router {
  2. val dsl:RouterFunctionDsl = this //<- o tutaj cała magia
  3. dsl.GET("/hello") { _ ->
  4. //and below is HandlerFunction
  5. ok().body(just("Hello World!"), String::class.java)
  6. }
  7. }

W powyższym przykładzie GET jest po prostu wygodnym opakowaniem na to API dla Javy, o którym mówiliśmy na samym początku :

  1. fun GET(pattern: String, f: (ServerRequest) -> Mono<ServerResponse>) {
  2. routes += RouterFunctions.route(RequestPredicates.GET(pattern),
  3. HandlerFunction { f(it) })
  4. }
  5.  
  6.  

I problem “bojlerplejtu” rozwiązany.

Mono

Zwróć uwagę na sygnaturę funkcji przekazanej do Handlera :

f: (ServerRequest) -> Mono

Czym jest to Mono i jak wpływa na przetwarzanie requestu? O tym w innym odcinku...