sobota, 11 sierpnia 2012

Wykorzystanie Dynamic Proxy jako narzędzia diagnostycznego

Wstęp.

Dawno nie pisałem. Jest to też trochę podyktowane tym, że ciężko pisać, gdy bije się z myślami: "Niee, ten temat jest za prosty żeby o nim pisać, przecież to żałosne!", ale przypomniałem sobie pewne słowa (a raczej literki napisane na blogu), człowieka, którego uważam za kogoś, kogo warto naśladować. Brzmiało to tak: "Chętnie służę pomocą w razie potrzeby *zmniejszenia* własnych oczekiwań odnośnie zawartości wpisu.". Melduję, że zmniejszam! :).

Nie wiem, czy tylko ja tak mam, ale czasami ciężko się zmotywować do nauki. Dobrym lekarstwem na to okazał się, już kiedyś przeze mnie wspominany, serwis knowledgeblackbelt - jasno określony cel i wymagania pomagają w skutecznym motywowaniu. Właśnie udało mi się zdać egzamin z Java Reflection API, dzięki któremu zdobyłem zielony pas. Z tej okazji pomyślałem, że naskrobię coś w końcu na blogu. Dziś będzie o wykorzystaniu Dynamic Proxy na przykładzie kodu do pomiaru wydajności metody (ile czasu zajmuje jej wykonanie powierzonego zadania). Zapewne istnieją do tego rozbudowane narzędzia, może jakieś pluginy do IDE - ot, to ma być tylko taki przykładzik.

Ogólne informacje o wzorcu Proxy (Pośrednik).

Wzorca Proxy używamy do stworzenia obiektu zastępczego, który kontroluje dostęp do innego obiektu, kiedy mamy do czynienia z takim obiektem zdalnym, którego stworzenie wiąże się z dużym kosztem lub który wymaga zabezpieczeń.
Cytując za książką Wzorce Projektowe. Rusz Głową!  (ang. Head First Design Patterns)

Żeby zrozumieć ideę wzorca rzućmy okiem na diagram klas.


Pośrednik i obiekt, do którego kontroluje dostęp, implementują wspólny interfejs. Dzięki temu zastosowanie pośrednika może być niewidoczne - w kodzie możemy te dwa obiekty traktować tak samo. Pośrednik posiada referencję na obiekt kontrolowany (Prawdziwy Przedmiot) i po wykonaniu swojego zadania może oddelegować żądanie do właściwego obiektu.

Przygotujmy klasy.

Wyobraźmy sobie, taką sytuację: mamy pracownika, który wykonuje pewne zadania. Zajmijmy się implementacją zadań - bajecznie proste. Symulujemy czas trwania zadania poprzez usypianie wątku.
// Interfejs zadania
public interface TaskInterface {
    void execute();
}
// Sprzątanie (Implementacja1)
public class Cleaning implements TaskInterface {

    @Override public void execute(){
        try {
            TimeUnit.MILLISECONDS.sleep(1000);
        } catch (InterruptedException e) {
        e.printStackTrace();
        }
        System.out.println("Cleaning done!");
    }
}
// Piłowanie (Implementacja2)
public class Sawing implements TaskInterface {

    @Override public void execute() {
        try {
            TimeUnit.MILLISECONDS.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Sawing done!");
     } 
}
Następnie stwórzmy klasę pracownika, który będzie wykonywał pewne zadania.
public class Worker {
    static TaskInterface[] tasks = new TaskInterface[] {
        new Sawing(), 
        new Cleaning()
    };
 
    public static void main(String[] args) {
        doSomeTasks(tasks);
    }
 
    private static void doSomeTasks(TaskInterface[] tasks) {
        for(TaskInterface task : tasks) {
            task.execute();
        }
    }
}
System działa, pracownik może wykonać swoje zadania. Dla uproszczenia do klasy Worker dodamy kod odpowiedzialny za stworzenie pośrednika (lepszym wyjściem było by zastosowanie tutaj fabryki i wtedy albo korzystalibyśmy z fabryki obiektów rzeczywistych, albo pośredników - w zależności od potrzeb).

Tworzenie dynamicznych pośredników.

Stworzenie klasy pośrednika możemy wziąć na własne barki, albo wykorzystać do tego klasę Proxy, która dynamicznie utworzy tą klasę w czasie wykonania programu. Jesteśmy leniwi i skorzystamy z drugiego sposobu (zresztą o jego pokazanie w tym wpisie chodzi). Cały trik polega na tym, że musimy utworzyć implementację interfejsu InvocationHandler, to właśnie w tej klasie będziemy reagować na wywołania metod pośrednika (najczęściej delegować do prawdziwego obiektu i zwracać wynik). Interfejs wymusza zaimplementowanie metody invoke(Object proxy, Method method, Object[] args).

  • Pierwszy jej parametr, to obiekt proxy od którego pochodzi wywołanie (bo można do kilku wykorzystać ten sam InvocationHandler).
  • Drugi przedstawia metodę, która została wywołana.
  • Trzeci to lista argumentów tej metody.

public class PerformanceAnalyzer implements InvocationHandler {
    TaskInterface proxied;
 
    public PerformanceAnalyzer(TaskInterface proxied) {
        this.proxied = proxied;
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) 
            throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = method.invoke(proxied, args); 
        long elapsedTime = System.currentTimeMillis() - startTime;
        System.out.println("[takes " + elapsedTime + " milliseconds]");
        return result;
    }
}
Poprzez konstruktor przekazujemy referencję na obiekt prawdziwy, którego przesłania pośrednik. Mierzymy czas (moglibyśmy użyć dokładniejszej metody System.nanoTime()) i po prostu wypisujemy ten czas. Wynik który zwrócił prawdziwy obiekt jest zwracany przez pośrednika do klienta.

Wróćmy do klasy Worker. Dodamy do niej metodę, która przyjmie prawdziwy obiekt i zwróci opakowującego go pośrednika. Pośrednika tworzymy za pomocą wywołania statycznej metody Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h).

  • Pierwszy parametr to class loader, który posłuży to utworzenia instancji obiektu pośrednika.
  • Drugi to lista obiektów Class interfejsów, które pośrednik ma implementować.
  • InvocationHandler, do którego pośrednik będzie przekazywał wywołania.
private static TaskInterface createAnalyzedTask(TaskInterface realObj) {
    return (TaskInterface) Proxy.newProxyInstance(
                  TaskInterface.class.getClassLoader(), 
                  new Class[] {TaskInterface.class}, 
                  new PerformanceAnalyzer(realObj));
}
Pozostało nam tylko jeszcze wykorzystać tą metodę do utworzenia pośredników, zmodyfikujmy tablicę tasks.
static TaskInterface[] tasks = new TaskInterface[] {
    createAnalyzedTask(new Sawing()), 
    createAnalyzedTask(new Cleaning())
};
Naszym oczom powinien ukazać się taki wydruk:

Sawing done!
[takes 2002 milliseconds]
Cleaning done!
[takes 1001 milliseconds]

1 komentarz: