Podczas pierwszego
slotu przemieszczałem się pomiędzy "Spring into the Next Decade" a "Paradoxes of API Design". Oryginalnie chciałem iść na Spring, ale jak się okazało Josh Long zdecydowanie nie miał pomysłu na aż 3 godzinną prezentacje. W efekcie przeprowadził ją jako kompilacje z kilku krótszych opuszczając przy tym kilka szczegółów. Przy omawianiu podstaw konfiguracji
Spring Framework przeniosłem się obok posłuchać o API. Prezentacja była dość ciekawa, ale trochę zbyt teoretyczno-filozoficzna (podobno w dalszej części powiało trochę praktyką). Jaroslav opowiadał o tym, że wiedzę o tym jak tworzyć API czerpiemy głównie ze szkoły i środowiska, w kŧórym pracujemy. Dokonał podziału potencjalnych użytkowników naszego API na:
rationalist (ci którzy zanim zaczną działać najpierw pogłębiają swoją wiedzę o API przez czytanie dokumentacji, książek, artykułów),
empirist (ci którzy uczą się poprzez prototypownie robione "rozpoznanie ogniem") oraz
clueless (ci co nie mają zbytnio pojęcia i nie mają czasu/nie chcą się uczyć). Zdecydowaną większość mają stanowić ci ostatni. Szczególnie dla nich przygotowywana jest dokumentacja w formie gotowych
use-cases, które mają poprowadzić ich jak po sznurku do realizacji najczęstszych scenariuszy. Przy tworzeniu API bardzo ważne jest zachowanie spójności (np. sposób konfiguracji związanej z zaczytaniem konfiguracji z pliku czy z BD ma być identyczny) oraz projektowanie w taki sposób aby ewentualnie umożliwić późniejsze zmiany zachowując kompatybilność. Jaroslav następnie omówił różne poziomy kompatybilności (np. na poziomie kodu źródłowego, na poziomie binarnym) oraz potencjalne
gotchas z nimi związane. W tym przydługawym momencie wróciłem na prezentacje o Spring i tu powoli zaczęło się coś dziać. Josh omawiał dostępne
scope komponentów (
conversation/flow/view web scopes, oraz
process scope wykorzystywany przez
ACTIVITI BPMN 2). Następnie omówił kilka infrastrukturalnych mechanizmów Springa:
BeanPostProcessor (użyteczne do modyfikowania/opakowywania
beanów),
BeanFactoryPostProcessor (modyfikowanie definicji
beanów, dodawanie
beanów, zaczytywanie konfiguracji
beanów z alternatywnych źródeł, np. BD),
FactoryBean (bardziej złożone tworzenie beanów, enkapsulacja potencjalnie złożonego kodu tworzącego
beana). Oprócz teorii pokazał kilka przykładów wykorzystania w/w struktur:
CorporationCodeStandardsAwareBeanFactoryPostProcessor, AutoToStringBeanPostProcessor, StartupAwareBeanPostProcessor, AutoToStringFactoryBean, ConditionalDelegatingPlatformTransactionManagerFactoryBean. Wszystkie przykłady kodu mają być dostępne w
git.springsource.org/spring-samples/spring-samples.git. W końcu pojawiło się trochę kodu i nowości ze Spring 3.0/3.1:
environmental beans, Spring EL (dostęp do
SystemProperties, wywołanie statycznej metody ), użycie
AnnotationConfigApplicationContext. Ważną zmianą w wersji 3.0 jest przeniesienie mechanizmu
OXM (
Object/
XML Mapping) z projektu
Spring Web Services do
Spring Framework. Dzięki temu mamy spójny mechanizm do wykorzystania w isntniejących i nowych projektach/modułach:
Spring Batch, Spring Integration, Spring MVC, Spring REST. Spring 3.0 dostarcza także prosty mechanizm związany z
cachowaniem. Całość jest zrealizowana za pomocą
interceptorów i nie wygląda na specjalnie skomplikowaną. Spring dostarcza
out-of-box implementacje, które jako
cache wykorzystują
Oracle Coherence, GemFire lub Ehcache. Ciekawy jest mechanizm wskazywania w annotacji w jaki sposób ma być tworzony klucz do cache (@Cacheable(key = "#p0")) - kluczem będzie pierwszy paramter wywołania metody). Josh następnie w kilku słowach przedstawił projekt
Spring Data, który wprowadza rozszerzenia dla istniejących mechanizmów DAO (np,
Spring Data JPA dla JPA), ale przede wszystkim ma być "parasolem" technologii dostępu do NoSQL datastore. Projekt jest jak najbardziej rozwojowy i nie wspiera jeszcze wielu istniejących
datastore, ale mam nadzieję że jest to tylko kwestia czasu. Josh pokazał przykład integracji z
Redis (używany przez
http://stackoverflow.com/) za pomocą
StringRedisTemplate. Nie wiem czy ze względu na to że było to jedynie demo, ale sam kod był trochę pokręcony, bo przy zapisywaniu/odczytywaniu obiektu
Customer (id, firstName, LastName) wykonywaliśmy operację na dwóch kluczach:
customer:fn: oraz customer:ln:. Wydaję mi się, że w
Redis można jako
value przechowywać
list/set wartości. Dodatkowo na potrzeby generowania kolejnych identyfikatorów trzymaliśmy dodatkowe
entry (z kluczem
customerId), które było inkrementowane przy zapisywaniu nowej instancji
Customer. Kolejne na liście "
what's new in Spring 3.0" zostały omówione mechanizmy związane z
lifecycle/scheduling/ thread pool/ asynchronous support. Oczywiście nie obyło się bez pokazu
abstraction of services i użycia
@Transactional i spójnego zarządzania transakcjami niezależnie od użytego
TransactionManager. Następnie Josh zaczał omawiać kilka projektów należących do
Spring Portfolio koncentrując się na najlepiej znanych sobie:
Spring Batch i
Spring Integration. Szczególnie ciekawie wygląda ten drugi, który ma być implementacją wzorców z książki
Enterprise Integration Patterns. Największe wrażenie zrobiła na mnie lista adapterów, których było chyba z 20 (np, umożliwiające dostęp za pomocą
POP3, IMAP, Twitter, XMPP, File, FTP, HTTP, JMS, AMQP). Josh zaczął się trochę rozwodzić na temat
AMQP i RabbitMQ, w którego to implementacje ma być mocno zaangażowana ekipa ze SpringSource. Podobno
RabbitMQ ze względu na szybkość i niezawodność jest częściej wybierany na depolymentach na EC2 od dedykowanego Amazon SQS. Nie obyło się, też bez kilku przykładów związanych z wczytywaniem "dużego" pliku csv do bazy danych za pomocą
Spring Batch. Przykład był dość trywialny, ale całkiem sensownie wygląda możliwość śledzenia stanu procesu oraz ewentualnych problemów w dedykowanych tabelach utworzonych przez
Spring Batch. Mnie najbardziej przekonało rozwiązanie combo w którym to
Spring Integration monitoruje dany katalog na dysku, a w przypadku pojawienia się nowego pliku, odpala jego przetwarzanie za pomocą
Spring Batch. Pomimo kilku prób nie udało się odpalić na emulatorze projektu wykorzystującego
Spring Android, lepiej poszło z
Spring Blazde DS, ale ze względu na brak czasu nie udało się pokazać
Spring Hadoop oraz
Spring Social. Osobiście żałowałem, że nie udało się przedstawić i omówić działań
SpringSource w obszarze
cloud computing. Jedyna informacja jaką wyłapałem to ta, że
CloudFoundry z produktu związanego z
Amazon AWS stało się platformą, która ma być niezależna od
cloud provider i (w przyszłości?) umożliwiać uruchamianie (dla celów testowych/demonstracyjnych)
cloud applications na maszynie deweloperskiej. W tym celu powstało
CloudFoundry.com,
CloudFoundry.org a w przyszłości
Cloud Foundry Micro Cloud. Więcej na temat tych projektów można znaleźć
tu.
SpringSource uruchomił swój kanał na
YouTube, na którym mają być wrzucane ich materiały. Dodatkowo warto przejrzeć
Green Beans.
"Patterns and Anti-Patterns in Hibernate" by Patrycja Węgrzynowicz - no prezentacja była dość przeciętna. Tematy, które zostały poruszone były całkiem ciekawe, ale było ich zdecydowanie za mało na 3 godziny. Na sam początek się trochę spóźniłem i trafiłem na omówienie rozwiązania problemu współbieżnego dostępu do danych w aplikacji
CaveatEmptor z "Hibernate In Action". Problemem ma być implementacja funkcjonalności dodawania nowego bida. Algorytm jest taki, że przy dodawaniu nowego
bida, sprawdzamy czy jest on większy od aktualnie najwyższej oferty. Implementacja jest następująca:
public someMethod(){
Bid currentMinBid = itemDao.getMinBid(itemId);
Bid currentMaxBid = itemDao.getMaxBid(itemId);
//lock for Upgrade
Item item = itemDao.getItemById(itemId, true);
Bid newBid = item.placeBid(userDao.findById(userId), newAmount,currentMaxBid, currentMinBid);
}
Patrycja pokazała, że może dojść do sytuacji, w której to będą składane jednocześnie 2
bidy (oba większe od aktualnie najwyższego
bida): 1000 i 1002. W specyficznym scenariuszu może dojść do tego, że
bid o wartości 1002 zostanie "wypchnięty" (każdy z wątków pobierze
maxBid o wartośći 700, wątek
bid-1002 zablokuję krotke, zmieni wartość na 1002 i zrobi
commit, w tym czasie wątek
bid-1000 będzie porównywać wartość 1000 z wartością 700, bo dla niego aktualnie najwyższy
bid został wyliczony wcześniej, bid 1000 zostanie przyjęty i zostanie wykonany
commit). W ten sposób
bid o wartości 1000 zostanie dodany po
bid wartości 1002. Ma to szczególnie znaczenie gdy mapujemy
bidy jako listę a nie zbiór.
Proponowane rozwiązania to: użycie wersjonowania (rozumiem że chodzi o
optimistic locking), ustawienie poziomu izolacji na
REPEATABLE READ, ustawienie poziomu izolacji na
SERIALIZABLE, zmiana kolejności instrukcji w kodzie. Pierwsze dwa rozwiązania mają być nieskuteczne. Jeśli chodzi o poziomy izolacji transakcji to sprawa jest tym bardziej ciekawa, gdyż niektóre bazy danych,np
ORACLE nie implementują wszystkich poziomów, a nawet jeśli implementują, to implementacje mogą się od siebie różnić. Przykład rozwiązania, które nie działa, był oparty na
MySql i pokazywał, że w momencie wydawania zapytania o
item MySQL ma robić
snapshot danych i reużywać go przy kolejnych zapytaniach. W wyniku takiego działania, końcowy rezultatem ma być dodanie 2 krotek do tabeli
bidów, ale z tymi samymi wartościami w kolumnie definiującej pozycje na liście - przy następnym odczycie
bidów Hibernate ma mieć problem z ich poprawnym załadowaniem. Chyba nie do końca trafia do mnie te wytłumaczenie. Szczególnie, że spodziewam się, że MySQL zrobi
snapshot dopiero po uzyskaniu
locka. W takim przypadku
snapshot zrobiony jako drugi powinien widzieć zmiany zrobione w właśnie zakończonej transakcji. Nawet jeśli moje rozumowanie jest poprawne to chyba i tak nie da się w ten sposób rozwiązać oryginalnego problemu. Jeśli chodzi o
SERIALIZABLE to rozwiązanie ma działać, ale chyba nie podejmę się dokładnej analizy.
SERIALIZABLE zawsze automatycznie kojarzy mi się z problemem braku skalowalności i jest przyjmowane "z założenia" jako zbyt ciężkie.
Jeśli chodzi o wersjonowanie to dodanie mechanizmu
versioning bez zmiany kodu nic się nie zmieni. Wynika to z tego, że druga transakcja uzyska
lock po wykonaniu
commit pierwszej, czyli już będzie miała
zupdatowany licznik wersji. Użycie
samego optimistic locking bez lockowania, też nie pomoże (wątek
bid-1000 odczyta "stare"
min/max bid , w tym momencie wykonany będzie
commit transakcji wątku
bid-1002,
wątek bid-1000 odczyta item z wersją ustawioną przez wątek
bid-1002, ale wykona porównanie ze "starymi"
min/max bid). Rozwiązaniem jest zmiana kodu poprzez zmiane kolejności wywołań na:
public someMethod(){
//lock for Upgrade
Item item = itemDao.getItemById(itemId, true);
Bid currentMinBid = itemDao.getMinBid(itemId);
Bid currentMaxBid = itemDao.getMaxBid(itemId);
Bid newBid = item.placeBid(userDao.findById(userId), newAmount,currentMaxBid, currentMinBid);
}
W tym przypadku najpierw
lockujemy a dopiero później wykonujemy kolejne operacje.Wydaję mi się, że taki kod byłby poprawny gdybyśmy zrezygnowali z
lockowania krotki
item, użyli jedynie
optimistic locking, przy jednoczesnym
updatowaniu item przy dodawaniu
bida. Ten ostatni krok, ma jak najbardziej wartość biznesową - możemy trzymać tam datę ostatniego
bidowania. W przypadku składania dwóch jednoczesnych
bidów jedna z operacji zakończy się wyjątkiem
OptimisticLockException. Wyjątek ten można (najlepiej za pomocą
AOP) złapać a całą operację powtórzyć.
Patrycja wskazała, też alternatywne rozwiązania: obliczanie najwyższego
bid programowo (wymaga załadowania całej listy
bidów), redundentne przechowywanie najwyższego
bid w
item (denormalizacja BD, ale szybki dostęp przy ponoszeniu kosztów utrzymywania kopii informacji), wykorzystanie zaawansowanych możliwości związanych z mapowaniem. Ten ostatni pomysł wykorzystuję dość rzadko używane
feature Hibernate i polega na tym, że do
item dodajemy dodatkowe
property wraz z mappingiem, dla którego jest wyspecyfikowane zapytanie(Embedded Query) definiujące to
property. Zapytanie to ma mieć możliwość korzystania z
order by, where, groupBy. Dodatkowo, dostęp do
property możemy określić jako lazy/eager (w tym przypadku chyba najsensowniejsze jest określenie tego
property jako
set z
fetch =FetchType.Lazy). Następnie zostały przedstawione 2 dość proste błędy w kodzie
CaveatEmptor, aby w końcu przejść do większych problemów związanych z aplikacjami używającymi
Hibernate a w ogólności dowolnego
ORM. Chodzi tu o problem
Anaemic Domain Model. Rozpoczęło się od próby zdefiniowania OOP i przedstawieniu podstawowych (
encapsulation/inheritance/polimorphism ) oraz zaawansowanych (
SOLID) zasad świata
OOP. W
CaveatEmptor brak enkapsulacji jest bardzo widoczny co może spowodować wzrost ilości bugów (szczególnie w stylu czemu coś się dodało/usunęło z bazy) oraz problemy z
maintainance. Rozwiązaniem ma być używania odpowiednich modyfikatorów dostępu, najlepiej mappowanie
fields zmiast
properties,
defensive copying (w przypadku kolekcji skutkuje to pobraniem lazy mapped collection z BD),
unmodifiableCollections (możliwy problem przedstawiony w przykładzie poniżej)
. Ważne jest dodatkowo zrozumienie w jaki sposób działa mechanizm
dirty checking w
Hibernate, szczególnie w stosunku do kolekcji
entity i
embedded objects - należy re-używać kolekcje które zostały stworzone/wypełnione przez Hibernate zamiast tworzyć nowe kolekcje (
dirty checking ma wykorzystywać
identity of collections w celu stwierdzenia czy kolekcja uległa zmianie). Patrycja pokazała dość podchwytliwy przykład:
public List getBids(){ return Collections.unmodifiableList(bids);}
public void setBids(List newBids){
bids.clear();
if(newBids != null){
bids.addAll(newBids);
}
}
W momencie gdy klient wywołałby:
bids = getBids();
setBids(bids);
to z bazy mogą zostać usunięte określone
bidy. Problem jest w tym, że
Collections.unmodifiableList() stworzy nam
wrapper opakowujący podaną mu w parametrze listę, ale wszystkie zmiany na źródłowej liście są odzwierciedlane we
wrapperze. W rezultacie do
bids zostanie dodana pusta lista. Następne na tapetę trafiły problemy wydajnościowe Hibernate. Byłem bardzo zdziwiony, że nie zostały zbytnio poruszone problemy:
n+1i
cartesian product. Patrycja pokazała 2 przypadki:
- nadpisywanie listy:
A a = (A)session.get(A.class,2);
a.setBs(a.getBs());
W przypadku standardowego (a'la JavaBean) mapowania uruchomienie poniższego kodu skutkuję następującą interakcją z BD: pobraniem A z id=2, pobraniem odpowiadającej kolekcji B, usunięcie wszystkich B przypisanych do danego A, wstawienie kolejno poprzednio usuniętych B.
- oraz wykorzystanie immutable list przy mapowaniu:
public class A{
@OneToMany
public List getBs(){
Collections.unmodifiableList(bs);
}
public void setBs(List b){
this.bs = bs;
}
}
Operacja załadowanie A po identyfikatorze ma skutkować następującą interakcją z BD: pobranie A, pobranie odpowiadających B, usunięcie odpowiadających B,dodanie kolejno usuniętych przed chwilą B.
Dalej przedstawiono porównanie dostępnych strategii mapowania hierarchii. Szło to dość sprawnie i w końcu Patrycja przeszła do omówienia swojego projektu
Yonita, który ma przeprowadzać analize kodu pod kątem potencjalnych defektów,
anti-patterns, bad practices, vulnerabilities i innych takich. Zgodnie z tym co można wyczytać na stronie projektu
Yonita dokonuję analizy statycznej jak i "dynamic web testing". Przy czym to drugie ma polegać na generowaniu testów automatycznych. Chyba jestem zbyt sceptycznie do tego nastawiony, ale parę lat temu rozmawiałem z goścmi z
Parasoft i chyba przestałem wierzyć w cudowne narzędzia, szczególnie jeśli chodzi o automatyczne generowanie testów.
"Code Generation on the JVM" by Hamlet D'arcy. Kilka razy miałem przyjemność a raczej nie-przyjemność bycia na prezentacji, która omawiała różne narzędzia i metody związane z generacją kodu. Wszystkie one jednak bazowały na narzędziach do generowaniu kodu źródłowego(CORBA stub generation, WSDL2Java, Java Beans generation). Hamlet skoncentrował się na zupełnie innych narzędziach, a w szczególności na:
Lombok,
Spring Roo oraz paru innych wynalazkach do Groovy.
W jednym z projektów korzystam z Lombok i bardzo sobię to narzędzie chwalę. Jedyna rzecz która mnie bardzo irytuję to fakt, że pod eclipse (może pod innym IDE jest podobny problem) w przypadku dodawania przez eclipse (CTRL+1) nowej metody do klasy z annotacją
Lombok dostajemy w edytorze błąd. Wynika to z tego, że eclipse wstawił nam sygnaturę nowo utworzonej metody w miejsce, w którym są wygenerowane, "niewidzialne" metody. Nie licząc tej niedogodności, samo narzędzie jest na prawdę super. Nie obyło się bez nieśmiertelnego przykładu z generowaniem
getterów/setterów, aby pokazać bardziej zaawansowane możliwości:
@Cleanup (w Java 7 będzie specjalna instrukcja związana z
automatic resource management), val (
final local variables oraz detekcja typu), @Synchronized (enkapsulacja
locków wraz z generowaniem ich jako puste
Object[] by były serializowalne),
@Log. W celu sprawdzania jakie zmiany zostały dokonane przez
Lomboka można użyć
javap lub
delombok. Hamlet wspomniał o samym mechanizmie działania
Lomboka, który opiera się na annotacji, oraz
Eclipse Handler i
Javac Handler. Podobno stworzenie własnych rozszerzeń nie jest specjalnie trudne a jako
first-step warto zobaczyć
tu.
Następne na tapetę poszło
Spring roo, ale nie było już tak dokładnie omawiane jako
Lombok. Chyba Hamlet nie do końca zna ten projekt i skończyło się na odpaleniu jakiegoś bardzo prościutkiego przykładu oraz wspomnieniu o tym, że
roo działa w czasie kompilacji, korzysta z
inter-type declarations oraz jest dostępny mechanizm
push-in refactoring. Po przejściu projektów
javowych, Hamlet przeszedł do swojego świata
Groovy. Zaczęło się od
Groovy transformations a już sama liczba dostępnych transformacji jest naprawdę imponująca. Dotyczą one: generowania kodu (np. @ToString, @Lazy (wraz z użyciem
volatile tworzy inteligentny
lock aby uniknąć dwukrotnego inicjalizowania), @TupleConstructor ), zarządzania współbieżnością (np. @Synchronized, @WithReadLock, @WithWriteLock), logowanie(np. @Log, @Slf4j) , ograniczaniem dostępu (np. @PackageScope), implementacji patternów (np. @Singleton, @Immutable, @Delegate) oraz wielu innych przypadków. Wszystkie z tych transformacji mają się opierać na implementacji interfejsu
GroovyCodeVisitor, którego to metody są wykonywane przy przechodzeniu przez
AST. Następnie Hamlet pokazał projekt
CodeNarc do statycznej analizy kodu w
groovy, który też opiera się na wzorcu
visitor. Kolejny narzędziem był
GContracts, który ma realizować idee
desing by contract dla
groovy. Pomimo tego, że projekt jest nowy, wygląda całkiem, całkiem. Ostatni był
Spock, który jest
frameworkiem do testowania napisanym w
groovy. Na pierwszy rzut oka sama idea przypomina użycie
Parameterized runner,w którym to dane podawane są w formacie
wiki.
Spock ma też podobno bardzo dobre wsparcie do
mocków.