最近在工作中,学到了几个知识点,还是很有用的,因此准备写几篇 blog 记录一下
上周接到一个新的需求,要获取程序的报错,并发送通知给负责人,实现其实不难,但难的是如何优雅的实现,而刚好我就看过 2 个知识点,结合一下就能完美实现需求
全局异常处理#
全局异常处理很简单,只需要 2 个注解。@RestControllerAdvice
和 @ExceptionHandler
首先 @RestControllerAdvice
的解释是 @RestControllerAdvice 是一个组合注解,由 @ControllerAdvice、@ResponseBody 组成,而 @ControllerAdvice 继承了 @Component,因此 @RestControllerAdvice 本质上是个 Component
而 @ExceptionHandler
则是一个异常拦截器,可以使用 @ExceptionHandler({Exception.class}) *//申明捕获那个异常类*
这两个注解加起来就是全局的异常拦截器
@RestControllerAdvice
public class GlobalController {
@ExceptionHandler
public void notifyWeChat(Exception e) throws
log.error(e)
}
机器人通知#
ok,现在我们有了全局的异常拦截器,只要程序报错,我们就能拦截到错误信息。但我们还需要发送通知给负责人啊。有没有什么优雅的通知方式呢?
当然有了,叮鸽 就是一个优雅的消息通知中间件,它支持使用 Spring Boot 集成钉钉 / 企业微信 / 飞书群机器人实现消息通知。
此处 是官方的开发文档。
首先引入依赖
<dependency>
<groupId>com.github.answerail</groupId>
<artifactId>dinger-spring-boot-starter</artifactId>
<version>${dinger.version}</version>
</dependency>
以企业微信机器人为例,配置文件
#Dinger
spring.dinger.project-id=sa-token
#微信机器人token
spring.dinger.dingers.wetalk.token-id=xxx
#配置@成员的手机号
wetalk.notify.phones = 17633*****,17633*****
然后需要定义一个接口
public interface DingerConfig {
@DingerText(value = "订单号${orderNum}下单成功啦, 下单金额${amt}")
DingerResponse orderSuccess(
@DingerPhone List<String> phones,
@Parameter("orderNum") String orderNo,
@Parameter("amt") BigDecimal amt
);
@DingerMarkdown(
value = "#### 方法错误\n - 请求时间: ${requestTime}\n - 请求路径: ${requestPath}\n - 请求参数: ${queryString}\n - 错误信息: ${exceptionMessage}",
title = "错误详情"
)
DingerResponse exceptionNotify(@DingerPhone List<String> phones, String requestTime, String requestPath, String queryString, String exceptionMessage);
@DingerText
是发送文本类型的消息,而 @DingerMarkdown
是发送 markdown 格式的消息,注意只有文本消息能正确的 @用户。
然后在启动类,添加扫描包路径
@DingerScan(basePackages = "xyz.qinfengge.satokendemo.dinger")
配置 @指定手机号用户,只需要添加一个组件
从配置文件中读取信息,然后变成需要的格式即可
@Component
public class NotifyPhones {
@Value("${wetalk.notify.phones}")
private String phones;
public List<String> handlePhones() {
return Arrays.asList(phones.split(","));
}
}
最后使用即可
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());
异步#
现在我们能捕获异常并发送消息了,但是访问接口出错的话,能感受到速度明显变慢了,那么怎么优化呢?
一般对于这种实时性要求比较低的操作,我们可以使用异步
使用异步也很简单,只需要在方法上加上 @Async
注解,表明这是个异步方法,然后在启动类上加上 @EnableAsync
开启异步即可。
加上异步是这样的
@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());
}
}
但是,另一个问题出现了,你会发现加了异步后,方法的返回值只能是 void 或者 **CompletableFuture ** 了,如果接口出错了那就没有返回了,所以还需要再改造一下,让它返回正常的接口结构
那么,最终的结构就是这样的
加入了 HttpServletRequest
来获取请求路径和请求参数,还可以加入其他的东西,比如请求的 IP 地址等。
@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());
}
/**
* 全局异常拦截
*
* @param e 异常
* @return 异常信息
*/
@Async
public CompletableFuture<Result<Object>> handleException(HttpServletRequest request, Exception e) {
// 在异步方法中获取异步上下文
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("请求 {0} 出错,请求参数{1},错误信息:{2}", request.getServletPath(), parameters, e.getMessage()));
// 异步方法执行完毕后,调用complete方法通知容器结束异步调用
// 回收request
asyncContext.complete();
// 返回结果,异步方法只能返回CompletableFuture或void,因此需要先返回一个CompletableFuture再调用其方法获取里面的值
return CompletableFuture.supplyAsync(() -> Result.fail(MessageFormat.format("请求 {0} 出错,请求参数{1},错误信息:{2}", request.getServletPath(), parameters, e.getMessage())));
// return CompletableFuture.completedFuture(Result.fail(MessageFormat.format("请求 {0} 出错,请求参数{1},错误信息:{2}", request.getServletPath(), parameters, e.getMessage())));
}
}
最后的结果就是这样的