Recently, I learned a few points at work that are very useful, so I plan 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.
On the other hand, @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 a graceful 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 number ${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 scan path.
@DingerScan(basePackages = "xyz.qinfengge.satokendemo.dinger")
To configure @ specified mobile phone users, just add a component.
Read the information from the configuration file and convert 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, you can use it.
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 a significant slowdown in speed. So how can we optimize it?
For operations with low real-time requirements like this, 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 then 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 modification is needed to make it return the normal interface structure.
The final structure is as follows.
Add HttpServletRequest
to get the request path and parameters, and you can also add other things 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.