AopUtils.getTargetClass() — desembrulhando os proxies do Spring
Com o bean envolto em proxy, bean.getClass() devolve a classe do proxy e a leitura de anotações falha. Como AopUtils.getTargetClass() recupera a classe real e quando usar ultimateTargetClass().
Neste artigo
No Spring, anotações como @Transactional, @Async, @Cacheable e @PreAuthorize só funcionam porque o container troca o bean original por um proxy: um objeto gerado em tempo de execução que intercepta as chamadas, executa a lógica extra e repassa a chamada ao objeto real (o modelo completo está no post AOP no Spring — JDK Dynamic Proxy, CGLIB e aspects custom). O efeito colateral é que bean.getClass() deixa de retornar a sua classe e passa a retornar a classe do proxy.
Neste post você vai ver por que isso quebra reflection, logs e mapas indexados por classe, como AopUtils.getTargetClass(bean) recupera a classe real e quando é preciso recorrer a AopProxyUtils.ultimateTargetClass().
O que o AOP faz pelo seu código, na prática
Sem AOP, as preocupações transversais (transação, segurança, métricas, que não fazem parte da regra de negócio) se espalham pelo método. Em um esboço simplificado:
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:
@Transactional@PreAuthorize("hasRole('ADMIN')")@Timed("criar") // Micrometer; exige o TimedAspect registradopublic Pedido criar(Pedido p) { log.info("criando pedido {}", p); return repo.save(p);}Cada anotação é atendida por um interceptor (o advice) que o container aplica ao bean. É exatamente por isso que o objeto injetado não é uma instância da sua classe: é o proxy que executa esses interceptores antes de chamar o seu código.
O problema: bean.getClass() devolve a classe do proxy
O Spring gera o proxy de duas formas:
- JDK Dynamic Proxy: uma classe criada pela JVM que implementa as interfaces do bean. É o padrão do Spring Framework quando o bean implementa ao menos uma interface.
- CGLIB: uma subclasse da sua classe gerada em tempo de execução. É usada quando o bean não implementa interfaces e é o padrão no Spring Boot (
spring.aop.proxy-target-class=true).
Por isso getClass() devolve algo assim:
br.com.exemplo.MeuServico$$SpringCGLIB$$0 // CGLIB: subclasse de MeuServicojdk.proxy2.$Proxy42 // JDK: pacote e número variamDesde o Spring Framework 6.0, o nome das classes CGLIB usa o marcador $$SpringCGLIB$$ seguido de um contador (antes era um hash). No proxy JDK, a especificação só reserva o prefixo $Proxy; o nome do pacote não é especificado.
Isso quebra qualquer código que dependa da identidade da classe real:
- Leitura de anotações via reflection. A subclasse CGLIB só enxerga as anotações de classe marcadas com
@Inherited(como@Transactional). Anotações sem@Inherited, como uma anotação sua, não aparecem, e as anotações de método também não, porque o proxy sobrescreve os métodos sem copiá-las. O proxy JDK não enxerga nenhuma anotação da classe alvo. - Logging com
getClass().getSimpleName(): o log mostraMeuServico$$SpringCGLIB$$0ou$Proxy42no lugar deMeuServico. - Mapas do tipo
Map<Class<?>, Handler>:handlers.get(bean.getClass())não encontra a entrada registrada comMeuServico.class. - Código de infraestrutura que descobre metadados pela classe, como um
BeanPostProcessorpróprio ou um registro de listeners: sofre dos mesmos problemas.
O diagrama resume a diferença: o proxy envolve o objeto real; getClass() para na camada de fora, e getTargetClass() atravessa essa camada e devolve a classe que você escreveu.
O utilitário: AopUtils.getTargetClass(Object)
Fica no pacote org.springframework.aop.support:
public static Class<?> getTargetClass(Object candidate)Segundo o Javadoc, o método determina a classe alvo de um bean que pode ser um proxy AOP: devolve a classe alvo quando é proxy e a própria classe do objeto nos demais casos, nunca null.
Como ele chega lá (código do Spring Framework 7.0.9):
- Se o objeto implementa
TargetClassAware, pergunta a ele qual é a classe alvo. Todo proxy Spring AOP implementa essa interface por meio deAdvised, a menos que tenha sido criado com a opçãoopaque. - Se não obteve resposta, usa a superclasse quando o objeto é um proxy CGLIB e
getClass()nos demais casos.
Na prática:
- Se o bean não é proxy, retorna
bean.getClass(). - Se é proxy CGLIB ou JDK, retorna a classe do objeto real que o proxy envolve.
- Se é um proxy de proxy, desce apenas um nível e retorna a classe do proxy interno (por exemplo,
MeuServico$$SpringCGLIB$$0). Para chegar ao fundo, useAopProxyUtils.ultimateTargetClass(), descrito mais abaixo. - Nunca retorna
null, garantia da API.
Exemplo prático
Como MeuServico não implementa interface, o Spring cria um proxy CGLIB por causa do @Transactional. A anotação @Auditavel foi criada sem @Inherited para mostrar a diferença:
import org.springframework.aop.support.AopUtils;import org.springframework.transaction.annotation.Transactional;// demais imports omitidos
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface Auditavel {} // sem @Inherited
@Service@Auditavelpublic class MeuServico { @Transactional public void processar() { /* ... */ }}
@Componentpublic class Inspetor { public void inspecionar(Object bean) throws NoSuchMethodException { System.out.println(bean.getClass().getName()); // br.com.exemplo.MeuServico$$SpringCGLIB$$0
Class<?> alvo = AopUtils.getTargetClass(bean); System.out.println(alvo.getName()); // br.com.exemplo.MeuServico
// Anotação de classe sem @Inherited bean.getClass().isAnnotationPresent(Auditavel.class); // false alvo.isAnnotationPresent(Auditavel.class); // true
// Anotação de método bean.getClass().getMethod("processar") .isAnnotationPresent(Transactional.class); // false alvo.getMethod("processar") .isAnnotationPresent(Transactional.class); // true }}Use a classe alvo só para ler metadados. Para chamar métodos, continue usando o próprio bean: é o proxy que aplica a transação, o cache e a segurança.
Métodos relacionados
O próprio AopUtils tem três métodos para descobrir que tipo de objeto você recebeu. Todos verificam também se o objeto implementa SpringProxy, ou seja, respondem true só para proxies criados pelo Spring AOP:
isAopProxy(Object):truese o objeto é um proxy Spring AOP, JDK ou CGLIB.isCglibProxy(Object):truese é um proxy CGLIB.isJdkDynamicProxy(Object):truese é um JDK Dynamic Proxy.
Em outras classes utilitárias:
AopProxyUtils.ultimateTargetClass(Object)(pacoteorg.springframework.aop.framework): desce por qualquer número de proxies aninhados até a classe final. Só atravessa um nível quando consegue fazer isso sem efeitos colaterais, isto é, quando o alvo é um singleton guardado em umSingletonTargetSource(oTargetSourceé o componente que entrega ao proxy o objeto real). Com outrosTargetSources (lazy, pool, protótipo, hot swap), para naquele nível e devolve a classe informada por ele. Também nunca retornanull.ClassUtils.getUserClass(Object)(pacoteorg.springframework.util): devolve a classe original quando a classe do objeto é uma subclasse gerada pelo CGLIB (nome com$$). Não reconhece proxy JDK, que continua como$Proxy42. Tem também a versãogetUserClass(Class<?>), útil quando você só tem a classe em mãos.
O diagrama abaixo mostra a diferença em um proxy JDK que envolve um proxy CGLIB:
Resumo do comportamento, conferido no Spring Framework 7.0.9:
| Método | Proxy CGLIB | Proxy JDK | Proxy de proxy |
|---|---|---|---|
AopUtils.getTargetClass | classe real | classe real | classe do proxy interno |
AopProxyUtils.ultimateTargetClass | classe real | classe real | classe real (alvos singleton) |
ClassUtils.getUserClass | classe real | classe do proxy ($Proxy…) | depende do tipo de cada proxy (não use) |
Regra prática
Sempre que for inspecionar um bean Spring por reflection (ler anotações, comparar tipos, registrar em mapa por classe, logar um nome legível), use AopUtils.getTargetClass(bean) em vez de bean.getClass(). Se o bean puder estar envolvido por mais de um proxy, prefira AopProxyUtils.ultimateTargetClass(bean). Não custa nada e evita bugs que só aparecem meses depois, quando alguém adiciona um @Transactional ao serviço e o proxy passa a existir.
Fontes
- Spring Framework Reference — Proxying Mechanisms
- Spring Framework Reference — AOP Proxies
- Javadoc — AopUtils
- Javadoc — AopProxyUtils
- Javadoc — ClassUtils
- Javadoc — TargetClassAware
- Javadoc — Transactional (declarada com
@Inherited) - Código-fonte do Spring Framework 7.0.9 — AopUtils.java, AopProxyUtils.java e SpringNamingPolicy.java
- Spring Boot — Common Application Properties (
spring.aop.proxy-target-class) - Java SE 21 API — java.lang.reflect.Proxy e java.lang.annotation.Inherited
- Micrometer — Timers (
@TimedeTimedAspect)
tagspring tagaop