tio-boot 案例 - 全局异常捕获与企业微信群通知

简介

在现代应用的开发中,稳定性和健壮性是至关重要的。生产环境中难免会遇到不可预见的异常,如何及时发现和处理这些异常是每个开发者和运维团队关心的问题。tio-boot 作为一款轻量级高性能框架,可以通过集成企业通知服务(如企业微信、飞书等)实现对异常的实时监控和告警。本文将介绍如何在 tio-boot 框架中实现全局异常捕获,并通过企业微信将异常信息推送到指定群组,帮助团队及时获悉和处理生产中的问题。

本案例旨在介绍如何在 tio-boot 框架中实现全局异常捕获,并将异常信息实时推送到企业微信群,从而实现及时的异常告警和问题跟踪。实现这一功能,涉及到以下关键知识点:

  • tio-boot 的全局异常处理机制。
  • tio-boot 的 notification(通知)组件的使用。

实现思路

我们可以通过 tio-boot 框架中的全局异常处理机制捕获所有未被显式处理的异常,并将这些异常信息打包成告警通知,推送到企业微信的群组,以实现对生产环境问题的快速响应。

全局异常捕获的意义:

  1. 减少遗漏:通过全局异常捕获机制,我们可以确保不会遗漏任何未被捕获的异常,尤其是在生产环境中,可以第一时间发现潜在的问题。
  2. 实时反馈:结合企业微信的 webhook 功能,可以将异常实时推送到群组,运维或开发团队可以及时响应,避免问题扩大。
  3. 问题跟踪:通过异常日志和推送的信息,团队可以更好地分析问题根源,记录问题处理的过程。

实现方案的主要步骤包括:

  • 配置项目的基础依赖和 webhook 地址。
  • 自定义全局异常捕获器,在捕获到异常时,构造异常信息推送模型。
  • 利用 okhttp 等库向企业微信的群组发送异常告警信息。

完整的开发步骤

1. 配置项目依赖(pom.xml)

首先,我们需要确保项目能够正常运行,并且可以使用 tio-bootokhttp 来处理通知请求。

代码片段:pom.xml

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <maven.compiler.source>${java.version}</maven.compiler.source>
    <maven.compiler.target>${java.version}</maven.compiler.target>
    <tio-boot.version>1.5.2</tio-boot.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>com.litongjava</groupId>
      <artifactId>tio-boot</artifactId>
      <version>${tio-boot.version}</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.30</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>okhttp</artifactId>
      <version>3.14.9</version>
    </dependency>
  </dependencies>

2. 配置 Webhook 地址

在配置文件中设置企业微信的 webhook URL,用于发送异常通知。 文件内容:app.properties

app.name=tio-boot-example-push-exception-to-wecome-demo
notification.webhook.url=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxx-xxxxx-xxxx-xxxx-xxxxxxxxxx

3. 创建应用启动类

初始化并启动 tio-boot 应用,设置组件扫描的包路径。

代码片段:ExceptionPushToWecomeApp.java

package com.litongjava.tio.web.hello;

import com.litongjava.jfinal.aop.annotation.AComponentScan;
import com.litongjava.tio.boot.TioApplication;

@AComponentScan
public class ExceptionPushToWecomeApp {
  public static void main(String[] args) {
    long start = System.currentTimeMillis();
    TioApplication.run(ExceptionPushToWecomeApp.class, args);
    long end = System.currentTimeMillis();
    System.out.println((end - start) + "ms");
  }
}

4. 实现全局异常处理器

通过实现 TioBootExceptionHandler 接口,自定义异常处理逻辑。在处理异常时,将异常信息通过 webhook 推送到企业微信群。

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.Map;

import com.litongjava.tio.boot.constatns.TioBootConfigKeys;
import com.litongjava.tio.boot.exception.TioBootExceptionHandler;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.utils.HttpIpUtils;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.litongjava.tio.utils.network.IpUtils;
import com.litongjava.tio.utils.notification.LarksuiteNotificationUtils;
import com.litongjava.tio.utils.notification.NotifactionWarmModel;
import com.litongjava.tio.utils.resp.RespVo;
import com.litongjava.tio.utils.thread.TioThreadUtils;

import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;

@Slf4j
public class GlobalExceptionHadler implements TioBootExceptionHandler {

  @Override
  public RespVo handler(HttpRequest request, Throwable e) {
    String requestId = request.getChannelContext().getId();

    String requestLine = request.getRequestLine().toString();
    String host = request.getHost();
    Map<String, String> headers = request.getHeaders();
    String bodyString = request.getBodyString();

    String realIp = HttpIpUtils.getRealIp(request);

    // 获取完整的堆栈跟踪
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    e.printStackTrace(pw);
    String stackTrace = sw.toString();

    log.info("requestId,{},{},{},{},{}", requestId, host, requestLine, headers, bodyString, stackTrace);
    NotifactionWarmModel model = new NotifactionWarmModel();

    String localIp = IpUtils.getLocalIp();
    model.setAppGroupName("tio-boot");
    model.setAppName(EnvUtils.get(TioBootConfigKeys.APP_NAME));
    model.setWarningName("GlobalExceptionHadler");
    model.setLevel("普通级别");
    String env = EnvUtils.env();

    model.setDeviceName(localIp);
    model.setTime(new Date());
    String content = "env:" + env + ",requestId:" + requestId + ",user ip:" + realIp + " fetch " + host + " uri:" + requestLine + "\n error:" + stackTrace;
    model.setContent(content);

    if (!EnvUtils.isDev()) {
      TioThreadUtils.submit(() -> {
        try (Response response = NotificationUtils.sendWarm(EnvUtils.get("warm.notification.webhook.url"), model)) {
          if (!response.isSuccessful()) {
            try {
              log.info("Faild to push :{}", response.body().string());
            } catch (IOException e1) {
              e1.printStackTrace();
            }
          }
        }
      });
    }

    return RespVo.fail(e.getMessage());
  }
}

这段代码实现了一个全局异常处理器,用于捕获并处理 tio-boot 项目中的所有未处理的异常,同时将这些异常的信息通过 NotificationUtils 发送给开发者。

代码说明:

  1. GlobalExceptionHandler 实现了 TioBootExceptionHandler 接口

    • TioBootExceptionHandler 是用于处理 tio-boot 框架中异常的接口。
    • GlobalExceptionHandler 覆写了 handler(HttpRequest request, Throwable e) 方法,用来处理传入的 HTTP 请求及其产生的异常。
  2. 异常处理逻辑

    • request.getChannelContext().getId();:获取请求的 ChannelContext ID,即请求的唯一标识符。
    • request.getRequestLine().toString();:获取请求的第一行,包括 HTTP 方法、URI 和 HTTP 版本。
    • request.getHost();:获取请求中的主机。
    • request.getHeaders();:获取请求头的所有键值对。
    • request.getBodyString();:获取请求的正文内容。
  3. 获取客户端 IP 地址

    • 使用 HttpIpUtils.getRealIp(request); 来获取请求的真实 IP 地址,通常会检查请求头中的 X-Forwarded-For 或者 X-Real-IP 等头信息。
  4. 获取异常堆栈信息

    • StringWriterPrintWriter 用于将异常堆栈跟踪信息转换为字符串。
    • e.printStackTrace(pw);:将异常的堆栈信息写入 PrintWriter,并最终存入 sw.toString() 中。
  5. 日志记录

    • log.info("requestId,...") 记录了请求 ID、主机、请求行、请求头、请求体和异常堆栈跟踪信息。
  6. 创建通知模型

    • 使用 NotifactionWarmModel 创建通知模型,包含应用名、设备 IP 地址、异常描述等信息。
    • 设置通知的内容,包含请求 ID、用户 IP 地址、请求 URI 和异常堆栈信息。
  7. 发送通知

    • 如果当前环境不是开发环境(通过 EnvUtils.isDev() 判断),则通过异步线程池 TioThreadUtils.submit() 发送通知。
    • 使用 NotificationUtils.sendWarm() 发送通知到预先配置的 webhook URL,URL 是从环境变量中获取的 warm.notification.webhook.url
    • 检查通知是否发送成功,若失败则记录失败信息。
  8. 异常处理的容错

    • 通过 try-with-resources 关闭 Response,保证资源的自动释放。
    • 如果在发送通知过程中发生异常,会捕获并打印异常堆栈信息。
  9. 返回错误信息

    • 将错误信息返回的到客户端

总体功能:

当 HTTP 请求发生异常时,该处理器会:

  • 获取请求的详细信息(例如请求行、主机、头信息、请求体)。
  • 获取客户端 IP 地址和异常的完整堆栈跟踪信息。
  • 通过日志记录这些信息。
  • 在生产环境中,利用异步线程发送异常通知给开发团队,方便及时排查问题。

这样做的好处是能够集中管理应用中的异常,并通过通知机制确保开发人员能及时知晓异常事件。

5. 配置异常处理器

在 TioBootServer 的配置类中注册自定义的异常处理器。

代码片段:TioBootServerConfiguration.java

import com.litongjava.jfinal.aop.annotation.AConfiguration;
import com.litongjava.jfinal.aop.annotation.AInitialization;
import com.litongjava.tio.boot.server.TioBootServer;

@AConfiguration
public class TioBootServerConfiguration {

  @AInitialization
  public void config() {
    TioBootServer.me().setExceptionHandler(new GlobalExceptionHadler());
  }
}

6. 创建触发异常的控制器

通过构建一个故意触发异常的控制器,测试异常处理器是否能够正常工作,并验证企业微信群是否能收到正确的告警信息。 开发一个简单的控制器,用于模拟触发异常的场景。

package com.litongjava.tio.web.hello.controller;

import com.litongjava.tio.http.server.annotation.RequestPath;

@RequestPath("/exception")
public class ExceptionController {

  @RequestPath()
  public Integer index() {
    return (0 / 0);
  }
}

7. 测试与验证

启动应用并访问 /exception 路径,触发异常。如果配置正确,你应该能在指定的企业微信群收到异常告警信息。

企业微信群收到的告警信息示例:

- Alarm Time : 2024-09-03 19:48:46
- App Group Name : tio-boot
- App Name : rumibot-backend-api
- Alarm name : 运行异常
- Alarm Level : 普通级别
- Alarm Device : 172.23.176.1
- Alarm Content :
user ip:0:0:0:0:0:0:0:1 fetch localhost uriGET /exception HTTP/1.1
 error:java.lang.ArithmeticException: / by zero
	at com.litongjava.open.chat.controller.ExceptionController.index(ExceptionController.java:10)
	at com.litongjava.open.chat.controller.ExceptionControllerMethodAccess.invoke(Unknown Source)
	at com.esotericsoftware.reflectasm.MethodAccess.invoke(MethodAccess.java:39)
	at com.litongjava.tio.boot.http.handler.RequestActionDispatcher.executeAction(RequestActionDispatcher.java:80)
	at com.litongjava.tio.boot.http.handler.DynamicRequestHandler.processDynamic(DynamicRequestHandler.java:23)
	at com.litongjava.tio.boot.http.handler.DefaultHttpRequestHandler.handler(DefaultHttpRequestHandler.java:319)
	at com.litongjava.tio.http.server.HttpServerAioHandler.handler(HttpServerAioHandler.java:86)
	at com.litongjava.tio.boot.server.TioBootServerHandler.handler(TioBootServerHandler.java:142)
	at com.litongjava.tio.core.task.HandlerRunnable.handler(HandlerRunnable.java:67)
	at com.litongjava.tio.core.task.DecodeRunnable.handler(DecodeRunnable.java:59)
	at com.litongjava.tio.core.task.DecodeRunnable.decode(DecodeRunnable.java:215)
	at com.litongjava.tio.core.ReadCompletionHandler.completed(ReadCompletionHandler.java:83)
	at com.litongjava.tio.core.ReadCompletionHandler.completed(ReadCompletionHandler.java:21)
	at sun.nio.ch.Invoker.invokeUnchecked(Invoker.java:126)
	at sun.nio.ch.Invoker$2.run(Invoker.java:218)
	at sun.nio.ch.AsynchronousChannelGroupImpl$1.run(AsynchronousChannelGroupImpl.java:112)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

8.推送消息到 Lark

NotificationUtils 默认推送消息到企业微信,过你希望推送消息到 lark,可以使用 tio-boot 内置的 LarksuiteNotificationUtils

LarksuiteNotificationUtils.sendWarm(EnvUtils.get("warm.notification.webhook.url"), model);

结语

通过上述步骤,我们演示了如何在 tio-boot 框架中实现异常捕获,并将异常信息实时推送到企业微信群。这种实时监控和通知机制对于及时发现并处理生产环境中的问题至关重要。