Como lidar com exceções em AOP baseado em reflexão

Ao trabalhar em um projeto recente, um problema surgiu com proxies dinâmicos de JDK e AOP baseado em reflexão. Em vez de usar o Spring AOP tradicional, o projeto utiliza o ReflectionUtils para interceptar chamadas de método dinamicamente. Essa abordagem oferece flexibilidade, mas também traz desafios no tratamento de exceções. Vamos entender melhor essa questão e como ela foi resolvida.

O Problema com Proxies Dinâmicos de JDK e AOP

Em vez de usar as anotações padrão do Spring AOP, o sistema se baseia em proxies implementados manualmente que interceptam as chamadas de método via reflexão. Isso permite aplicar preocupações transversais de forma dinâmica, mas também altera a forma como as exceções se propagam pelo sistema. O problema surgiu quando um método lançou uma exceção que deveria ser capturada em um nível superior, mas, em vez disso, foi encapsulada como uma UndeclaredThrowableException. Esse comportamento interrompeu a lógica de negócios, já que o tratamento de exceções era uma parte crucial dela.

Para ilustrar o problema, veja um exemplo simplificado da estrutura do código:

public class ServiceA {
    public void methodA() {
        try {
            try {
                new ServiceB().methodB();
            } catch (SpecialException ex) {
                System.out.println("Caught SpecialException in methodA");
            }
        } catch (Exception ex) {
            System.out.println("Caught Exception in methodA: " + ex.getClass().getName());
        }
    }
}

public class ServiceB {
    public void methodB() throws SpecialException{
        new ServiceC().methodC();
    }
}

public class ServiceC {
    public void methodC() throws SpecialException {
        throw new SpecialException("This is a special exception");
    }
}

public class SpecialException extends Exception {
    public SpecialException(String message) {
        super(message);
    }
}

Comportamento Esperado vs. Comportamento Real

O comportamento esperado era que methodC() lançasse SpecialException e methodA() capturasse essa exceção no bloco try-catch interno. No entanto, na prática, a exceção subia, mas não era capturada em methodA(). Em vez disso, methodA() capturava UndeclaredThrowableException no bloco catch externo. Essa diferença de comportamento causou a interrupção da lógica de negócios, pois a exceção não estava sendo tratada no local esperado.

A questão central é que o uso de proxies dinâmicos de JDK e AOP alterou a forma como as exceções eram propagadas, levando a um comportamento inesperado no tratamento de erros.

O Papel do AOP e dos Proxies Dinâmicos de JDK

O problema ocorreu porque methodB() estava sendo interceptado por um proxy AOP. Quando um método é interceptado usando proxies dinâmicos de JDK, a chamada é roteada por meio de um manipulador de invocação (invocation handler) em vez de ser executada diretamente. Isso muda a forma como as exceções são tratadas, pois elas são encapsuladas dentro de uma InvocationTargetException ou UndeclaredThrowableException antes de se propagarem. Como resultado, a lógica normal de captura de exceções no chamador pode não funcionar como o esperado. No momento em que methodB() foi chamado, ele foi realmente invocado por meio de um proxy dinâmico de JDK, alterando o comportamento de propagação da exceção.

No projeto, o AOP foi implementado usando ReflectionUtils. Abaixo está um exemplo de código:

public class AopProxyHandler {

    @Around("execution(* com.example.ServiceB.methodB(..))")
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        ...
        return ReflectionUtils.invokeMethod(method, target, args);

    }
}

Assim, a cadeia de chamadas de método se torna:

  • ServiceA.methodA() chama ServiceB.methodB().
  • O objeto proxy de ServiceB.methodB() invoca ReflectionUtils.invokeMethod().
  • Dentro de ReflectionUtils.invokeMethod(), ServiceB.methodC() é chamado.
  • ServiceB.methodC() lança SpecialException.
  • ReflectionUtils.invokeMethod() captura SpecialException, mas não pode lançar diretamente essa exceção verificada porque invoke() não declara SpecialException.

Análise do Código Fonte do ReflectionUtils

Para entender melhor o que aconteceu, vamos analisar o código de ReflectionUtils.invokeMethod:

public abstract class ReflectionUtils {

    @Nullable
    public static Object invokeMthod(Method method, @Nullable Object target, @Nullable Object... args){
        try{
            return method.invoke(target,args);
        } catch (Exception var4){
            Exception ex = var4;
            handleReflectionException(ex);
            throw new IllegalStateException("should never get here");
        }
    }
}

Como uma exceção foi lançada anteriormente, o método não continua a execução para method.invoke(target,args);, mas vai para a lógica de catch. Vamos explorar o código fonte para ver o que acontece a seguir:

public abstract class ReflectionUtils {
    public static void handleReflectionException(Exception ex) {
        if (ex instanceof NoSuchMethodException) {
            throw new IllegalStateException("Method not found: " + ex.getMessage());
        } else if (ex instanceof IllegalAccessException) {
            throw new IllegalStateException("Could not access method or field: " + ex.getMessage());
        } else {
            if (ex instanceof InvocationTargetException) {
                handleInvocationTargetException((InvocationTargetException)ex);
            }

            if (ex instanceof RuntimeException) {
                throw (RuntimeException)ex;
            } else {
                throw new UndeclaredThrowableException(ex);
            }
        }
    }

    public static void handleInvocationTargetException(InvocationTargetException ex){
        rethrowRuntimeException(ex.getTargetException());
    }

    public static void rethrowRuntimeException(Throwable ex) {
        if (ex instanceof RuntimeException) {
            throw (RuntimeException)ex;
        } else if (ex instanceof Error) {
            throw (Error)ex;
        } else {
            throw new UndeclaredThrowableException(ex);
        }
    }
}

Como SpecialException é uma exceção verificada (checked exception), ela não se enquadra em RuntimeException ou Error, levando ao encapsulamento final dentro de UndeclaredThrowableException.

A Solução para o Problema

Para resolver este problema, duas abordagens foram consideradas:

  1. Desembrulhar manualmente a UndeclaredThrowableException

A lógica AOP foi modificada para capturar e desembrulhar a exceção antes que ela se propagasse ainda mais. O código modificado se parece com:

public class AopProxyHandler {
    @Around("execution(* com.example.ServiceB.methodB(..))")
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            return ReflectionUtils.invokeMethod(method, target, args);
        } catch (UndeclaredThrowableException ex) {
            Throwable cause = ex.getCause();
            if (cause instanceof SpecialException) {
                throw (SpecialException) cause;
            }
            throw ex;
        }
    }
}

Agora, quando methodC() lança SpecialException, o proxy AOP a desembrulha antes de retornar o controle para methodA(), permitindo que ela seja capturada como esperado.

  1. Usar throws Throwable para ser compatível com todas as exceções

Se não for possível modificar a lógica do proxy e você quiser que methodA() lide com SpecialException, você pode tornar a instrução catch de methodA() compatível com todas as exceções:

public void methodA() {
    try {
        try {
            methodB();
        } 
    } catch (Throwable e) { 
        if (e instanceof UndeclaredThrowableException) {
            Throwable cause = ((UndeclaredThrowableException) e).getUndeclaredThrowable();
            if (cause instanceof SpecialException) {
                System.out.println("Caught SpecialException from UndeclaredThrowableException: " + cause.getMessage());
            }
        } else {
            System.out.println("Caught Exception: " + e.getMessage());
        }
    }
}

Essa abordagem permite capturar a exceção encapsulada e tratá-la adequadamente em methodA().

Principais Conclusões Sobre o Uso de Proxies Dinâmicos de JDK

  1. Proxies dinâmicos de JDK encapsulam exceções verificadas não declaradas em UndeclaredThrowableException.
  2. Spring AOP com proxies JDK pode alterar a propagação de exceções, afetando a lógica de negócios.
  3. Desembrulhar exceções dentro do AOP garante que elas se propaguem corretamente.

Compreender os detalhes internos dos proxies Java e do Spring AOP pode ajudar a evitar esses problemas no código de produção e garantir o tratamento adequado de exceções nas aplicações. Para se aprofundar em temas relacionados, você pode verificar como organizar seu feed do Google Discover de forma prática e descobrir cinco recursos essenciais do novo SUV da Xiaomi.

Este conteúdo foi auxiliado por Inteligência Artificial, mas escrito e revisado por um humano.

Leave a Comment

Exit mobile version