poniedziałek, 16 maja 2011

GeeCon 2011 UniversityDay

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

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

wtorek, 15 marca 2011

Testowanie wywołania JMSTemplate

Jakiś czas temu zostałem zapytany w jaki sposób przetestować jednostkowo coś takiego:

public class SenderService {
private JmsOperations jmsTemplate;

public void doSend(final MyMessage myMessage){
jmsTemplate.send(new MessageCreator() {

public Message createMessage(Session session) throws JMSException {
return session.createObjectMessage(myMessage);
}
});
}
}

Pytanie jest dość podchwytliwe, gdyż uważam, że testy jednostkowe bardzo dobrze się sprawdzają do testowania logiki systemu. Kod powyżej nie ma jakiejś extra rozbudowanej logiki, ale jest ona dość dobrze zakopana. Poza tym wychodząc od TDD (jak przykazane) jak mogło dojść, że mamy kod a teraz zastanawiamy się jak go przetestować ?
Tworzenie testów na tym etapie wydaję się sensowne jedynie w celach regresji.

Z drugiej strony można się upierać, czy jest sens aby ten kod testować jednostkowo, czy nie testować go jedynie integracyjnie. Takie podejście wydaje się zgodne z tym co można wyczytać w http://www.growing-object-oriented-software.com/ a powyższy kod leżałby w warstwie adaptera zgodnie z Ports and Adapters pattern. Jednakże, z pragmatycznego punktu widzenia (albo wtedy gdy w ogóle nie mamy automatycznych testów integracyjnych/systemowych) przetestowanie jednostkowo powyższego kodu jest jak najbardziej możliwe.

Najpierw zastanowiłbym się co chcemy przetestować, bo w metodzie send dzieją się 2 rzeczy: wywołanie JMSTemplate.send oraz pośrednio budowa JMS Message. Najsensowniejsze wydaje się rozdzielenie obu funkcjonalności i przetestowanie ich osobno. Zacznijmy od testowania budowania JMS Message za pomocą jednego z dwóch podejść:
  1. tworzymy top level class implementującą MessageCreator

    public class MyMessageCreator implements MessageCreator {
    private final MyMessage message;

    public MyMessageCreator(MyMessage message) {
    this.message = message;
    }
    @Override
    public Message createMessage(Session session) throws JMSException {
    return session.createObjectMessage(message);
    }
    }


  2. tworzymy factory

    MessageCreator getMessageCreator(final MyMessage message) {
    return new MessageCreator() {
    @Override
    public Message createMessage(Session session) throws JMSException {
    return session.createObjectMessage(message);
    }
    };
    }
    }


Dzięki temu możemy w stosunkowo łatwy sposób przetestować, że nasz kod poprawnie tworzyJMS message (na potrzebę testów użyjemy mocków/stubów)

Następnie, możemy sprawdzić czy nasza metoda send faktycznie użyje jmsTemplate.send z odpowiednie parametrem. Taki test będzie zależał od tego w jaki sposób zaimplementowaliśmy tworzenie JMS Message. Jeśli stworzyliśmy własną klasę MessageCreator to możemy sprawdzić czy parametr metody send jest właśnie jej typu. Ewentualnie można użyć (gruba rura!) PowerMock. W drugim przypadku będziemy mogli zamockować factory. Jeśli rolę fabryki pełni osobna klasa (potencjalnie ukryta pod interfejsem) to nie wydaję się to najgorsze (nie licząc tego że powstały nam kolejne artefakty), a w przypadku gdy fabryką będzie lokalna metoda to już wchodzimy w tematy partial-mocking.

Gdybyśmy jednak chcieli podany kod przetestować bez jego uprzedniej refaktoryzacji, czyli bez wprowadzania podziału na: budowania JMS message i wywołanie jmsTemplate, to też jest to jak najbardziej możliwe. Sprawa wydaje się stosunkowo prosta: należy sprawdzić czy metoda jmsTemplate.send jest wywołana z odpowiednim parametrem. Odpowiedni parametr to taki, który jak wywołamy na nim metodę send
to na przekazanej JMS session zostaną wywołane odpowiednie metody. Poniżej przykłady jak to zostało zrobione za pomocą EasyMock i Mockito

public class SenderServiceEasyMockTest {
private SenderService sut = new SenderService();
private JmsOperations jmsOperations;
private MyMessage myMessage = new MyMessage("neverMind");

@Before
public void setUp() {
jmsOperations = EasyMock.createMock(JmsOperations.class);
sut.setJmsTemplate(jmsOperations);
}

@Test
public void testDoSendByArgumentMatcher() {
final Session session = EasyMock.createMock(Session.class);
IArgumentMatcher argumentMatcher = new IArgumentMatcher() {

public boolean matches(Object argument) {

MessageCreator messageCreator = (MessageCreator) argument;
try {
EasyMock.expect(session.createObjectMessage(myMessage))
.andReturn(null);
EasyMock.replay(session);
messageCreator.createMessage(session);
} catch (JMSException e) {
throw new RuntimeException(e);
}
return true;
}

public void appendTo(StringBuffer buffer) {
}
};

EasyMock.reportMatcher(argumentMatcher);
jmsOperations.send((MessageCreator) null);
EasyMock.replay(jmsOperations);

sut.doSend(myMessage);

EasyMock.verify(jmsOperations, session);
}

@Test
public void testDoSendByCapture() throws JMSException {
Capture<Messagecreator> tocapture = new Capture();
jmsOperations.send((MessageCreator) EasyMock.and(EasyMock.anyObject(),
EasyMock.capture(tocapture)));
EasyMock.replay(jmsOperations);

sut.doSend(myMessage);

MessageCreator value = tocapture.getValue();
Session session = EasyMock.createMock(Session.class);
EasyMock.expect(session.createObjectMessage(myMessage)).andReturn(null);
EasyMock.replay(session);
value.createMessage(session);
EasyMock.verify(jmsOperations, session);
}
}



public class SenderServiceMockitoTest {
private SenderService sut = new SenderService();
private JmsOperations jmsOperations;
private MyMessage myMessage = new MyMessage("SSSS");

@Before
public void setUp() {
jmsOperations = Mockito.mock(JmsOperations.class);
sut.setJmsTemplate(jmsOperations);
}

@Test
public void testDoSendByMatcher() {
sut.doSend(myMessage);

Mockito.verify(jmsOperations).send(Mockito.argThat(new ArgumentMatcher<Messagecreator>() {

@Override
public boolean matches(Object argument) {
MessageCreator messageCreator = (MessageCreator) argument;
Session session = Mockito.mock(Session.class);
try {
messageCreator.createMessage(session);

Mockito.verify(session).createObjectMessage(myMessage);
} catch (JMSException e) {
throw new RuntimeException(e);
}
return true;
}
}));
}


@Test
public void testDoSendByCaptor() throws JMSException {
sut.doSend(myMessage);

ArgumentCaptor<Messagecreator> messaArgumentCaptor = ArgumentCaptor.forClass(MessageCreator.class);
Mockito.verify(jmsOperations).send(messaArgumentCaptor.capture());
Session session = Mockito.mock(Session.class);
MessageCreator value = messaArgumentCaptor.getValue();
value.createMessage(session);
Mockito.verify(session).createObjectMessage(myMessage);
}
}

Pomimo braku jakiegokolwiek refaktoringu testy w EasyMock wyglądają o wiele mniej czytelniej od tych napisanych w Mockito. Wynika to głównie z potrzeby wołania w odpowiednich miejscach EasyMock.replay oraz (co gorsze) braku mechanizmów do weryfikacji zachowań, które zaszły w ramach wywołania metody podległej testowi. W przypadku EasyMock jest potrzeba wyspecyfikowania a priori wszystkich interakcji, a w Mockito możemy wygodnie użyć Mockito.verify. Szczególnie w tym przypadku bardzo wpływa to na czytelność kodu, gdy sekcje given/when/then sa zaburzone. Dodatkowo użycie mechanizmu capture w obu przypadkach wydaję się być zdecydowanie wygodniejsze od użycia argumentMatcher

czwartek, 10 marca 2011

AspectJ a 2.2250738585072012e-308

Ostatnio Dawid Weiss podczas spotkania w ramach Poznan JUG pokazał w jaki sposób obejść problem związany z Double.parseDouble(). Do czasu wydania (a co ważniejsze) zainstalowania patcha można się posiłkować wykorzystaniem AspectJ - nie pokryję to jednak wszystkich możliwych przypadków w których ten bug może się objawić. Aspekt oryginalnie przedstawiony przez Dawida na prezentacji był - jak słusznie myślałem - niekompletny, bo m.in. nie pokrywał przypadku kiedy Double.parseDouble byłoby wołane po refleksji.

package jug.demos.aspectj.aspects;
public aspect ParseDoubleHotFix{
double around(String s):
!within(jug.demos.aspectj.aspects..*) &&
call(double java.lang.Double.parseDouble(String)) &&
args(s){
if (s.indexOf("2250738585072012") >= 0) {
throw new IllegalArgumentException(
"We apologize for inconvenience, but this number is"
" temporarily not parseable by Oracle: " + s);
} else {
return Double.parseDouble(s);
}
}
}


Pełny kod aspektu został przysłany dzień po prezentacji na listę Poznan JUG. Pierwsze co rzuciło się w oczy to fakt, że zdefiniowane pointcuty są typu call zamiast execute. Czyli zamiast "opakować" wykonanie Double.parseDouble za pomocą execute, "opakowujemy" wywołania. Jak wyjaśnił Dawid jest to związane z tym "że java.lang.Double jest klasą systemową i ładuje się bardzo wcześnie. Load time weaver w AspectJ nie byłby w stanie jej pewnie stosownie owinąć, nawet gdyby go zmusić. Domyślnie AspectJ nie przetwarza klas systemowych w ogóle (java.* i javax.*). To, co można ew. zrobić, to przetworzyć rt.jar w trybie offline (z wymuszeniem przetwarzania pakietów systemowych), ale nie próbowałem."

Próba obejścia tego błędu poprzez sprawdzenie wyłącznie własnego kodu jest zdecydowanie niewystarczające, ponieważ feralna liczba może być parsowana przez kod, który wykorzystujemy, a do którego nie mamy źródeł. Najgorzej jednak wygląda sprawa w przypadku gdy wywołanie byłoby z poziomu biblioteki standardowej java - wtedy rozwiązania z wykorzystaniem AspectJ nie za wiele by się zdało (chyba że offlinowe przetworzenie rt.jar o czym wspomniał Dawid). Jednak gdy odrzucimy ten skrajny przypadek to i tak zakres kodu który jest uruchamiany w ramach naszej aplikacji, a który bezpośrednio nie kontrolujemy daje duże pole do popisu wszelkiej maści vulnerabilities. Jako przykład posłużył Dawidowi Tomcat 6.0.24.
Wydaję mi się, że eksperyment najłatwiej przeprowadzić przy wykorzystaniu AJDT wraz z podpiętym pod Eclipse Tomcatem (najłatwiej zrobić to w STS):
  • tworzymy sobie projekt typu AspectJ Project.
  • tworzymy w nowo otwartym projekcie nowy Aspect,
  • uaktywniamy go poprzez utworzenie pliku z odpowiednimi wpisami w META-INF/aop-ajc.xml w projekcie utworzonym powyżej (dodatkowo dodamy sobie <weaver options=" -XnoInline -verbose -showWeaveInfo"/>)
  • z poziomu IDE przechodzimy do konfiguracji Tomcata i otwieramy ustawienia związane z uruchomieniem serwera
  • w zakładce Arguments do VM Arguments dodajemy -javaagent:<ścieżka do aspectjweaver-1.6.10.jar>
  • w zakładce Classpath do User Entries dodajemy utworzony powyżej projekt typu AspectJ
  • uruchamiamy Tomcata
W logach zobaczymy m.in. wpis w stylu:

weaveinfo Join point 'method-call(double java.lang.Double.parseDouble(java.lang.String))' in Type 'org.apache.catalina.connector.Request' (Request.java:2591) advised by around advice from

Oznacza to że w linii 2591 klasy org.apache.catalina.connector.Request znajduję się wywołanie Double.parseDouble (czyli w tej linii znajduje się jointpoint spełniający określony pointcut ). Przeglądając źródła klasy org.apache.catalina.connector.Request można zobaczyć, że wywołanie Double.parseDouble odbywa się przy w metodzie getLocale, a dokładniej przy parsowaniu wartości quality factor. Oznacza to, że w przypadku gdy na serwerze zostanie wykonana metoda request.getLocale w odpowiedzi na żądanie HTTP o nagłówku Accept-Language i jego dowolną wartością, ale z q=2-2250738585072012e-308, obsługujący wątek "trafi" w busy-loop.

Dawid w swojej prezentacji skupił się głównie na load-time weaving - w odróżnieniu od compile/binary time weaving takie podejście nie wymaga ingerencji w proces budowania kodu. Ma to znaczną zaletę, którą wykorzystałem podczas pracy u klienta, kiedy okazało się, że aplikacja działa zdecydowanie wolniej od tego co od niej oczekiwano. Zamiast próbować podłączać ją pod profiler czy też optymalizować na ślepo, chciałem zmierzyć czas obsługi żadania z podziałem na warstwy/komponenty. Dodanie takiej logiki w kodzie byłoby nie tylko bardzo upierdliwe, ale także wymagałoby przebudowania i skompilowania aplikacji (klient wersjonował otrzymywane binaria). Zamiast tego napisałem aspekt, zapakowałem go w jara i jedyne co pozostało to zmuszenie administratora aby zmodyfikował parametry uruchamiania JVM (dodajemy javagent oraz jar z aspektem do classpath). Nie potrzeba było w takim przypadku w żaden sposób przetwarzać dostarczonej poprzednio aplikacji.
Modyfikacja parametrów startowych JVM często trafia na opór ze strony administratorów, ale w tym przypadku szczęśliwie się udało.

Z punktu widzenie developmentu/deploymentu LTW jest strasznie wygodne - wystarczy stworzyć nowy AspectJ project, napisać w nim aspekty, dodać ten projekt do classpath (w IDE bajecznie łatwe) projektu, dla którego chcielibyśmy aby zadziałały aspekty, zmodyfikować parametry startowe projektu (javaagent ) i uruchomić....

W SpringFramework AspectJ jest w pełni wykorzystywany do implementacji @Configurable oraz do zarządzania transakcjami w trybie aspect. Uruchamianie testów za pomocą Spring TestContextFramework, które tworzą context korzystający z Load Time Weaving dość znacząco zwiększa czas uruchomienia testów. Wynika to (zgodnie z tym co powiedział Dawid) z tego że sam weaver jest napisany w javie (co implikuje problemy z opakowywaniem klas języka/biblioteki standardowej). Dodatkowo w kontekście wydajności:
  • unikanie dynamic pointcut (typu cflow, cflowbelow)
  • unikanie generycznych typów parametrów/wartości zwracanych
  • tworzenie pointcut minimalizujących ilość jointpointów
  • sprawdzanie weaving logs

Na końcu Dawid pokazał 2 dodatkowe zastosowania użycia AspectJ
  • mierzenie wydajności/śledzenie ścieżek wywołania
  • szukanie błędów związanych z concurrency
To drugie wygląda bardzo ciekawie, szczególnie, że błędy związane z concurrency mogą się pojawiać losowo i są bardzo ciężkie do wykrycia za pomocą standardowych technik: unit testy/debugging. Dawid zaproponował aby zdefiniować wymagania co do współbieżności wybranej klasy za pomocą aspektu - przy wejściu do metody podnosimy flagę (target + wątek), a przy wyjściu opuszczamy flagę (target + wątek) . Dzięki temu możemy sprawdzić czy przy wejściu do metody flaga jest opuszczona. Dawid zaproponował aby zrealizować to w taki sposób aby zdeployować aspekt wraz z aplikacją na serwerze uruchomionym w trybie debug. Dalej, podpiąć się do serwera z IDE, ustawić breakpoint wraz z Suspend Policy ustawioną na Suspend VM w linii, która nie powinna zostać wywołana jeśli nasz kod który poddajemy sprawdzeniu jest poprawny. Maciej Biłas w mailu następnego dnia zaproponował aby zamiast bawienie się w tryb debug i breakpointy programowowo wykonać heapdump i wczytać go do Eclipse Memory Analyzer. HeapDump wygenerowane przez jave w wersji od 6u14 mają zawierać extra informacje, która pozwolą Eclipse Memory Analyzer na dogłębną analizę zawartości pamięci wraz z stanem wątków. Myślę że warto to sprawdzić...
. W kontekście debuggingu warto sprawdzić projekt http://youdebug.kenai.com/