Pooling Proxy

Im zweiten Artikel unseres TechBlogs wird es technischer und ich beschreibe, wie man in Java mit Hilfe dynamischer Proxys einen transparenten Object-Pool erstellen kann.

Die konkrete Problemstellung war folgende: In einer Multi-Threading-Umgebung wollte ich ein Webservice aufrufen. Die Erstellung eines einzelnen Webservice-Clients dauerte 300 Millisekunden und war somit eine sehr teure Operation. Daher wollte ich die Client-Objekte wiederverwenden und in einem Object-Pool verwalten. In den Apache Commons gibt es dafür eine gute Object-Pool-API. Bei der Arbeit mit Object-Pools ist der Ablauf immer gleich:

  1. Objekt aus dem Pool holen
  2. Methoden auf dem Objekt aufrufen
  3. Objekt an den Pool zurückgeben

Wichtig ist, dass der dritte Schritt auch im Fall einer Exception durchgeführt wird, also mit Java in einem finally-Block. In meinem Fall bestand der zweite Schritt immer aus genau einem Methodenaufruf (weil es bei dem  Webservice-Client sowieso keinen State gab). Da es viele Verwendungen des Webservice-Clients gab, kam es zu Code-Wiederholungen: Immer wieder wurde es notwendig Objekte aus dem Pool zu holen und anschließend zurückzugeben. Außerdem ist diese Vorgehensweise fehleranfällig, weil das Zurückgeben oder der finally-Block vergessen werden kann. Schöner wäre eine Kapselung aller drei Schritte.

Eine Lösung dafür ist ein Proxy, der dasselbe Interface hat wie der Webservice-Client und bei jedem Methodenaufruf die oben genannten drei Schritte durchführt. Zuerst definieren wir ein Interface für den Webservice-Client:

interface WebserviceClient {
	void method1();
	void method2();
}

Der folgende PoolingWebserviceClient implementiert das Interface und holt sich bei jedem Methodenaufruf eine WebserviceClient-Instanz aus einem Object-Pool:

import org.apache.commons.pool.ObjectPool;

public class PoolingWebserviceClient implements WebserviceClient {
	ObjectPool pool = <INIT-POOL>;
	
	public void method1() {
		WebserviceClient client = pool.borrowObject();
		try {
			client.method1();
		} finally {
			pool.returnObject(client);
		}
	}

	public void method2() {
		WebserviceClient client = pool.borrowObject();
		try {
			client.method2();
		} finally {
			pool.returnObject(client);
		}
	}
}

Wie man schon an diesem kurzen Beispiel mit zwei Methoden sieht, kommt es dabei zu Code-Wiederholung. Bei jeder Methode müssen die drei Schritte explizit angegeben werden. Wenn man weitere Webservice-Clients entwickelt, erhält man noch mehr Wiederholung. Ideal wäre es die Klasse PoolingWebserviceClient generieren zu lassen. Genau das ermöglicht die Reflection API. Damit lassen sich dynamische Proxies für beliebige Interfaces zur Laufzeit erstellen:

WebserviceClient poolingClient = (WebserviceClient) Proxy.newProxyInstance(WebserviceClient.class.getClassLoader(), 
				new Class<?>[] {WebserviceClient.class}, 
				new PoolingInvocationHandler<WebserviceClient>(webserviceClientFactory));

Mit der statischen Methode newProxyInstance wird ein Proxy erzeugt, der das Interface WebserviceClient implementiert. Der erste Parameter bestimmt, in welchem Class-Loader der Proxy definiert wird. Der zweite Parameter bestimmt die Interfaces, die vom Proxy implementiert werden sollen – uns genügt eines. Die Funktionalität des Proxies (Parameter 3) wird durch einen InvocationHandler, in unserem Fall durch den PoolingInvocationHandler definiert:

import java.lang.reflect.InvocationHandler;
import org.apache.commons.pool.ObjectPool;
import org.apache.commons.pool.impl.GenericObjectPool;

class PoolingInvocationHandler<I> implements InvocationHandler {
    private ObjectPool<I> pool;
    
    public PoolingInvocationHandler(PoolableObjectFactory<I> factory) {
        pool = new GenericObjectPool<I>(factory, 20, GenericObjectPool.WHEN_EXHAUSTED_BLOCK, -1);
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getDeclaringClass() == Object.class) {
            if (method.getName().equals("equals"))
                return proxy == args[0];
            else if (method.getName().equals("hashCode"))
                return System.identityHashCode(proxy);
            else if (method.getName().equals("toString"))
                return "Pooling-Proxy@" + Integer.toHexString(System.identityHashCode(proxy));
            else 
                throw new IllegalStateException("Here we should never get " + 
                    "because there are only 3 public non-final methods on Object.");
        }
        
        I object = pool.borrowObject();
        try {
            return method.invoke(object, args);
        } catch(InvocationTargetException ite) {
            throw ite.getCause(); // throw the original exception
        } finally {
            pool.returnObject(object);
        }
    }
}

Im Konstruktor (Zeile 8) erstellt der PoolingInvocationHandler den Object-Pool. Die 4 Parameter definieren die zu verwendende Factory für neue WebserviceClients im Pool, die maximale Größe des Pools (20) und das Verhalten bei “Überlastung” des Pools. WHEN_EXHAUSTED_BLOCK und der Wert -1 im letzten Parameter bedeuten, dass Aufrufe von pool.borrowObject() warten müssen, wenn alle Objekte des Pools schon verborgt sind.

Interessant ist die Methode invoke(…). Sie wird bei jedem Methodenaufruf auf dem Proxy aufgerufen und bestimmt dessen Verhalten. Der if-Block stellt sicher, dass die Methoden equalshashCode und toString sinnvoll verarbeitet werden. Andere Methoden der Klasse Object (getClassnotifywait, …) müssen nicht beachtet werden, weil sie final bzw. nicht public sind.

Falls die aufgerufene Methode nicht in der Klasse Object definiert ist, dann muss sie im Interface I (= WebserviceClient in unserem Beispiel) definiert sein. In dem Fall wird aus dem Pool ein Objekt (in unserem Fall ein WebserviceClient) geborgt und auf diesem die Methode aufgerufen. Zum Schluss wird das Objekt an den Pool zurückgegeben. Falls beim Aufruf eine Exception geworfen wird, kommt diese in einer InvocationTargetException verpackt zum InvocationHandler. Der packt sie aus und wirft sie weiter.

Mit Hilfe des dynamisch generierten Proxies kann man beliebige Klassen transparent poolen, indem man die Funktionalität in ein Interface extrahiert. Durch die Verwendung von Reflection ergibt sich ein geringer Overhead. Dafür erhält man im Gegenzug weniger Code-Wiederholung und vermeidet Fehler, weil niemand mehr vergessen kann, ein Objekt in den Pool zu retournieren.

Weitere Informationen zu dynamischen Proxies findet man zum Beispiel in diesem Tutorial. Hast du Proxies aus der JAVA API schon verwendet? Wenn ja, wofür?

Abschließend noch ein Hinweis auf die W-JAX, die Anfang November in München stattfindet: Am GWT-Day werde ich einen Vortrag über MVP und Internationalisierung halten. Ich bin schon gespannt auf das Publikum und einen regen Austausch mit anderen GWT-Benutzern.

Andreas Hubmer (Software Architect)