Self-invocation e os limites do proxy

TL;DR

Toda a mágica de @Transactional, @Async, @Cacheable e afins depende de um proxy que intercepta a chamada antes de ela chegar ao objeto real. Quando um método chama outro método do mesmo objeto com this.metodo(), o proxy é ignorado completamente — a chamada vai direto para a instância, sem passar pelas instruções do aspecto. Além disso, métodos private e final nunca são interceptados pelo proxy CGLIB. A solução mais limpa é extrair o método para outro bean; self-injection e AopContext.currentProxy() existem, mas cada um traz um custo.

O que é

Self-invocation (auto-invocação) é quando um método de um bean Spring chama outro método do mesmo objeto usando a referência this — explícita ou implícita:

public void metodoA() {
    this.metodoB();   // self-invocation explícita
}
 
public void metodoA() {
    metodoB();        // self-invocation implícita (Java resolve como this.metodoB())
}

O problema é que esse padrão quebra silenciosamente qualquer anotação AOP (@Transactional, @Async, @Cacheable, @Retryable, etc.) presente em metodoB. Nenhuma exceção é lançada; o código compila e roda — só o comportamento esperado (transação, cache, execução assíncrona) não acontece.

Por que importa

Comportamentos declarativos do Spring — iniciar uma transação, executar em thread separada, armazenar resultado em cache — são implementados como adendos (advice) que o proxy executa antes de delegar para o objeto real. Se o proxy nunca vê a chamada, o adendo nunca roda.

Esse é o tipo de bug mais traiçoeiro no ecossistema Spring: o código parece correto, os testes unitários passam (porque geralmente testam a classe sem proxy), mas em produção a transação simplesmente não abre, o cache não é consultado, ou o método roda na thread principal ao invés de uma pool assíncrona.

Compreender self-invocation é pré-requisito para usar com confiança qualquer anotação AOP do Spring.

Como funciona

Por que a chamada interna bypassa o proxy

O Spring não modifica o bytecode do seu bean. Em vez disso, ele cria um objeto separado — o proxy — que envolve o bean real. O contêiner injeta o proxy nos pontos de dependência, não o objeto diretamente.

Quando código externo chama proxy.metodoA(), o proxy intercepta, executa os adendos relevantes e só então delega para o objeto real. Mas quando metodoA() chama this.metodoB(), a referência this aponta para o objeto real, não para o proxy. O proxy nem sabe que metodoB foi chamado.

Código externo → proxy.metodoA()   ← proxy intercepta ✓
                     ↓
              objeto.metodoA()
                     ↓
              this.metodoB()        ← proxy ignorado ✗
                     ↓
              objeto.metodoB()      ← @Transactional não aplicada

A documentação oficial do Spring resume: “once the call has finally reached the target object, any method calls that it may make on itself, such as this.bar() or this.foo(), are going to be invoked against the this reference, and not the proxy.”

private/final: AOP silenciosamente ignorado (CGLIB não intercepta/sobrescreve)

O proxy CGLIB funciona gerando uma subclasse do bean em tempo de execução e sobrescrevendo (override) os métodos públicos para injetar os adendos. Por isso:

  • Métodos private não podem ser sobrescritos por subclasses — o CGLIB simplesmente não os intercepta.
  • Métodos final também não podem ser sobrescritos — o CGLIB os ignora da mesma forma.
  • Classes final não podem ser estendidas — o CGLIB não consegue nem criar o proxy (falha em startup).

Colocar @Transactional em um método private ou final não causa erro de compilação nem de startup (em muitas configurações). A anotação é silenciosamente ignorada. O Spring não avisa — é responsabilidade do desenvolvedor saber dessa limitação.

Armadilha silenciosa

@Transactional private void processarPagamento() compila e roda sem erro. A transação simplesmente nunca abre.

Soluções

1. Extrair para outro bean — a solução mais limpa

Mover o método anotado para um bean separado elimina o problema pela raiz: a chamada agora sai de um objeto e chega em outro, passando obrigatoriamente pelo proxy.

@Service
public class NotificacaoService {
 
    @Transactional
    public void enviarConfirmacao(Long pedidoId) {
        // lógica transacional aqui
    }
}
 
@Service
public class PedidoService {
 
    private final NotificacaoService notificacaoService;
 
    public PedidoService(NotificacaoService notificacaoService) {
        this.notificacaoService = notificacaoService;
    }
 
    public void criarPedido(Long pedidoId) {
        // ...
        notificacaoService.enviarConfirmacao(pedidoId); // passa pelo proxy ✓
    }
}

Vantagem: sem acoplamento ao Spring AOP; testável de forma independente; deixa as responsabilidades claras.

2. Self-injection com @Autowired (ou @Lazy)

O bean injeta uma referência a si mesmo pelo contêiner — que entrega o proxy, não this:

@Service
public class PedidoService {
 
    @Autowired
    private PedidoService self; // Spring injeta o proxy
 
    public void criarPedido(Long pedidoId) {
        // ...
        self.enviarConfirmacao(pedidoId); // passa pelo proxy ✓
    }
 
    @Transactional
    public void enviarConfirmacao(Long pedidoId) {
        // lógica transacional aqui
    }
}

Cuidado com dependência circular

Dependendo da versão do Spring Boot e da configuração, self-injection pode disparar aviso de dependência circular. Use @Lazy no campo se necessário: @Autowired @Lazy private PedidoService self;

3. Anotar o método de entrada público (quando aplicável)

Se a transação (ou outro comportamento) puder abranger o método de entrada inteiro, a solução mais simples é mover a anotação para o método chamado externamente:

@Service
public class PedidoService {
 
    @Transactional // transação cobre criarPedido E a lógica interna
    public void criarPedido(Long pedidoId) {
        // ...
        enviarConfirmacao(pedidoId); // self-invocation, mas já está na mesma transação
    }
 
    private void enviarConfirmacao(Long pedidoId) {
        // lógica aqui — não precisa de @Transactional própria
    }
}

Limitação: funciona apenas quando a propagação padrão (REQUIRED) é suficiente. Se enviarConfirmacao precisasse de REQUIRES_NEW (nova transação independente), essa abordagem não serve. (A semântica de propagação — REQUIRED/REQUIRES_NEW — é tema do Galho 10, Persistência de dados, planejado.)

Na prática

O cenário mais comum em código Spring: criarPedido é chamado externamente (proxy intercepta, transação abre), mas internamente chama this.enviarConfirmacao() que também tem @Transactional. A anotação do método interno é ignorada.

@Service
public class PedidoService {
 
    @Transactional
    public void criarPedido(Long pedidoId) {
        // persiste o pedido...
        this.enviarConfirmacao(pedidoId); // ← PROXY IGNORADO
    }
 
    @Transactional // ← NUNCA EXECUTADA quando chamada via this
    public void enviarConfirmacao(Long pedidoId) {
        // se criarPedido lançar exceção após este ponto,
        // qualquer lógica de rollback específica de enviarConfirmacao é ignorada
    }
}

Fix 1 — extrair para outro bean:

@Service
public class ConfirmacaoService {
 
    @Transactional
    public void enviarConfirmacao(Long pedidoId) { ... }
}
 
@Service
public class PedidoService {
 
    private final ConfirmacaoService confirmacaoService;
 
    public PedidoService(ConfirmacaoService confirmacaoService) {
        this.confirmacaoService = confirmacaoService;
    }
 
    @Transactional
    public void criarPedido(Long pedidoId) {
        // ...
        confirmacaoService.enviarConfirmacao(pedidoId); // proxy ativo ✓
    }
}

Fix 2 — self-injection:

@Service
public class PedidoService {
 
    @Autowired
    @Lazy
    private PedidoService self;
 
    @Transactional
    public void criarPedido(Long pedidoId) {
        // ...
        self.enviarConfirmacao(pedidoId); // proxy ativo ✓
    }
 
    @Transactional
    public void enviarConfirmacao(Long pedidoId) { ... }
}

Fix 3 — anotação no método de entrada (propagação REQUIRED é suficiente):

@Service
public class PedidoService {
 
    @Transactional // cobre tudo abaixo
    public void criarPedido(Long pedidoId) {
        // ...
        enviarConfirmacao(pedidoId); // dentro da mesma transação — ok
    }
 
    private void enviarConfirmacao(Long pedidoId) { ... }
}

Armadilhas

1. Refatorar “puxando lógica pra dentro” quebra @Transactional

Ao mover um método de uma classe auxiliar para dentro do próprio serviço (por parecer mais simples), a chamada deixa de passar pelo proxy. O comportamento muda silenciosamente em produção.

// ANTES (funcionava): chamada cruzava fronteira de bean → proxy ativo
confirmacaoService.enviarConfirmacao(pedidoId);
 
// DEPOIS (quebrado): método movido para dentro da mesma classe
this.enviarConfirmacao(pedidoId);   // proxy ignorado; @Transactional não roda

Fix: mantenha o método no bean auxiliar ou use self-injection se a consolidação for indispensável.

2. Método @Async private

Colocar @Async em um método private não gera erro, mas o método sempre roda na thread atual. O comportamento assíncrono nunca acontece.

@Service
public class RelatorioService {
 
    // PROBLEMA: @Async em método private — CGLIB não intercepta
    @Async
    private void gerarRelatorio(Long id) {
        // roda na thread do chamador, nunca numa thread de pool
    }
}

Fix: torne o método public — o CGLIB só pode sobrescrever (e portanto interceptar) métodos públicos.

3. Achar self-injection elegante

Self-injection resolve o problema técnico, mas sinaliza que a classe provavelmente acumula responsabilidades demais. Em revisão de código, é um indicador de que uma extração de bean seria mais adequada.

// FUNCIONA, mas é sinal de design ruim
@Service
public class PedidoService {
    @Autowired @Lazy
    private PedidoService self;   // ← sinal de acúmulo de responsabilidades
 
    @Transactional
    public void criarPedido(Long id) { self.enviarConfirmacao(id); }
 
    @Transactional
    public void enviarConfirmacao(Long id) { ... }
}

Fix: extraia enviarConfirmacao para um ConfirmacaoService separado; o problema de self-invocation desaparece e o design fica mais coeso.

4. Assumir que AspectJ tem o mesmo limite

AspectJ compile-time weaving e load-time weaving não têm esse problema — eles tecem o adendo diretamente no bytecode, sem proxy. O limite é específico do modelo de proxy do Spring AOP.

// Com Spring AOP (proxy): this.metodoB() IGNORA o aspecto
// Com AspectJ weaving: this.metodoB() APLICA o aspecto normalmente
// — o bytecode do objeto real já contém a lógica de interceptação

Fix: se self-invocation for padrão inevitável na arquitetura, considere configurar AspectJ load-time weaving (spring-instrument.jar). Na maioria dos casos, a extração de bean é a resposta mais simples.

Em entrevista

Frase pronta (inglês)

  • “Spring AOP is proxy-based: when you call this.method() inside the same bean, you bypass the proxy entirely, so annotations like @Transactional or @Async on the called method are silently ignored.”

  • “The cleanest fix is to extract the annotated method into a separate bean — that way the call always goes through the proxy. Self-injection works too, but it’s a code smell that usually means the class has too many responsibilities.”

  • “Private and final methods can’t be intercepted by CGLIB because it generates a subclass at runtime and overrides methods — it simply can’t override what’s private or final.”

  • “Unlike AspectJ weaving, which modifies the bytecode directly, Spring AOP proxies only intercept calls that cross the bean boundary.”

Vocabulário

Termo PTTermo EN
auto-invocação / chamada internaself-invocation
adendoadvice
aspectoaspect
proxy baseado em subclasseCGLIB proxy
proxy baseado em interfaceJDK dynamic proxy
tecelagem em tempo de compilaçãocompile-time weaving
limite do proxyproxy boundary
injeção circularcircular dependency injection
extração de beanbean extraction / extract-to-bean refactor

Veja também

Referências