qinfengge

qinfengge

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

Java异步及返回

最近在工作中,学到了几个知识点,还是很有用的,因此准备写几篇 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())));
    }

}

最后的结果就是这样的

image

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。