Proxy dinamici in Java

1. Introduzione

Questo articolo riguarda i proxy dinamici di Java, che è uno dei principali meccanismi proxy a nostra disposizione nel linguaggio.

In poche parole, i proxy sono fronti o wrapper che passano l'invocazione di funzioni attraverso le proprie strutture (di solito su metodi reali), aggiungendo potenzialmente alcune funzionalità.

I proxy dinamici consentono a una singola classe con un unico metodo di servire più chiamate di metodo a classi arbitrarie con un numero arbitrario di metodi. Un proxy dinamico può essere pensato come una sorta di facciata , ma può fingere di essere un'implementazione di qualsiasi interfaccia. Sotto la copertura, instrada tutte le invocazioni dei metodi a un singolo gestore : il metodo invoke () .

Sebbene non sia uno strumento pensato per le attività di programmazione quotidiane, i proxy dinamici possono essere molto utili per gli autori di framework. Può anche essere utilizzato in quei casi in cui le implementazioni di classi concrete non saranno note fino al runtime.

Questa funzione è incorporata nel JDK standard, quindi non sono necessarie dipendenze aggiuntive.

2. Gestore delle invocazioni

Costruiamo un semplice proxy che in realtà non fa altro che stampare quale metodo è stato richiesto di essere invocato e restituire un numero hardcoded.

Innanzitutto, dobbiamo creare un sottotipo di java.lang.reflect.InvocationHandler :

public class DynamicInvocationHandler implements InvocationHandler { private static Logger LOGGER = LoggerFactory.getLogger( DynamicInvocationHandler.class); @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { LOGGER.info("Invoked method: {}", method.getName()); return 42; } }

Qui abbiamo definito un semplice proxy che registra quale metodo è stato invocato e restituisce 42.

3. Creazione di istanza proxy

Un'istanza proxy servita dal gestore di invocazione che abbiamo appena definito viene creata tramite una chiamata al metodo factory sulla classe java.lang.reflect.Proxy :

Map proxyInstance = (Map) Proxy.newProxyInstance( DynamicProxyTest.class.getClassLoader(), new Class[] { Map.class }, new DynamicInvocationHandler());

Una volta che abbiamo un'istanza proxy, possiamo invocare i suoi metodi di interfaccia normalmente:

proxyInstance.put("hello", "world");

Come previsto , nel file di registro viene stampato un messaggio sul metodo put () invocato.

4. Gestore delle invocazioni tramite espressioni Lambda

Poiché InvocationHandler è un'interfaccia funzionale, è possibile definire il gestore inline utilizzando l'espressione lambda:

Map proxyInstance = (Map) Proxy.newProxyInstance( DynamicProxyTest.class.getClassLoader(), new Class[] { Map.class }, (proxy, method, methodArgs) -> { if (method.getName().equals("get")) { return 42; } else { throw new UnsupportedOperationException( "Unsupported method: " + method.getName()); } });

Qui, abbiamo definito un gestore che restituisce 42 per tutte le operazioni get e genera UnsupportedOperationException per tutto il resto.

Viene invocato esattamente allo stesso modo:

(int) proxyInstance.get("hello"); // 42 proxyInstance.put("hello", "world"); // exception

5. Esempio di proxy dinamico di temporizzazione

Esaminiamo un potenziale scenario del mondo reale per i proxy dinamici.

Supponiamo di voler registrare quanto tempo impiegano le nostre funzioni per essere eseguite. A tal fine, definiamo prima un gestore in grado di avvolgere l'oggetto "reale", tracciare le informazioni sui tempi e invocazioni riflessive:

public class TimingDynamicInvocationHandler implements InvocationHandler { private static Logger LOGGER = LoggerFactory.getLogger( TimingDynamicInvocationHandler.class); private final Map methods = new HashMap(); private Object target; public TimingDynamicInvocationHandler(Object target) { this.target = target; for(Method method: target.getClass().getDeclaredMethods()) { this.methods.put(method.getName(), method); } } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start = System.nanoTime(); Object result = methods.get(method.getName()).invoke(target, args); long elapsed = System.nanoTime() - start; LOGGER.info("Executing {} finished in {} ns", method.getName(), elapsed); return result; } }

Successivamente, questo proxy può essere utilizzato su vari tipi di oggetti:

Map mapProxyInstance = (Map) Proxy.newProxyInstance( DynamicProxyTest.class.getClassLoader(), new Class[] { Map.class }, new TimingDynamicInvocationHandler(new HashMap())); mapProxyInstance.put("hello", "world"); CharSequence csProxyInstance = (CharSequence) Proxy.newProxyInstance( DynamicProxyTest.class.getClassLoader(), new Class[] { CharSequence.class }, new TimingDynamicInvocationHandler("Hello World")); csProxyInstance.length()

Qui, abbiamo proxy una mappa e una sequenza di caratteri (String).

Le chiamate dei metodi proxy delegheranno all'oggetto avvolto e produrranno istruzioni di registrazione:

Executing put finished in 19153 ns Executing get finished in 8891 ns Executing charAt finished in 11152 ns Executing length finished in 10087 ns

6. Conclusione

In questo breve tutorial, abbiamo esaminato i proxy dinamici di Java e alcuni dei suoi possibili utilizzi.

Come sempre, il codice negli esempi può essere trovato su GitHub.