Java – Dynamiczne Proxy. Co to jest i do czego można go użyć?

Java – Dynamiczne Proxy. Co to jest i do czego można go użyć?

Prawdopodobnie ciężko Ci będzie w to uwierzyć, ale istnieje grupa osób, w sumie nawet całkiem spora, która do szczęścia nie potrzebuje wiedzieć czymże jest tytułowe dynamiczne proxy’owanie klas. Skoro jednak trafiłeś na mojego bloga, to istnieje całkiem spora szansa, że akurat Tobie ta wiedza może się kiedyś przydać.

Omawiając tytułowe zagadnienie postaram się przybliżyć Ci pojęcie proxy aby następnie przejść i omówić wybrane przykłady wykorzystania go w praktyce. W ostatniej części artykułu przedstawię dwa sposoby utworzenia dynamicznego proxy dla klas Java. Każdy z nich ma swoja wady i zalety, ale o tym za chwilę…

O co chodzi z tym dynamicznym proxy?

Zacznijmy od wyjaśnienia słowa “proxy”. Najlepszym polskim tłumaczeniem jakie mi się nasuwa, to określenie “pośrednik”. Rozwińmy zatem trochę ten termin i zastanówmy się jaka może być jego rola.

Dla lepszego zobrazowania wyobraźmy sobie, że mamy strumień wody (danych, żądań http, wywołań metod).

Strumień

Na takim strumieniu możemy postawić “pośrednika” np. filtr zanieczyszczeń. Z wody (np. czystego ruchu sieciowego) filtrowane będą zanieczyszczenia (w naszym przykładnie mogą to być nieautoryzowane żądania sieciowe). Takie wyłapane wyjątki mogą być blokowane i raportowane do działu bezpieczeństwa.

Proxy fiter

Kolejnym zastosowaniem serwera proxy może być zapamiętywanie wyników żądań http, tak aby przy kolejnym podobnym żądaniu (np. ponowne pobranie tego samego pliku) móc od razu zwrócić odpowiedź, bez konieczności jego ponownego pobierania. Taki rodzaj webcache’a może być zarówno po stronie klienta, jak i serwera. U klienta będzie on zapobiegał nadmiernej utylizacji łącza sieciowego, a w przypadku serwera ogranicza zużycie mocy obliczeniowej.

Jeszcze innym rodzajem proxy są load balancer’y. Idea, która przyświecała ich powstaniu, to chęć rozłożenia ruchu http z jednego serwera na ich grupę. Nawiązując do naszego przykładu – byłby to podział strumienia wejściowego na szereg mniejszych strumyków.

Load balancer

Podsumowując – najlepsze, według nie, tłumaczeniem słowa “proxy” to “pośrednik, który wzbogaca źródło o dodatkowe funkcje”. A jak to ma się do dynamicznego prox’owania klas w Java? Otóż dynamiczne prox’owanie polega na przechwyceniu wywołań metod na danym obiekcie oraz ich udekorowaniu o dodatkowe funkcje.

Ktoś by zapytał… A na co to komu? Komu to potrzebne?

Osobiście, na tematy opisujące zagadnienia takie jak np. proxy dynamiczne, natrafiam zazwyczaj przypadkiem – często, jako element rozwiązania zupełnie innego zagadnienia. I tak zaczynając od pytania “Jak zalogować wywołania wszystkich metod danego obiektu”, prędzej czy później, otrzesz się o tytułowy temat. Dlaczego? Ponieważ tak zadane pytanie prowadzi do kilku rozwiązań – np aspekty, czy właśnie dynamiczne prox’owanie klas. Zgłębienie idei i sensu każdej z tych technik, pozwala w przyszłości na lepsze wyczucie i dobór odpowiedniego narzędzia do zaistniałego problemu. Przejdźmy zatem po kilku przykładach użycia dynamic proxy.

Sytuacja #1: Powtarzalny kod wokół wywołań metod

Wyobraź sobie, że chcesz zbierać statystyki wywołań metod jakiegoś obiektu. Powiedzmy, że będą to liczba i czas wywołań. Niech Twoja klasa ma ,powiedzmy, 10 metod. Pisane kodu zbierającego takie statystyki w każdej z metod byłoby, delikatnie mówiąc, nieeleganckie. Mało tego! Nie dość że duplikujemy kod, to jeszcze przy dopisywaniu każdej kolejnej metody trzeba pamiętać o dopisaniu logowania.

A może tak zamiast pisać kod logowania w każdej z metod, posłużyć się poznanym już konceptem “pośrednika”? Tu z pomocą przychodzi tytułowy dynamic proxy. Zauważ proszę, że wspomniany efekt logowania możesz uzyskać przechwytując wywołanie metody, oraz dekorując ją o dodatkowe metody logujące.

Sytuacja #2: Przetwarzanie adnotacji

Wyobraź sobie, że chcesz pójść o krok dalej i re-użyć sposobu zbierania statystyk z poprzedniego punktu. Fajnie byłoby, aby forma re-użycaia tego kodu była zwięzła i prosta. Tu z pomocą przychodzą adnotacje, które umożliwiłyby np. oznaczenie obiektu adnotacją @Metrics. Sam temat adnotacji i ich przetwarzania jest na tyle duży, że wymaga osobnego artykułu, a nawet, całej ich serii. Na potrzeby tego wpisu wystarczy świadomość, że takie dekorowanie obiektu na bazie adnotacji może odbywać się właśnie przy użyciu dynamic proxy.

Sytuacja #3: Zmiany zachowania zamkniętego kodu

Wyobraź sobie sytuację, że masz dostarczoną jakąś bibliotekę w postaci skompilowanej. Nie masz do niej źródeł, a co a tym idzie, nie możesz samodzielnie wytworzyć nowego artefaktu. Chcesz zmienić zachowanie jednej z metod ponieważ jest w niej błąd, a dostawca nie przygotował jeszcze poprawki. Również w tej sytuacji dynaimc proxy może okazać się rozwiązaniem. Jeśli chcesz wiedzieć ja, to zostaw proszę komentarz pod artykułem.

Sytuacja #4: Własny framework

Ostatnią omawianym przypadkiem, który chciałem poruszyć jest standaryzacja zachowania aplikacji. Świetnym przydałem są wszelkiego rodzaju frameworki, które jak sama nazwa wykazuje – strukturyzują i standaryzują kod aplikacji. Jeśli miałeś okazję programować w nowym Springu, to zapewne świetnie rozumiesz co mam na myśli mówiąc – “programowanie przez adnotacje”. Jeśli nie, to gorąco zachęcam do eksperymentów z tym frameworkiem. Swoja przygodę polecam zacząć od:

Odrobina praktyki – Czyli, jak zrobić dynamiczne proxy klas w Java

W tym artykule przedstawię tylko dwie metody na uzyskanie dynamicznego proxy. Zaletą pierwszej z nich jest to, że do uzyskania proxy możemy użyć natywnego mechanizmu refleksji. Niestety metoda ta ma pewne ograniczenie. Mianowicie, prox’owana klasa musi implementować interfejs i to właśnie jego metody będą przechwytywane.

Jeśli z jakiegoś powodu Twoja klasa nie implementuje, żadnego interfejsu, lub metoda, którą chcesz przechwycić nie należy do żadnego z implementowanych interfejsów, to z pomocą przychodzi drugi sposób.

Metoda #1: java.lang.reflect.InvocationHandler – Dynamiczne Proxy na bazie interfejsu

Spójrz proszę na poniższy przykład. Mamy tu do czynienia z kasą CarImpl implementującą interfejs Car. Załóżmy, że chcesz przechwycić i udekorować wywołania metod tego interfejsu o wypisanie dodatkowego komunikatu na ekran. W tym celu wystarczy, że zaimplementujesz stosowny InvocationHandler oraz utworzysz proxy obiektu klasy CarImpl. W poniższym przykładzie zdecydowałem się zaznaczyć kolorem fragmenty kodu, które należy dopisać do oryginalnej wersji kody, aby stworzyć dynamiczne proxy.

package com.bettercoding.lab;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class DynamicProxyApp {
    public static void main(String[] args) {
        System.out.println("Without proxy:");
        System.out.println("---------------------");
        Car car = new CarImpl();
        car.runEngine("Driver");
        System.out.println("---------------------");

        System.out.println("\nWith proxy:");
        System.out.println("---------------------");
        Car carProxy = (Car) Proxy.newProxyInstance(Car.class.getClassLoader(),
                new Class[]{Car.class},
                new CarInvocationHandler(new CarImpl()));
        carProxy.runEngine("Driver");
        System.out.println("---------------------");
    }

    interface Car {
        void runEngine(String message);
    }

    static class CarImpl implements Car {

        @Override
        public void runEngine(String who) {
            System.out.println(String.format("Engine is running. [%s] run it.", who));
        }
    }

    static class CarInvocationHandler implements InvocationHandler {
        private final Object original;

        CarInvocationHandler(Object original) {
            this.original = original;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                System.out.println(String.format(">> before method: [%s]", method.getName()));
                return method.invoke(original, args);
            } finally {
                System.out.println(String.format("<< after method: [%s]", method.getName()));
            }
        }
    }
}

Metoda #2: javassist proxy – Dynamiczne Proxy na bazie klasy

W sytuacji, gdy nie możesz wydzielić stosownego interfejsu, a mimo wszystko chcesz przechwycić wywołania metod danego obiektu, to możesz użyć dodatkowej biblioteki o nazwie javassist. W związku z tym, że nie ma jej natywnie w JDK, to musisz ją dodać do zależności Twojego projektu.

dependencies {
    // https://mvnrepository.com/artifact/javassist/javassist
    compile group: 'javassist', name: 'javassist', version: '3.12.1.GA'
    testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.3.2'
}

Podobnie jak w poprzednim przykładzie, poniżej również zaznaczyłem kolorem zmiany prowadzące do uzyskania proxy.

package com.bettercoding.lab;

import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;

import java.lang.reflect.Method;

public class DynamicProxyJAApp {
    public static void main(String[] args) {
        System.out.println("Without proxy:");
        System.out.println("---------------------");
        CarImpl car = new CarImpl();
        car.runEngine("Driver");
        System.out.println("---------------------");

        System.out.println("\nWith proxy:");
        System.out.println("---------------------");
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setSuperclass(CarImpl.class);

        CarImpl carProxy;
        try {
            carProxy = (CarImpl) proxyFactory.create(new Class<?>[0],
                    new Object[0],
                    new CarInvocationHandler());
        } catch (Exception e) {
            throw new RuntimeException("An exception occurred during proxy creation", e);
        }

        carProxy.runEngine("Driver");
        System.out.println("---------------------");
    }

    static class CarImpl {

        public void runEngine(String who) {
            System.out.println(String.format("Engine is running. [%s] run it.", who));
        }
    }

    static class CarInvocationHandler implements MethodHandler {

        @Override
        public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable {
            try {
                System.out.println(String.format(">> before method: [%s]", proceed.getName()));
                return proceed.invoke(self, args);
            } finally {
                System.out.println(String.format("<< after method: [%s]", proceed.getName()));
            }
        }
    }
}

To byłoby na tyle jeśli chodzi o ten wpis. Więcej artykułów dostępnych jest w  angielskiej wersji bloga Jeśli podoba Ci się konwencja i chciałbyś więcej takich wpisów, zostaw mi komentarz z tematem, który Cię interesuje a ja postaram się go przygotować. Tymczasem możesz mi pomóc dotrzeć do większej liczby odbiorców udostępniając ten wpis.
Dziękuję

Leave a Reply

avatar
  Subscribe  
Notify of
Close Menu