Proxies dinámicos en Java
Crea implementaciones proxy de interfaces en tiempo de ejecución en Java con java.lang.reflect.Proxy e InvocationHandler.
Un proxy dinámico es un objeto que implementa una o más interfaces, pero cuya llamada a cada método se enruta — en tiempo de ejecución — a través de un único handler que tú escribes. La JVM sintetiza la clase proxy sobre la marcha; nunca escribes la implementación. Este es el rincón más poderoso de java.lang.reflect, y es cómo funcionan AOP, el registro transparente, la carga diferida, los stubs RPC y las bibliotecas de mocking. Este capítulo muestra cómo Proxy.newProxyInstance e InvocationHandler encajan entre sí, y qué pueden y no pueden hacer.
Las dos piezas: Proxy e InvocationHandler
Un proxy dinámico necesita tres entradas:
- Un class loader (dónde definir la clase sintetizada).
- Un array de interfaces que el proxy implementará.
- Un
InvocationHandler— el único método que recibe cada llamada.
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 es ahora un objeto real que implementa MyService. Llamar a svc.doThing(x) no ejecuta ningún cuerpo de doThing — no existe ninguno — llama a handler.invoke(proxy, <Method doThing>, [x]). El handler decide qué hacer y qué devolver.
La firma de invoke
Object invoke(Object proxy, Method method, Object[] args) throws Throwableproxy— la instancia del proxy en sí (raramente se usa; cuidado al llamar métodos sobre él desde dentro deinvoke, ya que re-entra en el handler y puede producir un bucle infinito).method— elMethodque fue llamado;method.getName(),method.getReturnType(), sus anotaciones, etc. están todos disponibles.args— los argumentos comoObject[](nullsi el método no recibe ninguno); los primitivos están encapsulados en sus tipos envolventes.- retorno — lo que el llamador debe recibir; debe ser compatible con
method.getReturnType()o se produce unaClassCastException. Para un métodovoid, devuelvenull.
Un patrón frecuente es reenviar a un objeto "target" real: method.invoke(target, args) — envolviendo esa llamada con registro, temporización, transacciones o reintentos. Esa llamada a Method.invoke es el mismo despacho reflectivo cubierto en Java Reflection: Methods; aquí está impulsado completamente por el Method que la JVM entrega a tu handler. Esta forma de reenvío es el idioma decorador mediante proxy, y es la base de Spring AOP.
Solo interfaces
La restricción más importante: java.lang.reflect.Proxy proxifica interfaces, no clases. No puedes crear un proxy dinámico de una clase concreta con esta API. Si necesitas proxificar una clase, recurres a una biblioteca de bytecode (CGLIB, ByteBuddy) que genera una subclase — por eso los frameworks incluyen esas bibliotecas. Para diseños basados en interfaces, el Proxy integrado es suficiente y no requiere dependencia adicional.
La clase proxy sintetizada:
- Extiende
java.lang.reflect.Proxye implementa tus interfaces. - Tiene un nombre generado como
$Proxy0. - Enruta
equals,hashCodeytoString(los métodos deObject) también a través deinvoke— por lo que tu handler debe estar preparado para manejarlos, o delegarlos de manera sensata.
Un ejemplo completo: un proxy de registro y temporización
El programa define una interfaz Repository y una implementación real, luego envuelve la implementación en un proxy dinámico cuyo handler registra cada llamada, la cronometra, reenvía al objeto real y registra el resultado — añadiendo comportamiento transversal sin tocar la implementación.
Lo que se puede extraer de la ejecución:
repoera utilizable exactamente como unRepository—repo.save(...),repo.count(),repo.find(...)se compilaron y ejecutaron correctamente — sin embargo no existe ninguna clase llamada "logging repository" en el código fuente. La JVM generó una clase$Proxy0que implementa la interfaz, y cada llamada llegó aLoggingHandler.invoke. El proxy es unRepositoryreal (instanceofdevolviótrue).- Cada método de negocio obtuvo un registro automático de entrada/salida y temporización sin ningún cambio en
InMemoryRepository. Esa separación — la implementación se mantiene ignorante, la preocupación transversal vive en el handler — es el punto central de AOP, y los proxies dinámicos son cómo Spring implementa@Transactional,@Cacheabley similares para beans basados en interfaces. - El handler reenvió cada llamada con
method.invoke(target, args), lo que significa que un fallo enfind(99)regresó comoInvocationTargetException. El handler lo desempaquetó congetCause()y volvió a lanzar elNoSuchElementExceptionreal, de modo que el llamador capturó la excepción natural en lugar de un contenedor de reflexión. Un proxy que olvida desempaquetar filtraInvocationTargetExceptiona los llamadores. - Los métodos de
Objecttambién se enrutan a través deinvoke, por lo que el handler gestionó el caso especial demethod.getDeclaringClass() == Object.classy los reenvió directamente. Sin esa guarda,toString/equals/hashCodetambién se registrarían (generando ruido) o, si construyes cadenas a partir del proxy dentro deinvoke, podrían recursarse. Manejar los métodos deObjectde forma deliberada es una parte estándar de escribir un handler de proxy. Proxy.isProxyClass(repo.getClass())confirmó que la clase está sintetizada por la JVM, y su nombre$Proxy0muestra que fue generada, no escrita. Debido a que la API acepta unClass<?>[]de interfaces, un proxy puede implementar varias a la vez — que es cómo un único mock o stub puede satisfacer múltiples contratos simultáneamente.
Cuándo usar qué
- Interfaz, sin dependencia adicional →
java.lang.reflect.Proxy. Integrado, simple, solo interfaces. - Necesitas proxificar una clase concreta → ByteBuddy o CGLIB (basados en subclases). Necesarios porque
Proxyno puede. - Solo necesitas hacer stub de interfaces en pruebas → una biblioteca de mocking (Mockito) construida sobre estos mecanismos — no lo hagas a mano.
Los proxies dinámicos cierran la parte de reflexión: desde inspeccionar un objeto Class, hasta leer y escribir campos, invocar métodos, construir instancias mediante constructores, leer anotaciones, y finalmente sintetizar implementaciones completas en tiempo de ejecución. En conjunto son el kit de herramientas que permite a los frameworks operar de forma genérica sobre tipos contra los que nunca fueron compilados — usados con moderación y detrás de abstracciones limpias, son lo que hace posible el ecosistema de contenedores, mapeadores y ejecutores de Java.