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.