czwartek, kwietnia 10, 2008

Sztuczki tropiciela błędów, part 2 ;-)

Przedstawiłem tutaj jakiś czas temu parę sztuczek tropiciela błędów, dziś nowe ;-)

Polowanie na zapomniany atrybut

Problem:

Mamy metodę kopiującą jeden obiekt do drugiego.
Chcemy upewnić się, że wszystkie wartości atrybutów z jednego obiektu są kopiowane do drugiego.
Zakładamy, że mamy obiekt, który ma wiele, ale to naprawdę wiele atrybutów, mamy do nich także metody set i get [mogą być też is, choć tutaj je pominiemy].
Jeżeli nie mamy metod set i get to powinniśmy sie przerzucić na ogrodnictwo i zrezygnować z programowania w Java'ie ;-) [hint - Eclipse oraz NetBeans są w stanie wygenerować metody set i get].

Rozwiązanie:

Class type = orig.getClass();
Method[] methods = type.getMethods();
for (Method method:methods) {
if (method.getName().startsWith("get")) {
try {
if (!method.invoke(orig).equals(method.invoke(retVal))) {
System.out.println(method.getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}


orig to obiekt wejściowy, retVal to obiekt do którego kopiujemy.

Powyższy kawałek kodu iteruje przez wszystkie metody naszego typu, jeżeli metoda zaczyna się na get czyli tak jak wg. konwencji JavaBans powinien nazywać się getter, to próbuje ją wywołać na obiekcie wejściowym i wyjściowym, oraz porównać ich wyniki, jeżeli wyniki są różne, oznacza to, że najprawdopodobniej nie skopiowaliśmy zawartości tego pola [nie zadziała to tak jak byśmy chcieli w sytuacji gdy akurat w obiekcie oryginalnym jest wartość domyślna, bo i tak będzie w docelowym].

W wyniku dostaniemy na konsoli listę metod get, które odnoszą się do atrybutów które nie zostały skopiowane.

Btw. do szybkiego "sklonowania" obiektu możemy użyć wariacji powyższego kodu, którą przedstawiam niżej:
Class type = orig.getClass();
Method[] methods = type.getMethods();
for (Method method:methods) {
if (method.getName().startsWith("get")) {
try {
String methodName = method.getName().replace("get","set");
Method setter = type.getMethod(methodName,method.getReturnType());
setter.invoke(retVal,method.invoke(orig));
} catch (Exception e) {
e.printStackTrace();
}
}
}


Taki kod może się przydać do weryfikacji czy źródłem naszego problemu jest fakt, że nie kopiujemy wszystkiego między obiektami.

Wstrzykiwanie breakpointów

Przydatne gdy mamy legacy code w którym mamy długie metody operujące na jakichś obiektach i chcielibyśmy namierzyć miejsce gdzie stan obiektu zmienia się na taki jaki nas interesuje [np. miejsce gdy ktoś ustawi jakąś konkretną wartość zmiennej, albo gdy doda do np. StringBuilder'a jakiegoś String'a].

Technika ta bywa trudna i zdradliwa, ale czasem jest bardzo przydatna ;-)

W skrócie polega na nadpisaniu [lub modyfikacji] klasy której obiekt chcemy monitorować w taki sposób by kod w momencie ustawienia zadanej wartości wchodził do danej sekcji kodu, tam stawiamy breakpointa i korzystając ze stack trace'a widzimy gdzie został nasz obiekt tak potraktowany... brzmi dobrze, ale na najciekawszych obiektach takich jak String, StringBuilder czy StringBuffer bywa trudne do zrealizowania. Z powodu bezpiczeństwa i wydajności Twórcy JDK zdecydowali, że wszystkie te klasy są finalne... ale nie straszne nam to ;-) gdyż w większości przypadków możemy zastąpić te obiekty innymi klasami, które będą na zewnątrz zachowywały się w naszym kodzie tak jak nasi delikwenci. A tam gdzie w końcu potrzebować będziemy wartości o odpowiednim typie to spokojnie możemy wyposażyć w taki getter nasz obiekt.

Zakładanie haka ;-)

Podobnie jak poprzednia technika polega na nadpisaniu niektórych metod przeciwnika ;-) Przydatne np. w trakcie walki ze strumieniami, gdy nie wiemy czy strumień dostaje to co zamierzyliśmy czy coś innego. W takim przypadku nadpisujemy [w klasie anonimowej najlepiej, bo na miejscu i łatwe do usunięcia] metodę read lub write w taki sposób, że wewnątrz wołamy supermetody, a dane wyrzucamy na konsolę.

Bisekcja breakpointem ;-)

Czasem, jest tak że błąd gdzieś jest, widać go w wartości jakiejś zmiennej [lub kilu zmiennych], ale nie mamy pojęcia gdzie się pojawia, żadna z powyższych technik się nie sprawdza, albo nie mamy ochoty lub możliwości popełniania klasy która odwali za nas robotę. W takim przypadku najprościej zastosować taki trick, ustawiamy breakpointa na końcu podejrzanego kodu, powodujemy wejście do tego kodu, gdy jesteśmy w breakpoint'cie sprawdzamy czy nasza zmienna ma wartość oznaczającą kłopoty, jeżeli nie ma, to stawiamy breakpointa w połowie kodu do przepadania [nie musi to być dokładnie połowa], wracamy do punktu wejścia [czyli do ostatniego elementu w stack trace'ie] i wykonujemy kod, który zatrzyma się w naszym brakpoint'cie, sprawdzamy wartość zmiennej, jeżeli jest taka jak przy błędzie to ustawiamy breakpoint w połowie kodu przed tym breakpointem gdzie akurat stoimy, w przeciwnym wypadku stawiamy breakpointa w połowie między aktualnym a tym poprzednio postawionym, wracamy do punktu wejścia i znowu w momencie zatrzymania sprawdzamy wartość.... w końcu [i to dość szybko] trafimy do feralnego kawałka, który powoduje problemy.

I to by było na tyle ;-)


Podobne postybeta
Niecne wykorzystanie refleksji... czyli jak poszukać tekstu w drzewie obiektów? ;-)
Refleksje i serializacja w Java'ie - podstawy i obalanie mitów ;-)
Sortujemy JTable gdy się da ;-)
Sztuczki tropiciela błędów, part 4
Sztuczki tropiciela błędów, part 3 - hackujemy klasy finalne ;-)