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.
@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.
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.
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.Criar → PedidoService.Criar.
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: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
-
Escrever proxy à mão. Em Java ou C#,
implemente um proxy dinâmico simples sem usar JDK Proxy ou
Castle. Em Java, use
Proxy.newProxyInstancee escreva umInvocationHandlerque loga cada método chamado. Em C#, useDispatchProxy(parte do .NET, mais simples que Castle). Esse exercício torna concreta a mecânica que normalmente fica escondida. -
Reproduzir a armadilha do
this. Crie um pequeno projeto Spring (ou .NET com Castle) com um serviço e dois métodos:publicMethod()chamainternalMethod()viathis. Anote ointernalMethodcom@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. -
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, gerepartialmé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
- docs Java — java.lang.reflect.Proxy.
- docs Castle DynamicProxy Documentation.
- docs Spring Framework Reference — AOP.
- docs Roslyn Source Generators.
- docs Go — go generate.
- livro AspectJ in Action (2ª ed.) — Ramnivas Laddad (Manning, 2009).
- livro Spring in Action (6ª ed.) — Craig Walls (Manning, 2022).
- livro Pro .NET Memory Management — Konrad Kokosa (Apress, 2018).
- artigo Implementing Dispatch Proxies in .NET Core — Steve Gordon (blog, 2019).
- artigo How Java Dynamic Proxies Work — Brian Goetz, Mark Reinhold (palestra técnica, 2014–2018).
- artigo Unraveling Source Generators in .NET — Andrew Lock (série de blog, 2021–2024).
- vídeo Behind the Scenes of Source Generators — Phillip Carter, Mads Torgersen (.NET Conf, 2022).