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.

TermoSignificadoNo Spring AOP
Join pointUm ponto da execução do programa onde se poderia adicionar comportamentoSempre a execução de um método em um bean do Spring
PointcutUm predicado que seleciona join pointsUma expressão AspectJ como execution(* com.shop.service.*.*(..))
AdviceO comportamento a executar em um join pointUm método de um aspecto anotado com @Before, @Around etc.
AspectoUm módulo que agrupa pointcuts e advicesUm bean anotado com @Aspect
Alvo (target)O objeto que recebe o adviceO seu bean
ProxyO objeto criado para aplicar o adviceUm proxy JDK ou CGLIB que envolve o alvo
WeavingLigar os aspectos ao códigoEm 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 atributo returning.
  • @AfterThrowing é executado quando o método lança uma exceção e pode vinculá-la com throwing. Ele não engole a exceção.
  • @After é executado tanto na saída normal quanto na excepcional, como um finally.
  • @Around envolve a chamada. Recebe um ProceedingJoinPoint, decide se e quantas vezes chamar proceed(), 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:

DesignadorSelecionaExemplo
execution(…)Padrão de assinatura de métodoexecution(public * *..OrderService.*(..))
within(…)Todos os métodos dos tipos de um pacote/classewithin(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çãoargs(java.io.Serializable, ..)
bean(…)Padrão de nome de bean do Springbean(*Repository)
this(…) / target(…)Tipo do proxy / do alvotarget(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 @Order explí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 evite final em beans do Spring.
  • Use os aspectos que já vêm com o Spring (@Transactional, @Cacheable, @Async, @Retryable do 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 @Aspect contê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, args e bean; 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 final ou 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 @Transactional e @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.