MÓDULO 05 · CONCEITO 06 DE 14

Proxy dinâmico & weaving

A mecânica que faz aspects funcionarem em runtime. Castle DynamicProxy, JDK Proxy, CGLIB, Spring AOP, Roslyn source generators — e por que Go, deliberadamente, evitou esse caminho e escolheu geração de código.

Tempo de leitura ~22 min Pré-requisito Conceitos 02, 03 e 05 (AOP, decorator, interceptors) Próximo Logging estruturado como cross-cutting

Em fevereiro de 2002, no JDK 1.3, a Sun introduziu uma classe modesta no pacote java.lang.reflect: a Proxy. Sua API era curta — um método estático newProxyInstance(loader, interfaces, handler) — e seu propósito era ainda mais discreto: gerar, em tempo de execução, uma classe nova que implementa um conjunto de interfaces, e cujas chamadas todas vão a um único InvocationHandler. Em retrospecto, foi uma das adições mais consequentes da plataforma. Sem ela, Spring AOP teria sido impossível, JPA proxies não existiriam, e meia dúzia de mecanismos de mock que viraram padrão de teste teriam precisado de bytecode generation explícito.

O conceito de proxy dinâmico é a infraestrutura central do AOP em runtime. A intuição é simples: em vez de o programador escrever uma classe decorator manualmente para cada interface, o framework gera essa classe automaticamente, em runtime, com base em um handler central que recebe todas as invocações. O handler é onde mora o código do aspect; o proxy é a casca tipada que torna o handler indistinguível, do ponto de vista do chamador, do objeto real.

O termo weaving — tecimento — descreve o processo mais geral, do qual proxy dinâmico é uma das três variantes principais. Weaving é como o código do aspect e do código base ficam efetivamente combinados antes da execução. Pode acontecer em três momentos: durante a compilação (compile-time weaving, AspectJ ajc), na carga das classes (load-time weaving, AspectJ LTW via java agent), ou em runtime (runtime weaving via proxy). O conceito 02 introduziu o vocabulário; este conceito destrincha a mecânica de cada um e mostra o que sobreviveu na engenharia de 2026.

Saber a mecânica importa por dois motivos. Primeiro, debug: stack traces de aspects em runtime são confusos para quem não sabe que $Proxy42 é uma classe sintética que delega para um handler — saber identificar isso economiza horas. Segundo, design: cada técnica de weaving tem restrições que afetam o que se pode fazer no aspect (proxy dinâmico só intercepta chamadas via proxy, não chamadas internas, etc.). Frameworks com peculiaridades conhecidas (Spring, Hibernate, Castle DynamicProxy) ficam menos misteriosos quando se conhece o motor que os move.

JDK Proxy — só funciona para interfaces

java.lang.reflect.Proxy tem uma limitação central: só gera proxy para interfaces. Se você quer interceptar chamadas em uma classe concreta sem interface, JDK Proxy não serve. Essa restrição é estrutural — Java não permite estender uma classe arbitrária em runtime —, e foi a razão pela qual CGLIB (Code Generation Library) apareceu em 2003 com bytecode generation que estende classes, não interfaces.

// Java — proxy dinâmico via JDK reflection
public interface PedidoService {
    Pedido criar(CriarPedidoCmd cmd);
}

InvocationHandler handler = (proxy, method, args) -> {
    long started = System.nanoTime();
    try {
        return method.invoke(impl, args);                  // delega ao real
    } finally {
        long ms = (System.nanoTime() - started) / 1_000_000;
        log.info("method={} duration_ms={}", method.getName(), ms);
    }
};

PedidoService proxied = (PedidoService) Proxy.newProxyInstance(
    PedidoService.class.getClassLoader(),
    new Class<?>[]{ PedidoService.class },
    handler);

proxied.criar(cmd);                  // chama handler, que delega ao impl

Internamente, newProxyInstance gera uma classe $Proxy0 (ou $Proxy1, etc.) que implementa PedidoService e delega cada método ao handler. A classe é gerada usando geração de bytecode da própria JVM, e fica anônima — não aparece no classpath, mas é uma classe legítima até para o garbage collector. Spring AOP "tradicional" usa esse mecanismo quando o bean implementa pelo menos uma interface; quando não implementa, cai em CGLIB.

CGLIB e bytecode generation

CGLIB foi criada em 2003 por Juozas Baliuka e mantida hoje sob o guarda-chuva da Spring Source. A técnica é diferente de JDK Proxy: em vez de gerar uma classe que implementa interfaces, CGLIB estende a classe-alvo, sobrescreve cada método público (não-final), e injeta a chamada ao MethodInterceptor em cada um. Funciona com classes concretas, mas tem suas próprias restrições.

A primeira é que métodos final e classes final não podem ser interceptados — não há como sobrescrevê-los. A segunda é que o construtor da classe pai precisa ser invocável — proxies CGLIB só funcionam com classes que têm construtor sem parâmetros, ou exigem configuração adicional. A terceira é que CGLIB usa ASM como biblioteca de bytecode, e esse acoplamento é parte do motivo pelo qual Spring AOP carrega centenas de classes de infraestrutura no classpath de qualquer aplicação — overhead que ninguém vê, mas que existe.

Spring AOP — proxy + pointcut linguísticos

Spring AOP, desde a versão 2.0 (2006), oferece um modelo híbrido: linguagem de pointcut emprestada do AspectJ, mas execução via proxy dinâmico (JDK Proxy ou CGLIB) em runtime. A escolha foi consciente — Spring queria a expressividade de AspectJ sem exigir ajc ou agente JVM. O preço foi as limitações estruturais de proxy: aspects só atuam em chamadas externas, métodos finais ou privados não podem ser interceptados, e auto-injeção de @Transactional em método chamado de dentro da mesma classe simplesmente não funciona.

// Spring AOP com aspect e pointcut
@Aspect
@Component
public class PerformanceAspect {

    @Around("@within(org.springframework.stereotype.Service) " +
            "&& execution(public * *(..))")
    public Object measure(ProceedingJoinPoint pjp) throws Throwable {
        long started = System.nanoTime();
        try {
            return pjp.proceed();
        } finally {
            long ms = (System.nanoTime() - started) / 1_000_000;
            log.info("{} duration_ms={}",
                pjp.getSignature().toShortString(), ms);
        }
    }
}

O Spring procura pelos aspects no startup, casa cada bean com os pointcuts que aplicam, e gera proxy para cada bean que tem ao menos um aspect aplicável. O proxy é o que vai para o ApplicationContext — o resto do sistema injeta o proxy, e o aspect roda transparentemente. Tudo bonito até alguém escrever this.outroMetodo(), e descobrir que o aspect não atua porque a chamada interna não passa pelo proxy.

armadilha clássica

@Transactional em método chamado de dentro da mesma classe. O sintoma é que a transação não abre — o método executa, persiste, e nada é commitado/rolledback como esperado. A causa é que this é a referência ao objeto real, não ao proxy: o aspect está conectado ao proxy, chamadas via this escapam dele. Soluções recorrentes: extrair o método para outra classe (que vira bean próprio com proxy próprio), injetar a si mesmo via @Autowired e chamar via essa referência, ou usar AspectJ load-time weaving que tece no bytecode e não depende de proxy. Toda biblioteca AOP-via-proxy convive com essa peculiaridade — saber que existe é o que separa pleno de sênior em revisão.

Castle DynamicProxy — a tradição equivalente em .NET

No mundo .NET, Castle DynamicProxy faz o que JDK Proxy + CGLIB fazem em Java, com unificação: gera proxy dinâmico para interfaces e para classes (com restrição de métodos virtual ou abstract). É a base de NHibernate, Moq, FakeItEasy e várias bibliotecas de AOP em .NET. A API central é ProxyGenerator e IInterceptor:

using Castle.DynamicProxy;

public class TimingInterceptor : IInterceptor
{
    private readonly ILogger _log;
    public TimingInterceptor(ILogger<TimingInterceptor> log) => _log = log;

    public void Intercept(IInvocation invocation)
    {
        var sw = Stopwatch.StartNew();
        try
        {
            invocation.Proceed();              // delega
        }
        finally
        {
            _log.LogInformation("{Method} {Ms}ms",
                invocation.Method.Name, sw.ElapsedMilliseconds);
        }
    }
}

var generator = new ProxyGenerator();
var real = new PedidoService(repo);
var proxied = generator.CreateInterfaceProxyWithTarget<IPedidoService>(
    real, new TimingInterceptor(logger));

proxied.Criar(cmd);                            // passa pelo Intercept

Para classes concretas, troca-se por CreateClassProxy<T>, com o detalhe de que os métodos a interceptar precisam ser virtual. Container DI como Autofac, Castle Windsor e DryIoc fazem essa composição automaticamente — você registra o tipo + lista de interceptors, o container gera o proxy e devolve no Resolve. Microsoft.Extensions.DependencyInjection (a DI nativa do .NET) não tem suporte direto a proxy, e por isso a comunidade usa Scrutor (decorator manual) ou troca de container quando precisa.

Source generators — o sucessor moderno

Em maio de 2020, com .NET 5, a Microsoft introduziu Roslyn Source Generators, uma forma de compile-time code generation com integração total ao compilador C#. Ao contrário de proxy dinâmico (que gera classe em runtime via emit de IL) e ao contrário de geração de código por T4 ou scripts externos (que precisam de etapa antes do build), source generator é parte da própria compilação: o gerador inspeciona a AST do código que está sendo compilado e produz arquivos C# adicionais, que entram no mesmo binário.

A consequência é importante: o "código gerado" é visível no Solution Explorer, debugável, e sem nenhum custo em runtime. Não há reflection, não há classe sintética, não há proxy. É a forma mais limpa em 2026 de obter os benefícios de compile-time weaving — exatamente o que AspectJ tinha em Java — sem pagar o preço de toolchain especial.

// Microsoft.Extensions.Logging usa source generator para gerar logging
public partial class PedidoService
{
    [LoggerMessage(
        EventId = 100,
        Level = LogLevel.Information,
        Message = "Pedido criado {PedidoId} para cliente {ClienteId}")]
    private partial void LogPedidoCriado(Guid pedidoId, Guid clienteId);

    public async Task<Pedido> Criar(CriarPedidoCmd cmd)
    {
        var p = new Pedido(cmd.ClienteId, cmd.Itens);
        await _repo.AddAsync(p);
        LogPedidoCriado(p.Id, p.ClienteId);    // gerado em compile-time
        return p;
    }
}

O partial void LogPedidoCriado não é implementado pelo programador — o source generator de Microsoft.Extensions.Logging lê a anotação [LoggerMessage] e gera a implementação tipada em arquivo paralelo. O resultado é logging que não usa formatadores em runtime, não aloca array de boxing para argumentos, e ainda assim mantém a legibilidade de uma chamada de método. É decorator de compile-time — o aspect é tecido durante a compilação, e o código final é tão eficiente quanto se você tivesse escrito tudo à mão.

A tendência em .NET é clara: source generators substituem proxy dinâmico onde podem. MediatR está estudando geração de handlers; FastEndpoints já é todo source-generated; .NET 8+ tem source generator para System.Text.Json e Regex que eliminam reflection completa. Saber operar com source generator vai ser, em 2027–2028, expectativa de sênior em ecossistema .NET.

Por que Go não foi por proxy

Go é a linguagem mainstream que mais explicitamente rejeitou o caminho de proxy/AOP. A decisão é articulada por Rob Pike e por convenção da comunidade desde o início: a ausência de reflection-by-default que mude o comportamento de método em runtime é considerada uma feature, não bug. O design da linguagem prioriza que ler o código local seja suficiente para entender o que ele faz; proxies dinâmicos quebram isso por construção.

A consequência prática é que Go usa duas técnicas no lugar. Primeira: composição explícita de funções — o conceito 03 mostrou — onde decorators são higher-order functions visíveis na declaração de cada serviço. Segunda: code generation via go generate, que produz código Go novo em arquivos paralelos, marcado com comentário // Code generated by <tool>; DO NOT EDIT.. Geração acontece como parte do processo de build, mas é etapa explícita — você roda go generate e vê o que mudou no diff.

// pedido_service.go
//go:generate mockgen -source=pedido_service.go -destination=mocks/mock_pedido.go

type PedidoService interface {
    Criar(ctx context.Context, cmd CriarPedidoCmd) (*Pedido, error)
}

// pedido_service_logged.go (escrito à mão ou gerado com tool específica)
type LoggedPedidoService struct {
    inner  PedidoService
    logger *slog.Logger
}

func (s *LoggedPedidoService) Criar(ctx context.Context, cmd CriarPedidoCmd) (*Pedido, error) {
    started := time.Now()
    p, err := s.inner.Criar(ctx, cmd)
    s.logger.LogAttrs(ctx, slog.LevelInfo, "Criar",
        slog.Duration("dur", time.Since(started)),
        slog.Bool("ok", err == nil),
    )
    return p, err
}

Bibliotecas como mockgen (Uber/Google), wire (DI compile-time do Google), sqlc (gera código de query a partir de SQL) e oapi-codegen (gera HTTP client/server a partir de OpenAPI) usam essa técnica. O resultado é semelhante a Roslyn Source Generators, sem integração tão profunda com o compilador, mas com a mesma filosofia: aspect tecido em compile-time, código final visível e auditável.

Comparativo das três técnicas — quando cada uma ganha

As três variantes de weaving — runtime via proxy, compile-time via source generator, code generation explícito — não são equivalentes. Cada uma tem perfil distinto de custo e benefício, e a escolha certa depende do ecossistema e do tipo de concern.

Runtime via proxy ganha quando: o framework precisa interceptar tipos que vêm de outras bibliotecas (sem acesso ao fonte); o conjunto de tipos a interceptar se decide em startup com base em configuração; e a equipe está confortável com debug indireto. Spring AOP e bibliotecas de mock vivem aqui. Custos: stack traces confusos, restrições estruturais (final, this), performance ligeiramente menor, e dificuldade de inspecionar o que vai rodar antes de subir o sistema.

Compile-time via source generator ganha quando: o framework é parte do ecossistema do compilador (Roslyn em .NET, annotation processor em Java); a regra do aspect é estável e baseada em anotação; e legibilidade do código gerado importa. Microsoft.Extensions.Logging, JsonSerializer source-generated, FastEndpoints. Custos: acoplamento ao compilador específico, complexidade de escrever o gerador (raramente vale escrever; geralmente vale usar um pronto).

Code generation explícito ganha quando: a cultura da linguagem prefere magia visível (Go, Rust com macros declarativas); o aspect tem semântica suficientemente complexa para merecer arquivo dedicado; e auditoria de diff é parte do workflow. go generate, cargo expand, ferramentas de codegen em Python (atrs/Pydantic com plugin do mypy/Pyright). Custos: etapa manual no workflow (rodar generate), risco de "esquecer de regenerar" em CI.

Debugging proxy — sobreviver ao stack trace

Quem trabalha com Spring AOP, Castle DynamicProxy ou bibliotecas de mock acaba lendo stack traces que mencionam classes nunca escritas: $Proxy42, PedidoService_Proxy42, PedidoServiceCastleProxy. Saber identificar essas classes como sintéticas — e olhar para a linha logo abaixo, onde está o InvocationHandler ou o IInterceptor que executa de verdade — é uma habilidade prática.

A ferramenta certa é o IDE. Em IntelliJ ou Visual Studio, "Open Source" em uma classe gerada por proxy abre o decompilado ou indica que a classe é sintética; conhecer onde se conectar a um aspect (breakpoint dentro do Intercept, não dentro do método "real") economiza tempo. Em runtime, vale logar todas as chamadas de aspect no startup do sistema para que se saiba quais foram efetivamente conectados — Spring tem logs de debug que mostram cada bean wrapped; Castle expõe isso via API; FastAPI mostra a árvore de dependencies em /docs.

heurística do sênior

Antes de adotar qualquer biblioteca AOP em um projeto novo, responda em uma frase: "qual técnica de weaving essa biblioteca usa, e quais são as três limitações estruturais dela?". Se a equipe não sabe responder, vai aprender via bug em produção. Spring AOP via proxy: chamadas internas escapam, métodos finais não funcionam, performance marginalmente menor. Castle DynamicProxy: classes precisam de método virtual, container DI específico. Source generator: parte da build, requer .NET 5+, não atua em código de outras bibliotecas. go generate: precisa rodar a ferramenta, CI precisa validar que código gerado bate com fonte. Saber as restrições antes evita surpresa cara depois.

O mesmo aspect, três técnicas de weaving

Para fechar o conceito, considere o aspect canônico — medir tempo de chamada — implementado nas três técnicas em três ecossistemas. As diferenças vão muito além de sintaxe.

C# — proxy dinâmico em runtime via Castle
public class TimingInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        var sw = Stopwatch.StartNew();
        try { invocation.Proceed(); }
        finally
        {
            Log.Information("{Method} {Ms}ms",
                invocation.Method.Name, sw.ElapsedMilliseconds);
        }
    }
}

// container Autofac
builder.RegisterType<PedidoService>()
    .As<IPedidoService>()
    .EnableInterfaceInterceptors()
    .InterceptedBy(typeof(TimingInterceptor));

Runtime weaving: o proxy é gerado quando o container resolve IPedidoService. Stack trace passa por TimingInterceptor.Intercept$Proxy.CriarPedidoService.Criar.

Python — runtime via metaclass / decorator dinâmico
import time
import logging
import wrapt              # biblioteca de Graham Dumpleton

log = logging.getLogger(__name__)

@wrapt.decorator
async def measured(wrapped, instance, args, kwargs):
    started = time.perf_counter()
    try:
        return await wrapped(*args, **kwargs)
    finally:
        elapsed_ms = (time.perf_counter() - started) * 1000
        log.info("%s %.2fms", wrapped.__qualname__, elapsed_ms)

class PedidoService:
    @measured
    async def criar(self, cmd: CriarPedidoCmd) -> Pedido:
        ...

Python escolhe meio-termo: decorator é resolvido em load-time (quando o módulo é importado), não em runtime puro nem em compile. wrapt é a biblioteca canônica para decorators que preservam assinatura, __qualname__ e introspecção.

Go — code generation explícito
//go:generate go run ./cmd/gen-decorators -input=pedido_service.go -aspect=timing

// pedido_service_timing.gen.go (gerado, NÃO editar)
type TimedPedidoService struct {
    inner  PedidoService
    logger *slog.Logger
}

func (s *TimedPedidoService) Criar(ctx context.Context, cmd CriarPedidoCmd) (*Pedido, error) {
    started := time.Now()
    p, err := s.inner.Criar(ctx, cmd)
    s.logger.LogAttrs(ctx, slog.LevelInfo, "Criar",
        slog.Duration("dur", time.Since(started)),
        slog.Bool("ok", err == nil),
    )
    return p, err
}

func NewTimedPedidoService(inner PedidoService, logger *slog.Logger) *TimedPedidoService {
    return &TimedPedidoService{inner: inner, logger: logger}
}

Em Go, o aspect é arquivo gerado: visível no diff, auditável em CI (validar que fonte e gerado batem), performance idêntica a código manual. Sem proxy, sem reflexão, sem stack trace estranho. A magia é apenas a ferramenta que escreveu o arquivo.

Por que importa para a sua carreira

Quem entende a mecânica de weaving lê códigos de framework com olhos diferentes. Não é mais "a biblioteca tem AOP, magia"; é "essa biblioteca usa proxy CGLIB, então final não funciona; aquela usa source generator, então a build precisa de .NET 6+". Em entrevista de seniors, a pergunta "explique como Spring AOP funciona por baixo dos panos" é uma das mais usadas para separar quem usa o framework de quem entende; a resposta forte cita JDK Proxy, CGLIB, a limitação de chamadas internas, e o motivo histórico (Java não permite estender classes em runtime). Em revisão de código de uma adoção nova de framework, saber as três limitações estruturais antes de adotar evita ano e meio de bug "estranho" que ninguém debugava direito.

Como praticar

  1. Escrever proxy à mão. Em Java ou C#, implemente um proxy dinâmico simples sem usar JDK Proxy ou Castle. Em Java, use Proxy.newProxyInstance e escreva um InvocationHandler que loga cada método chamado. Em C#, use DispatchProxy (parte do .NET, mais simples que Castle). Esse exercício torna concreta a mecânica que normalmente fica escondida.
  2. Reproduzir a armadilha do this. Crie um pequeno projeto Spring (ou .NET com Castle) com um serviço e dois métodos: publicMethod() chama internalMethod() via this. Anote o internalMethod com @Transactional (ou um aspect customizado de log). Verifique que o aspect não atua na chamada interna. Refatore para uma das três soluções padrão (extrair, auto-injetar, AspectJ LTW) e verifique que o aspect agora atua. Esse é o tipo de bug que sai caro descobrir em produção.
  3. Source generator simples em .NET. Implemente um source generator que adiciona logging automático a todo método de uma classe marcada com [TraceAll]. Use Roslyn, gere partial métodos, e verifique no Solution Explorer o código gerado. Esse exercício abre a porta para entender uma das principais tendências do .NET moderno.

Referências para aprofundar

  1. docs Java — java.lang.reflect.Proxy. docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/reflect/Proxy.html — A documentação oficial do Proxy do JDK. Curta. Vale ler antes de qualquer biblioteca de AOP em Java/Kotlin.
  2. docs Castle DynamicProxy Documentation. github.com/castleproject/Core/blob/master/docs/dynamicproxy.md — Documentação oficial. Cobre interface vs class proxy, restrições de virtual/final, e debugging de stack trace.
  3. docs Spring Framework Reference — AOP. docs.spring.io/spring-framework/reference/core/aop.html — Tratamento canônico de Spring AOP. Seção "Proxying Mechanisms" explica em detalhe a escolha entre JDK Proxy e CGLIB.
  4. docs Roslyn Source Generators. learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview — Overview oficial. Conjunto de cookbooks práticos disponível em github.com/dotnet/roslyn-sdk.
  5. docs Go — go generate. go.dev/blog/generate — Post no blog oficial do Go por Rob Pike, em 2014, introduzindo go generate. Texto curto que explica a filosofia de geração de código em vez de reflexão.
  6. livro AspectJ in Action (2ª ed.) — Ramnivas Laddad (Manning, 2009). Capítulos 7–9 cobrem load-time weaving e a integração com Spring. Para entender a mecânica de bytecode weaving sem ler papers acadêmicos.
  7. livro Spring in Action (6ª ed.) — Craig Walls (Manning, 2022). Capítulo de AOP com tratamento prático. Walls é claro sobre a armadilha de this e as opções de mitigação. Comparar com Laddad é didático.
  8. livro Pro .NET Memory Management — Konrad Kokosa (Apress, 2018). Cobre como CGLIB, Castle e source generators afetam memória e GC. Útil para sêniores que precisam justificar overhead em sistemas de alta performance.
  9. artigo Implementing Dispatch Proxies in .NET Core — Steve Gordon (blog, 2019). stevejgordon.co.uk — Tutorial concreto de DispatchProxy, a alternativa nativa do .NET ao Castle DynamicProxy. Útil para quem quer entender a primitiva sem dependência externa.
  10. artigo How Java Dynamic Proxies Work — Brian Goetz, Mark Reinhold (palestra técnica, 2014–2018). Diversos talks no canal Oracle/Java do YouTube. Goetz é arquiteto de Java e escreve com clareza ímpar. Vale procurar.
  11. artigo Unraveling Source Generators in .NET — Andrew Lock (série de blog, 2021–2024). andrewlock.net — Lock escreveu uma das melhores séries sobre source generators existentes. Cobre desde o "hello world" até generator complexos com incremental generation.
  12. vídeo Behind the Scenes of Source Generators — Phillip Carter, Mads Torgersen (.NET Conf, 2022). YouTube. Os arquitetos do compilador C# explicam como source generators funcionam por dentro do Roslyn. Para quem quer entender o motor, não só usar.