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...

Brak komentarzy:

Prześlij komentarz