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:
- 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.
- 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
- Proxies dinâmicos de JDK encapsulam exceções verificadas não declaradas em UndeclaredThrowableException.
- Spring AOP com proxies JDK pode alterar a propagação de exceções, afetando a lógica de negócios.
- 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.