Troubleshooting of undeclaredtrowableexception encountered by custom SPI using JDK dynamic agent

Posted by DeadlySin3 on Tue, 14 Dec 2021 04:33:21 +0100

preface

We talked about the last article How to integrate custom SPI with sentinel to realize fuse current limiting . In the process of implementing integration testing, an interesting exception Java lang.reflect. Undeclaredtowableexception. At that time, a global exception was caught in the code layer. The example is as follows

@RestControllerAdvice
public class GlobalExceptionHandler {


    @ExceptionHandler(Exception.class)
    public AjaxResult handleException(Exception e) {
        String msg = e.getMessage();
        return AjaxResult.error(msg,500);
    }


    @ExceptionHandler(BlockException.class)
    public AjaxResult handleBlockException(BlockException e) {
        String msg = e.getMessage();
        return AjaxResult.error(msg,429);
    }

}

Originally, it was expected that BlockException would be captured when current limiting was triggered, and then a layer of rendering would be encapsulated. Unexpectedly, BlockException could not be captured alive and dead.

Troubleshooting

Through debug ging, it is found that the problem is caused by the jdk dynamic agent. Later, I found some information and found such a paragraph in the official API document

The main idea is that if the invoke method of the call handler of the proxy instance throws a checked exception (which cannot be assigned to RuntimeException or Throwable of Error), and the exception cannot be assigned to any exception class declared by the throws sub Office of the method, the method call on the proxy instance throws an undeclaredtrowableexception.

In this passage, we can analyze the following scenarios

1. There is no declared exception on the real instance method, and the checked exception is thrown when the proxy instance is called

2. The real instance method declares a non checked exception, and the proxy instance throws a checked exception when calling

Solution

Scheme 1: the real instance also declares the detected exception

Example:

public class SqlServerDialect implements SqlDialect {
    @Override
    public String dialect() throws Exception{
        return "sqlserver";
    }

Scheme 2: jdk can capture the invoke of dynamic agent, and can customize the exception thrown at the same time

Example:

 @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        CircuitBreakerInvocation invocation = new CircuitBreakerInvocation(target,method,args);

        try {

            return new CircuitBreakerInvoker().proceed(invocation);
        } catch (Throwable e) {
            throw new CircuitBreakerException(429,"too many request");
        }

    }

Scheme 3: catch the InvocationTargetException exception and throw the real exception

The reason why we need InvocationTargetException is that our custom exception will be wrapped by InvocationTargetException

Example

  @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        CircuitBreakerInvocation invocation = new CircuitBreakerInvocation(target,method,args);

        try {

            return new CircuitBreakerInvoker().proceed(invocation);
            //The package with InvocationTargetException is Java lang.reflect. Undeclaredtowableexception problem
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }

    }

summary

If it is a component implemented by ourselves, we recommend directly using scheme 3, that is, capturing InvocationTargetException exceptions.

If the component is implemented by a third party, the recommended scheme 1 is to declare an exception in the called instance method. For example, when using springcloud alibaba sentinel, there is a probability that an undeclaredtowableexception exception will occur, because it is also based on a dynamic agent, and the BlockException thrown by it is also a checked exception. Examples are as follows

public class SqlServerDialect implements SqlDialect {
    @Override
    public String dialect() throws BlockException{
        return "sqlserver";
    }

If you use a third-party component instead of scheme 1, you can also add a layer of agent on the basis of the third-party component, or intercept the third-party component

demo link

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-spi-enhance/springboot-spi-framework-circuitbreaker

Topics: Java Dynamic Proxy sentinel