Java

AopUtils.getTargetClass() — desembrulhando os proxies do Spring

Quando o Spring envolve um bean num proxy, bean.getClass() passa a mentir — e reflection, leitura de anotações e logging quebram de formas sutis. O utilitário oficial que devolve a classe real, e quando usar cada variação.

No Spring, anotações como @Transactional, @Async, @Cacheable, @Scheduled e @PreAuthorize só funcionam porque o container troca o bean original por um proxy que intercepta as chamadas (o modelo completo está no post AOP no Spring — JDK Dynamic Proxy, CGLIB e aspects custom). O efeito colateral: bean.getClass() deixa de retornar a classe real — retorna a do proxy gerado. AopUtils.getTargetClass(bean) é o utilitário oficial do Spring que desembrulha o proxy e devolve a classe original — essencial para reflection, leitura de anotações e logging.

O que o AOP faz pelo seu código, na prática

Sem AOP, as preocupações transversais se espalham pelo método:

Sem AOP
public Pedido criar(Pedido p) {
long inicio = System.currentTimeMillis();
tx.begin();
try {
if (!user.hasRole("ADMIN")) throw new AccessDenied();
log.info("criando pedido {}", p);
var salvo = repo.save(p);
tx.commit();
return salvo;
} catch (Exception e) {
tx.rollback();
throw e;
} finally {
metrics.record("criar", System.currentTimeMillis() - inicio);
}
}

Com AOP, o método volta a falar só de negócio:

Com AOP
@Transactional
@PreAuthorize("hasRole('ADMIN')")
@Timed("criar")
public Pedido criar(Pedido p) {
log.info("criando pedido {}", p);
return repo.save(p);
}

Cada anotação corresponde a um aspect registrado no container. E é exatamente por isso que o bean que você recebe injetado não é a sua classe — é o proxy que executa esses aspects.

O problema: bean.getClass() mente

Como o bean injetado é o proxy, getClass() devolve algo assim:

br.com.exemplo.MeuServico$$SpringCGLIB$$0 // CGLIB
com.sun.proxy.$Proxy42 // JDK

Isso quebra qualquer código que dependa da identidade da classe real:

  • Leitura de anotações via reflection — o proxy CGLIB herda algumas, mas não todas; o JDK proxy não tem nenhuma do alvo.
  • Logging com getClass().getSimpleName() — vira lixo ilegível.
  • Mapas do tipo Map<Class<?>, Handler> que usam a classe como chave.
  • Descoberta de metadados, BeanPostProcessors, listeners de evento.

getClass devolve a classe do proxy; AopUtils.getTargetClass desembrulha e devolve a classe real

O utilitário: AopUtils.getTargetClass(Object)

Do pacote org.springframework.aop.support:

public static Class<?> getTargetClass(Object candidate)

Comportamento, segundo o javadoc do Spring:

Determine the target class of the given bean instance which might be an AOP proxy. Returns the target class for an AOP proxy or the plain class otherwise. Never null.

Na prática:

  • Se o bean não é proxy → retorna bean.getClass() direto.
  • Se é proxy CGLIB → retorna a superclasse (a classe original que foi estendida).
  • Se é proxy JDK → desembrulha via TargetSource e devolve a classe concreta do alvo.
  • Se for proxy aninhado (proxy de proxy), desce até chegar no fundo.
  • Nunca retorna null — garantia da API.

Exemplo prático

Inspetor.java
import org.springframework.aop.support.AopUtils;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MeuServico {
@Transactional
public void processar() { /* ... */ }
}
@Component
public class Inspetor {
public void inspecionar(Object bean) {
System.out.println(bean.getClass());
// MeuServico$$SpringCGLIB$$0
System.out.println(AopUtils.getTargetClass(bean));
// MeuServico
// Agora dá para ler anotações da classe real:
boolean tx = AopUtils.getTargetClass(bean)
.isAnnotationPresent(Transactional.class);
}
}

Os métodos parentes no AopUtils

  • isAopProxy(Object)true se o bean é um proxy Spring AOP (de qualquer tipo).
  • isCglibProxy(Object)true se é CGLIB especificamente.
  • isJdkDynamicProxy(Object)true se é JDK Dynamic Proxy.
  • AopProxyUtils.ultimateTargetClass(Object) — versão mais agressiva, que também desembrulha TargetSource dinâmico (lazy init, hot-swap). Em 99% dos casos getTargetClass resolve; para TargetSource dinâmico, ultimateTargetClass é mais confiável.

Regra prática

Sempre que for inspecionar um bean Spring por reflection — ler anotações, comparar tipos, registrar em mapa por classe, logar nome legível — use AopUtils.getTargetClass(bean) em vez de bean.getClass(). Custa nada e evita bugs que só aparecem quando alguém adiciona um @Transactional no serviço meses depois e o proxy começa a interferir.

Fontes

AopUtils.getTargetClass() — desembrulhando os proxies do Spring

Autor

Cesar Schutz

Publicado em

19 - 05 - 2026

Licença CC BY 4.0
Avatar
Cesar Schutz

Arquitetura, código e aprendizado contínuo.