Proxy Dinamici in Java
Crea implementazioni proxy di interfacce a runtime in Java con java.lang.reflect.Proxy e InvocationHandler.
Un proxy dinamico è un oggetto che implementa una o più interfacce, ma ogni chiamata di metodo viene instradata — a runtime — attraverso un singolo handler che scrivi tu. La JVM sintetizza la classe proxy al volo; non scrivi mai l'implementazione. Questa è la parte più potente di java.lang.reflect, ed è il modo in cui funzionano AOP, logging trasparente, lazy loading, stub RPC e librerie di mocking. Questo capitolo mostra come Proxy.newProxyInstance e InvocationHandler si incastrano, e cosa possono e non possono fare.
I due componenti: Proxy e InvocationHandler
Un proxy dinamico richiede tre input:
- Un class loader (dove definire la classe sintetizzata).
- Un array di interfacce che il proxy implementerà.
- Un
InvocationHandler— il singolo metodo che riceve ogni chiamata.
InvocationHandler handler = (proxy, method, args) -> {
// called for EVERY method invoked on the proxy
return ...; // becomes the method's return value
};
MyService svc = (MyService) Proxy.newProxyInstance(
MyService.class.getClassLoader(),
new Class<?>[]{ MyService.class },
handler);svc è ora un oggetto reale che implementa MyService. Chiamare svc.doThing(x) non esegue alcun corpo doThing — non esiste — ma chiama handler.invoke(proxy, <Method doThing>, [x]). L'handler decide cosa fare e cosa restituire.
La firma di invoke
Object invoke(Object proxy, Method method, Object[] args) throws Throwableproxy— l'istanza del proxy stesso (raramente usata; attenzione a chiamare metodi su di essa dall'interno diinvoke, poiché ri-entra nell'handler e può causare un loop infinito).method— ilMethodche è stato chiamato;method.getName(),method.getReturnType(), le sue annotazioni, ecc. sono tutti disponibili.args— gli argomenti comeObject[](nullse il metodo non ne accetta); i primitivi sono in forma boxed.- return — ciò che il chiamante deve ricevere; deve essere compatibile per assegnazione con
method.getReturnType()o si ottiene unaClassCastException. Per un metodovoid, restituiscenull.
Un pattern frequente è fare il forward a un oggetto "target" reale: method.invoke(target, args) — avvolgendo quella chiamata con logging, timing, transazioni o retry. Quella chiamata a Method.invoke è lo stesso dispatch riflessivo trattato in Reflection Java: Metodi; qui è guidata interamente dal Method che la JVM passa al tuo handler. Questa forma di inoltro è l'idioma decorator via proxy, ed è la base di Spring AOP.
Solo interfacce
Il vincolo più grande: java.lang.reflect.Proxy fa il proxy di interfacce, non di classi. Non puoi fare il proxy dinamico di una classe concreta con questa API. Se hai bisogno di fare il proxy di una classe, utilizzi una libreria bytecode (CGLIB, ByteBuddy) che genera invece una sottoclasse — ecco perché i framework le includono. Per i design basati su interfacce, il Proxy integrato è sufficiente e non richiede dipendenze.
La classe proxy sintetizzata:
- Estende
java.lang.reflect.Proxye implementa le tue interfacce. - Ha un nome generato come
$Proxy0. - Instrada
equals,hashCodeetoString(i metodi diObject) attraversoinvoke— quindi il tuo handler deve essere pronto a gestirli, o delegarli sensibilmente.
Un esempio pratico: proxy di logging + timing
Il programma definisce un'interfaccia Repository e una vera implementazione, poi avvolge l'implementazione in un proxy dinamico il cui handler registra ogni chiamata, ne misura il tempo, fa il forward all'oggetto reale e registra il risultato — aggiungendo comportamento trasversale senza toccare l'implementazione.
Cosa trarre dall'esecuzione:
repoera utilizzabile esattamente come unRepository—repo.save(...),repo.count(),repo.find(...)compilavano e giravano tutti — eppure nessuna classe denominata "logging repository" esiste nel sorgente. La JVM ha generato una classe$Proxy0che implementa l'interfaccia, e ogni chiamata è arrivata inLoggingHandler.invoke. Il proxy è un veroRepository(instanceofha restituitotrue).- Ogni metodo di business ha ricevuto un log automatico di entrata/uscita e timing senza alcuna modifica a
InMemoryRepository. Quella separazione — l'implementazione rimane all'oscuro, la logica trasversale vive nell'handler — è l'intero punto dell'AOP, e i proxy dinamici sono il modo in cui Spring implementa@Transactional,@Cacheablee simili per i bean basati su interfacce. - L'handler ha fatto il forward di ogni chiamata con
method.invoke(target, args), il che significa che un fallimento difind(99)è tornato comeInvocationTargetException. L'handler l'ha spacchettato congetCause()e ha rilanciato la veraNoSuchElementException, così il chiamante ha catturato l'eccezione naturale anziché un wrapper di reflection. Un proxy che dimentica di spacchettare fa trapelareInvocationTargetExceptionai chiamanti. - I metodi di
Objectpassano anche attraversoinvoke, quindi l'handler ha gestito il caso specialemethod.getDeclaringClass() == Object.classe li ha inoltrati normalmente. Senza quella guardia,toString/equals/hashCodeverrebbero anch'essi registrati (rumorosi) o, se si costruissero stringhe dal proxy all'interno diinvoke, potrebbero ricorrere. Gestire deliberatamente i metodi diObjectè una parte standard della scrittura di un handler proxy. Proxy.isProxyClass(repo.getClass())ha confermato che la classe è sintetizzata dalla JVM, e il suo nome$Proxy0mostra che è stata generata, non scritta. Poiché l'API accetta unClass<?>[]di interfacce, un proxy può implementarne diverse contemporaneamente — ed è così che un singolo mock o stub può soddisfare più contratti simultaneamente.
Quando usare cosa
- Interfaccia, nessuna dipendenza richiesta →
java.lang.reflect.Proxy. Integrato, semplice, solo per interfacce. - Bisogno di fare il proxy di una classe concreta → ByteBuddy o CGLIB (basato su sottoclasse). Necessario perché
Proxynon può. - Solo bisogno di stub di interfacce nei test → una libreria di mocking (Mockito) costruita su questi meccanismi — non farlo a mano.
I proxy dinamici concludono la parte sulla reflection: dall'ispezione di un oggetto Class, alla lettura e scrittura di campi, all'invocazione di metodi, alla creazione di istanze tramite costruttori, alla lettura di annotazioni, e infine alla sintesi di intere implementazioni a runtime. Insieme costituiscono il toolkit che permette ai framework di operare genericamente su tipi contro cui non erano stati compilati — usati con parsimonia e dietro astrazioni pulite, sono ciò che rende possibile l'ecosistema Java di container, mapper e runner.