Spring Framework
Programação orientada a aspectos com Spring AOP e AspectJ
Intermediário9 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
Logging, medição de tempo, retries, verificações de segurança e transações atravessam todas as camadas de uma aplicação. O Spring AOP permite escrever cada um deles uma única vez como um aspecto e aplicá-lo de forma declarativa. Ao final deste guia, você saberá escrever aspectos com @Aspect, selecionar métodos com precisão usando expressões de pointcut, escolher o tipo de advice certo e, principalmente, explicar por que o modelo baseado em proxies faz com que algumas chamadas nunca sejam interceptadas.
O que você vai aprender
- Definir aspecto, join point, pointcut, advice e weaving e relacioná-los ao modelo de proxies do Spring
- Escrever advices @Around, @Before, @AfterReturning e @AfterThrowing com @Aspect
- Compor expressões de pointcut com os designadores execution, within, @annotation, args e bean
- Explicar a diferença entre proxies dinâmicos do JDK e proxies CGLIB por subclasse, e quando cada um é usado
- Diagnosticar advices que silenciosamente não são executados por causa de autoinvocação, métodos final ou instanciação fora do Spring
Alguns comportamentos se recusam a ficar em um lugar só. Medir o tempo de um método, verificar uma permissão, tentar de novo após uma falha transitória, abrir uma transação: cada um deles é necessário em dezenas de classes, em todas as camadas, e copiar e colar o mesmo try/finally em cada uma espalha uma única decisão por todo o código. A programação orientada a aspectos (AOP) dá nome a esse problema — os interesses transversais (cross-cutting concerns) — e oferece uma forma de escrever o comportamento uma única vez e declarar onde ele se aplica. @Transactional, @Cacheable, @Async, @PreAuthorize e @Retryable são todos AOP por baixo dos panos, então entender o mecanismo é o que separa usar essas anotações de saber depurá-las. Este guia tem como alvo o Spring Framework 6.x.
Vocabulário
A AOP tem um vocabulário pequeno e preciso, emprestado do AspectJ, que o Spring usa sem alterações.
| Termo | Significado | No Spring AOP |
|---|---|---|
| Join point | Um ponto da execução do programa onde se poderia adicionar comportamento | Sempre a execução de um método em um bean do Spring |
| Pointcut | Um predicado que seleciona join points | Uma expressão AspectJ como execution(* com.shop.service.*.*(..)) |
| Advice | O comportamento a executar em um join point | Um método de um aspecto anotado com @Before, @Around etc. |
| Aspecto | Um módulo que agrupa pointcuts e advices | Um bean anotado com @Aspect |
| Alvo (target) | O objeto que recebe o advice | O seu bean |
| Proxy | O objeto criado para aplicar o advice | Um proxy JDK ou CGLIB que envolve o alvo |
| Weaving | Ligar os aspectos ao código | Em tempo de execução, criando proxies durante o pós-processamento dos beans |
A restrição da primeira linha é a que importa: o Spring AOP só enxerga execuções de métodos em beans que ele mesmo criou. Acessos a campos, construtores, métodos static e objetos que você cria com new são invisíveis para ele.
Seu primeiro aspecto
Ative o mecanismo com @EnableAspectJAutoProxy em uma classe de configuração, adicione spring-aop e aspectjweaver ao classpath (este último fornece o parser de pointcuts, mesmo que nenhuma compilação com AspectJ aconteça) e declare um aspecto como um bean comum:
@Aspect
@Component
public class TimingAspect {
private static final Logger log = LoggerFactory.getLogger(TimingAspect.class);
@Around("execution(public * com.example.shop.service..*(..))")
public Object time(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed(); // executa o método real
} finally {
long micros = (System.nanoTime() - start) / 1_000;
log.debug("{} took {} µs", pjp.getSignature().toShortString(), micros);
}
}
}
Na inicialização, o auto-proxy creator inspeciona cada bean; se algum pointcut de qualquer aspecto corresponder a pelo menos um de seus métodos, o bean é substituído no contêiner por um proxy. Todos os outros beans que dependem dele recebem o proxy. Quando OrderService.confirm() é chamado no proxy, ele executa a cadeia de advices correspondente, e proceed() invoca o método real no alvo envolvido.
Tipos de advice
Existem cinco tipos de advice; escolha o menos poderoso que resolva o problema.
@Beforeé executado antes do método. Não pode impedir a chamada, a não ser lançando uma exceção.@AfterReturningé executado após um retorno normal e pode ler o valor retornado com o atributoreturning.@AfterThrowingé executado quando o método lança uma exceção e pode vinculá-la comthrowing. Ele não engole a exceção.@Afteré executado tanto na saída normal quanto na excepcional, como umfinally.@Aroundenvolve a chamada. Recebe umProceedingJoinPoint, decide se e quantas vezes chamarproceed(), pode alterar os argumentos e o valor de retorno, e pode capturar exceções. É o único advice capaz de implementar retry, cache ou demarcação de transações.
@Aspect
@Component
public class AuditAspect {
@Before("@annotation(audited) && args(id, ..)") // vincula a anotação e o primeiro argumento
public void logIntent(JoinPoint jp, Audited audited, long id) {
Audit.record(audited.value(), jp.getSignature().getName(), id);
}
@AfterThrowing(pointcut = "within(com.example.shop.payment..*)", throwing = "ex")
public void alertOnPaymentFailure(PaymentException ex) {
Alerts.raise("payment failure", ex);
}
@AfterReturning(pointcut = "execution(* com.example.shop.service.OrderService.place(..))",
returning = "order")
public void indexOrder(Order order) {
SearchIndex.add(order);
}
}
Um advice @Around precisa retornar o resultado de proceed() (ou um substituto deliberado) e declarar throws Throwable. Esquecer o return transforma cada método interceptado em um método que retorna null — um bug que assusta quando aparece e é fácil de cometer.
Expressões de pointcut
O Spring suporta um subconjunto dos designadores de pointcut do AspectJ. Estes são os que você vai usar:
| Designador | Seleciona | Exemplo |
|---|---|---|
execution(…) | Padrão de assinatura de método | execution(public * *..OrderService.*(..)) |
within(…) | Todos os métodos dos tipos de um pacote/classe | within(com.example.shop.service..*) |
@within(…) | Tipos que têm uma anotação | @within(org.springframework.stereotype.Service) |
@annotation(…) | Métodos que têm uma anotação | @annotation(com.example.Audited) |
args(…) | Tipos dos argumentos em tempo de execução | args(java.io.Serializable, ..) |
bean(…) | Padrão de nome de bean do Spring | bean(*Repository) |
this(…) / target(…) | Tipo do proxy / do alvo | target(com.example.Refundable) |
O padrão de execution se lê modifiers? return-type declaring-type?.method-name(params) throws?. Nele, * corresponde a um único elemento (qualquer tipo de retorno, qualquer nome de método, um único parâmetro), .. em uma lista de parâmetros corresponde a qualquer número de parâmetros, e .. em um nome de pacote inclui os subpacotes.
Pointcuts reutilizáveis são declarados como métodos vazios anotados com @Pointcut e combinados com &&, || e !:
@Aspect
public class SystemArchitecture {
@Pointcut("within(com.example.shop..web..*)")
public void webLayer() {}
@Pointcut("within(com.example.shop..service..*)")
public void serviceLayer() {}
@Pointcut("execution(* com.example.shop..repository.*.*(..))")
public void dataAccessOperation() {}
@Pointcut("serviceLayer() && @annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalService() {}
}
Os outros aspectos os referenciam pelo nome totalmente qualificado: @Around("com.example.shop.SystemArchitecture.serviceLayer()"). Centralizar os pointcuts em uma única classe transforma a pergunta "quais métodos são interceptados?" de um trabalho de arqueologia na leitura de um único arquivo.
Prefira pointcuts baseados em anotações (@annotation, @within) para comportamentos que um desenvolvedor deve ativar explicitamente, como auditoria ou retries; prefira pointcuts baseados em pacotes (within, execution) para regras de arquitetura que valem para uma camada inteira.
Proxies: como o truque funciona
O Spring cria um destes dois tipos de proxy.
Um proxy dinâmico do JDK é uma classe gerada em tempo de execução que implementa as interfaces do alvo e encaminha cada chamada por um InvocationHandler. Ele só pode ser usado quando existe uma interface, e só pode ser injetado pelo tipo da interface: um campo do tipo OrderServiceImpl não pode receber um proxy JDK de OrderService, e o contêiner falha com um erro "expected single matching bean" ou BeanNotOfRequiredTypeException.
Um proxy CGLIB é uma subclasse gerada da classe alvo que sobrescreve seus métodos public e protected. Não precisa de interface, pode ser injetado pelo tipo concreto e é o padrão no Spring Boot. Suas restrições são as da herança: a classe e os métodos interceptados não podem ser final, e o proxy precisa conseguir chamar um construtor — a partir do Spring 4, o Objenesis é usado para pular a invocação do construtor, então uma classe sem construtor sem argumentos também funciona.
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true) // sempre CGLIB, como o Boot faz
public class AopConfig { }
Seja qual for o tipo criado, o proxy guarda uma referência para o alvo real e delega a chamada a ele depois que a cadeia de advices é executada. Disso decorrem três consequências, que explicam quase todas as perguntas do tipo "a AOP não está funcionando".
A autoinvocação não é interceptada
@Service
public class InvoiceService {
public void closeMonth() {
for (var account : accounts()) {
issue(account); // this.issue(): chamada direta, o proxy é ignorado
}
}
@Transactional
public void issue(Account account) { … } // NÃO é transacional quando chamado a partir de closeMonth
}
O this dentro do alvo é o objeto cru, não o proxy. closeMonth chama issue no objeto cru, então o interceptor de transações nunca vê a chamada. A correção que mantém o design honesto é mover issue para um bean separado. A alternativa é injetar o próprio proxy do bean (ObjectProvider<InvoiceService> self) e chamar self.getObject().issue(account), o que é válido, mas merece um comentário, porque quem ler o código depois não vai esperar isso.
Só beans criados pelo Spring ganham proxy
Um objeto criado com new, ou retornado por um método factory que o Spring não gerencia, não tem proxy e não recebe nenhum advice. Beans prototype obtidos do contêiner ganham proxy; os objetos que eles próprios criam, não.
O tipo em tempo de execução muda
service.getClass() retorna a classe do proxy. instanceof OrderService continua funcionando com os dois tipos de proxy quando OrderService é uma interface; com CGLIB, instanceof OrderServiceImpl também funciona, porque o proxy é uma subclasse. Use AopUtils.getTargetClass(bean) ou AopProxyUtils.ultimateTargetClass nos raros trechos de código com cara de framework que precisam da classe real.
Ordem e múltiplos aspectos
Quando vários aspectos correspondem ao mesmo join point, a ordem entre eles é indefinida, a menos que você a especifique com @Order nas classes de aspecto (o menor valor é executado primeiro, ou seja, fica mais externo). Os próprios interceptores do Spring também usam Ordered.LOWEST_PRECEDENCE por padrão — a ordem do interceptor de transações é configurável em @EnableTransactionManagement —, então um aspecto próprio sem @Order não tem posição definida em relação a @Transactional. Se o seu aspecto de retry precisa rodar fora da transação para que cada tentativa ganhe uma nova, dê a ele explicitamente um valor de ordem menor e deixe o interceptor de transações com o valor padrão.
Quando o Spring AOP não basta: weaving com AspectJ
O AspectJ completo modifica o bytecode em vez de envolver objetos. Ele consegue interceptar construtores, leituras e escritas de campos, métodos static e private, e classes que o Spring nunca instancia. Existe em duas variantes: weaving em tempo de compilação, pelo compilador ajc ou por um plugin do Maven/Gradle, e weaving em tempo de carga, por um agente Java (-javaagent:aspectjweaver.jar, ou @EnableLoadTimeWeaving do Spring com um servidor ou agente de instrumentação adequado). O Spring suporta os dois e consegue injetar dependências em aspectos tecidos pelo AspectJ e até em objetos de domínio (@Configurable). O custo é um build mais complexo e stack traces que exibem métodos sintéticos. A maioria dos times nunca precisa disso; quando acha que precisa, o problema real costuma ser a autoinvocação, e a melhor correção é uma pequena refatoração.
Erros comuns
Esquecer o return em um advice @Around. Todo método interceptado retorna null. O compilador não consegue detectar isso, porque o advice retorna Object.
Interceptar com proxies JDK uma classe injetada pelo tipo concreto. A inicialização falha porque o proxy só implementa a interface. Defina proxyTargetClass = true ou injete pela interface.
Um pointcut que casa com coisas demais. execution(* *(..)) intercepta todos os beans, inclusive os internos do próprio Spring, e pode criar proxies de classes que não devem ter proxy. Sempre ancore o pointcut no seu pacote.
Esperar advice em uma classe ou método final com CGLIB. A subclasse não consegue sobrescrevê-lo; o Spring registra um warning na inicialização e o método é executado sem advice.
Colocar regra de negócio nos aspectos. Aspectos servem para interesses realmente ortogonais — medição de tempo, auditoria, transações. Regras de validação e decisões de fluxo de trabalho pertencem ao serviço, onde ficam visíveis e testáveis.
Testar um aspecto com teste unitário do alvo. Um simples new OrderService() não tem proxy. Verifique os aspectos com um contexto de teste do Spring que carregue o aspecto e o alvo, ou teste o método de advice diretamente com um ProceedingJoinPoint mockado.
Boas práticas
- Mantenha os aspectos pequenos, sem estado e em pouca quantidade. Dois ou três aspectos bem nomeados para logging, auditoria e resiliência são um número saudável; vinte são um problema de manutenção.
- Declare os pointcuts uma única vez em uma classe compartilhada e referencie-os.
- Prefira pointcuts baseados em anotações para comportamentos opcionais, para que ler um método revele o que o envolve.
- Dê a cada aspecto um
@Orderexplícito quando mais de um puder se aplicar ao mesmo método. - Padronize o uso de proxies CGLIB (
proxyTargetClass = true) para que tanto a injeção pelo tipo concreto quanto pela interface funcione, e evitefinalem beans do Spring. - Use os aspectos que já vêm com o Spring (
@Transactional,@Cacheable,@Async,@Retryabledo Spring Retry) antes de escrever os seus para a mesma finalidade.
Principais pontos
- A AOP separa o comportamento transversal em aspectos; o Spring a implementa com proxies criados em tempo de execução ao redor dos beans, limitados à execução de métodos.
- Beans
@Aspectcontêm pointcuts e advices;@Aroundé o advice mais poderoso e o único capaz de substituir ou repetir a chamada. - Expressões de pointcut combinam os designadores
execution,within,@annotation,argsebean; centralize-as em uma única classe. - Proxies JDK implementam interfaces; proxies CGLIB são subclasses do alvo e são o padrão do Spring Boot. Nenhum dos dois consegue interceptar métodos
finalou static. - Chamadas que um bean faz aos próprios métodos ignoram o proxy e não recebem advice — o bug de AOP mais comum, que afeta igualmente
@Transactionale@Cacheable. - O weaving completo do AspectJ remove essas limitações ao custo de um build mais complexo; raramente é necessário.
Fontes e leituras complementares
Perguntas frequentes
- Por que meu aspecto não é aplicado quando um método chama outro da mesma classe?
- O Spring AOP funciona envolvendo o bean em um proxy. Só recebem advice as chamadas que passam pelo proxy. Quando um método chama this.otherMethod(), a chamada vai direto para o objeto alvo e ignora o proxy, então nenhum advice é executado. Mova o método em questão para outro bean, injete o próprio proxy do bean via ObjectProvider ou passe a usar o weaving do AspectJ em tempo de compilação ou de carga se você realmente precisar interceptar a autoinvocação.
- O Spring AOP consegue interceptar métodos private, static ou final?
- Não. Um proxy JDK só consegue implementar métodos de interface; um proxy CGLIB sobrescreve métodos em uma subclasse gerada, o que exclui métodos e classes final, e métodos static ou private nem sequer são virtuais. O weaving completo do AspectJ modifica o bytecode diretamente e consegue interceptar todos eles, ao custo de configurar o build ou um agente.
- O Spring usa proxies JDK ou CGLIB por padrão?
- No Spring Framework, @EnableAspectJAutoProxy usa por padrão proxies dinâmicos do JDK quando o bean implementa pelo menos uma interface, e CGLIB caso contrário. O Spring Boot inverte esse padrão e usa CGLIB (proxyTargetClass=true) para que os beans possam ser injetados pela classe concreta. Em qualquer caso, você pode forçar proxies baseados em classe com proxyTargetClass = true.
- Spring AOP é a mesma coisa que AspectJ?
- Não. O Spring AOP reaproveita as anotações e a linguagem de pointcuts do AspectJ, mas as implementa com proxies criados em tempo de execução e limitados à execução de métodos em beans do Spring. O AspectJ é uma extensão completa da linguagem, com compilador e weaver, capaz de interceptar acesso a campos, construtores, métodos static e objetos que o Spring nunca vê. O Spring AOP atende à grande maioria das necessidades corporativas; recorra ao AspectJ só quando o modelo de proxies for de fato insuficiente.