W3docs

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:

  1. Un class loader (dónde definir la clase sintetizada).
  2. Un array de interfaces que el proxy implementará.
  3. 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 Throwable
  • proxy — la instancia del proxy en sí (raramente se usa; cuidado al llamar métodos sobre él desde dentro de invoke, ya que re-entra en el handler y puede producir un bucle infinito).
  • method — el Method que fue llamado; method.getName(), method.getReturnType(), sus anotaciones, etc. están todos disponibles.
  • args — los argumentos como Object[] (null si 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 una ClassCastException. Para un método void, devuelve null.

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.Proxy e implementa tus interfaces.
  • Tiene un nombre generado como $Proxy0.
  • Enruta equals, hashCode y toString (los métodos de Object) también a través de invoke — 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.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • repo era utilizable exactamente como un Repositoryrepo.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 $Proxy0 que implementa la interfaz, y cada llamada llegó a LoggingHandler.invoke. El proxy es un Repository real (instanceof devolvió 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, @Cacheable y similares para beans basados en interfaces.
  • El handler reenvió cada llamada con method.invoke(target, args), lo que significa que un fallo en find(99) regresó como InvocationTargetException. El handler lo desempaquetó con getCause() y volvió a lanzar el NoSuchElementException real, de modo que el llamador capturó la excepción natural en lugar de un contenedor de reflexión. Un proxy que olvida desempaquetar filtra InvocationTargetException a los llamadores.
  • Los métodos de Object también se enrutan a través de invoke, por lo que el handler gestionó el caso especial de method.getDeclaringClass() == Object.class y los reenvió directamente. Sin esa guarda, toString/equals/hashCode también se registrarían (generando ruido) o, si construyes cadenas a partir del proxy dentro de invoke, podrían recursarse. Manejar los métodos de Object de 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 $Proxy0 muestra que fue generada, no escrita. Debido a que la API acepta un Class<?>[] 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 adicionaljava.lang.reflect.Proxy. Integrado, simple, solo interfaces.
  • Necesitas proxificar una clase concreta → ByteBuddy o CGLIB (basados en subclases). Necesarios porque Proxy no 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.

Práctica

Práctica
Quieres envolver un servicio definido por una interfaz 'PaymentGateway' para que cada llamada a un método quede registrada, sin modificar la implementación real. Llamas a 'Proxy.newProxyInstance(...)' pasando 'new Class<?>[]{ PaymentGateway.class }' y un handler. Dentro del 'invoke' del handler, ¿cuál es la forma estándar de producir el resultado real del método?
Quieres envolver un servicio definido por una interfaz 'PaymentGateway' para que cada llamada a un método quede registrada, sin modificar la implementación real. Llamas a 'Proxy.newProxyInstance(...)' pasando 'new Class<?>[]{ PaymentGateway.class }' y un handler. Dentro del 'invoke' del handler, ¿cuál es la forma estándar de producir el resultado real del método?
Was this page helpful?