qinfengge

qinfengge

醉后不知天在水,满船清梦压星河
github

Java Asynchronous and Return

Recently, I learned a few points at work that are very useful, so I'm planning to write a few blog posts to record them.

Last week, I received a new requirement to capture program errors and send notifications to the responsible person. The implementation is not difficult, but the challenge lies in how to implement it elegantly. Fortunately, I have learned two points that can be combined to achieve the requirement perfectly.

Global Exception Handling#

Global exception handling is very simple, only requiring two annotations: @RestControllerAdvice and @ExceptionHandler.

First, the explanation of @RestControllerAdvice is that @RestControllerAdvice is a composite annotation composed of @ControllerAdvice and @ResponseBody, and @ControllerAdvice inherits from @Component, so @RestControllerAdvice is essentially a Component.

@ExceptionHandler is an exception interceptor that can be used to declare which exception class to catch.

These two annotations together form a global exception interceptor.

@RestControllerAdvice
public class GlobalController {

    @ExceptionHandler
    public void notifyWeChat(Exception e) throws 
        log.error(e)
    }
}

Robot Notification#

Okay, now we have a global exception interceptor, so we can intercept error messages whenever the program encounters an error. But we still need to send notifications to the responsible person. Is there an elegant way to do this?

Of course, there is. Dinger is an elegant message notification middleware that supports using Spring Boot to integrate DingTalk/WeChat Work/Feishu group robots for message notification.

Here is the official development documentation.

First, import the dependency.

<dependency>
    <groupId>com.github.answerail</groupId>
    <artifactId>dinger-spring-boot-starter</artifactId>
    <version>${dinger.version}</version>
</dependency>

Taking WeChat Work robot as an example, the configuration file is as follows.

#Dinger
spring.dinger.project-id=sa-token
#WeChat Work robot token
spring.dinger.dingers.wetalk.token-id=xxx
#Configure the mobile phone numbers of @ members
wetalk.notify.phones = 17633*****,17633*****

Then, define an interface.

public interface DingerConfig {
    @DingerText(value = "Order ${orderNum} placed successfully, order amount ${amt}")
    DingerResponse orderSuccess(
            @DingerPhone List<String> phones,
            @Parameter("orderNum") String orderNo,
            @Parameter("amt") BigDecimal amt
    );

    @DingerMarkdown(
            value = "#### Method Error\n - Request Time: ${requestTime}\n - Request Path: ${requestPath}\n - Request Parameters: ${queryString}\n - Error Message: ${exceptionMessage}",
            title = "Error Details"
    )
    DingerResponse exceptionNotify(@DingerPhone List<String> phones, String requestTime, String requestPath, String queryString, String exceptionMessage);
}

@DingerText is used to send text messages, while @DingerMarkdown is used to send messages in markdown format. Note that only text messages can correctly @ users.

Then, in the startup class, add the package to scan.

@DingerScan(basePackages = "xyz.qinfengge.satokendemo.dinger")

To configure @ specified mobile phone users, just add a component that reads information from the configuration file and converts it into the required format.

@Component
public class NotifyPhones {

    @Value("${wetalk.notify.phones}")
    private String phones;

    public List<String> handlePhones() {
        return Arrays.asList(phones.split(","));
    }
}

Finally, it can be used.

List<String> phones = notifyPhones.handlePhones();
String requestDate = DateUtil.format(new Date(), "yyyy-MM-dd--HH:mm:ss");
dingerConfig.exceptionNotify(phones, requestDate, request.getServletPath(), parameters, e.getMessage());

Asynchronous#

Now we can capture exceptions and send messages, but if there is an error accessing the interface, you will notice that the speed has obviously slowed down. So how can we optimize it?

For operations with low real-time requirements, we can use asynchronous processing.

Using asynchronous processing is also very simple, just add the @Async annotation to the method to indicate that it is an asynchronous method, and add @EnableAsync to the startup class to enable asynchronous processing.

The modified code is as follows.

@RestControllerAdvice
public class GlobalController {

    @Resource
    private NotifyPhones notifyPhones;

    @Resource
    private DingerConfig dingerConfig;

    @ExceptionHandler
    @Async
    public void handleException(Exception e) {
        List<String> phones = notifyPhones.handlePhones();
        String requestDate = DateUtil.format(new Date(), "yyyy-MM-dd--HH:mm:ss");
        dingerConfig.exceptionNotify(phones, e.getMessage());
    }
}

However, another problem arises. After adding asynchronous processing, you will find that the return value of the method can only be void or CompletableFuture. If there is an error in the interface, there will be no return value. Therefore, further modifications are needed to make it return the normal interface structure.

The final structure is as follows.

The HttpServletRequest is added to obtain the request path and request parameters, and other things can be added, such as the IP address of the request.

@Slf4j
@RestControllerAdvice
public class GlobalController {

    @Resource
    private NotifyPhones notifyPhones;

    @Resource
    private DingerConfig dingerConfig;

    @ExceptionHandler
    public Result<Object> notifyWeChat(HttpServletRequest request, Exception e) throws ExecutionException, InterruptedException {
        return Result.fail(this.handleException(request, e).get());
    }

    /**
     * Global exception handling
     *
     * @param e Exception
     * @return Exception information
     */
    @Async
    public CompletableFuture<Result<Object>> handleException(HttpServletRequest request, Exception e) {
        // Get the asynchronous context in the asynchronous method
        AsyncContext asyncContext = request.startAsync();
        List<String> phones = notifyPhones.handlePhones();
        String requestDate = DateUtil.format(new Date(), "yyyy-MM-dd--HH:mm:ss");
        Map<String, String[]> parameterMap = request.getParameterMap();
        String parameters = JSON.toJSONString(parameterMap);
        dingerConfig.exceptionNotify(phones, requestDate, request.getServletPath(), parameters, e.getMessage());
        log.error(MessageFormat.format("Request {0} error, request parameters {1}, error message: {2}", request.getServletPath(), parameters, e.getMessage()));
        // After the asynchronous method is executed, call the complete method to notify the container to end the asynchronous call
        // Recycle the request
        asyncContext.complete();
        // Return the result. Asynchronous methods can only return CompletableFuture or void, so you need to return a CompletableFuture first and then call its method to get the value inside
        return CompletableFuture.supplyAsync(() -> Result.fail(MessageFormat.format("Request {0} error, request parameters {1}, error message: {2}", request.getServletPath(), parameters, e.getMessage())));
//        return CompletableFuture.completedFuture(Result.fail(MessageFormat.format("Request {0} error, request parameters {1}, error message: {2}", request.getServletPath(), parameters, e.getMessage())));
    }
}

The final result is as follows.

image

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.