前言

在我们开发过程中,出现bug是非常常见的,不会说产品一旦上线就没有bug,出现bug没关系,关键是需要能够及时发现异常。

当工程基本完成,开始部署到生产环境上,线上的工程一旦出现异常时,开发团队就需要主动感知异常并协调处理,当然人不能一天24小时去盯着线上工程,

所以就需要一种机制来自动化的对异常进行通知,并精确到谁负责的那块代码。这样会极大地方便后续的运维。

本项目的开发愿景是为了给使用者在线上项目的问题排查方面能够带来帮助,简单配置,做到真正的开箱即用,同时异常信息尽量详细,帮助开发者快速定位问题。

目前支持基于钉钉,企业微信和邮箱的异常通知

一、核心代码

这里只展示一些核心代码,具体的完整代码可以看github: spring-boot-exception-notice

1、异常信息数据model

@Data
public class ExceptionInfo {

    /** 工程名 */
    private String project;

    /** 异常的标识码 */
    private String uid;

    /** 请求地址 */
    private String reqAddress;

    /** 方法名 */
    private String methodName;

    /** 方法参数信息 */
    private Object params;

    /** 类路径*/
    private String classPath;

    /** 异常信息 */
    private String exceptionMessage;

    /** 异常追踪信息*/
    private List<String> traceInfo = new ArrayList<>();

    /** 最后一次出现的时间 */
    private LocalDateTime latestShowTime = LocalDateTime.now();
    }

2、异常捕获切面

@Aspect
@RequiredArgsConstructor
public class ExceptionListener {

    private final ExceptionNoticeHandler handler;

    @AfterThrowing(value = "@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller) || @within(com.jincou.core.aop.ExceptionNotice)",
    throwing = "e")
    public void doAfterThrow(JoinPoint joinPoint, Exception e) {
        handler.createNotice(e, joinPoint);
    }
}

3、异常信息通知配置类

@Configuration
@ConditionalOnProperty(prefix = ExceptionNoticeProperties.PREFIX, name = "enable", havingValue = "true")
@EnableConfigurationProperties(value = ExceptionNoticeProperties.class)
public class ExceptionNoticeAutoConfiguration {

    private final RestTemplate restTemplate = new RestTemplate();

    @Autowired(required = false)
    private MailSender mailSender;

    /**
     * 注入 异常处理bean
     */
    @Bean(initMethod = "start")
    public ExceptionNoticeHandler nticeHandler(ExceptionNoticeProperties properties) {
        List<INoticeProcessor> noticeProcessors = new ArrayList<>(2);
        INoticeProcessor noticeProcessor;
        DingTalkProperties dingTalkProperties = properties.getDingTalk();
        if (null != dingTalkProperties) {
            noticeProcessor = new DingTalkNoticeProcessor(restTemplate, dingTalkProperties);
            noticeProcessors.add(noticeProcessor);
        }
        WeChatProperties weChatProperties = properties.getWeChat();
        if (null != weChatProperties) {
            noticeProcessor = new WeChatNoticeProcessor(restTemplate, weChatProperties);
            noticeProcessors.add(noticeProcessor);
        }

        MailProperties email = properties.getMail();
        if (null != email && null != mailSender) {
            noticeProcessor = new MailNoticeProcessor(mailSender, email);
            noticeProcessors.add(noticeProcessor);
        }

        Assert.isTrue(noticeProcessors.size() != 0, "Exception notification configuration is incorrect");
        return new ExceptionNoticeHandler(properties, noticeProcessors);
    }

    /**
     * 注入异常捕获aop
     */
    @Bean
    @ConditionalOnClass(ExceptionNoticeHandler.class)
    public ExceptionListener exceptionListener(ExceptionNoticeHandler nticeHandler) {
        return new ExceptionListener(nticeHandler);
    }
}

4、异常信息推送处理类

@Slf4j
public class ExceptionNoticeHandler {

    private final String SEPARATOR = System.getProperty("line.separator");

    private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

    private final BlockingQueue<ExceptionInfo> exceptionInfoBlockingDeque = new ArrayBlockingQueue<>(1024);

    private final ExceptionNoticeProperties exceptionProperties;

    private final List<INoticeProcessor> noticeProcessors;

    public ExceptionNoticeHandler(ExceptionNoticeProperties exceptionProperties,
                                  List<INoticeProcessor> noticeProcessors) {
        this.exceptionProperties = exceptionProperties;
        this.noticeProcessors = noticeProcessors;
    }

    /**
     * 将捕获到的异常信息封装好之后发送到阻塞队列
     */
    public Boolean createNotice(Exception ex, JoinPoint joinPoint) {

        //校验当前异常是否是需要 排除的需要统计的异常
        if (containsException(ex)) {
            return null;
        }
        log.error("捕获到异常开始发送消息通知:{}method:{}--->", SEPARATOR, joinPoint.getSignature().getName());
        //获取请求参数
        Object parameter = getParameter(joinPoint);
        //获取当前请求对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String address = null;
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            //获取请求地址
            address = request.getRequestURL().toString() + ((request.getQueryString() != null && request.getQueryString().length() > 0) ? "?" + request.getQueryString() : "");
        }

        ExceptionInfo exceptionInfo = new ExceptionInfo(ex, joinPoint.getSignature().getName(), exceptionProperties.getIncludedTracePackage(), parameter, address);
        exceptionInfo.setProject(exceptionProperties.getProjectName());
        return exceptionInfoBlockingDeque.offer(exceptionInfo);
    }

    /**
     * 启动定时任务发送异常通知
     */
    public void start() {
        executor.scheduleAtFixedRate(() -> {
            ExceptionInfo exceptionInfo = exceptionInfoBlockingDeque.poll();
            if (null != exceptionInfo) {
                noticeProcessors.forEach(noticeProcessor -> noticeProcessor.sendNotice(exceptionInfo));
            }
        }, 6, exceptionProperties.getPeriod(), TimeUnit.SECONDS);
    }

    /**
     * 排除的需要统计的异常
     */
    private boolean containsException(Exception exception) {
        Class<? extends Exception> exceptionClass = exception.getClass();
        List<Class<? extends Exception>> list = exceptionProperties.getExcludeExceptions();
        for (Class<? extends Exception> clazz : list) {
            //校验是否存在
            if (clazz.isAssignableFrom(exceptionClass)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 根据方法和传入的参数获取请求参数
     * 注意这里就需要在参数前面加对应的RequestBody 和 RequestParam 注解
     */
    private Object getParameter(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        Parameter[] parameters = method.getParameters();

        Object[] args = joinPoint.getArgs();
        List<Object> argList = new ArrayList<>(parameters.length);
        for (int i = 0; i < parameters.length; i++) {
            RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
            if (requestBody != null) {
                argList.add(args[i]);
            }
            RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
            if (requestParam != null) {
                Map<String, Object> map = new HashMap<>(1);
                String key = parameters[i].getName();
                if (!StringUtils.isEmpty(requestParam.value())) {
                    key = requestParam.value();
                }
                map.put(key, args[i]);
                argList.add(map);
            }
        }
        if (argList.size() == 0) {
            return null;
        } else if (argList.size() == 1) {
            return argList.get(0);
        } else {
            return argList;
        }
    }
}

二、如何配置

1、钉钉配置

第一步:创建钉钉群 并在群中添加自定义机器人

对于不太了解钉钉机器人配置的同学可以参考:钉钉机器人

具体的也可以参考这篇博客: 钉钉机器人SDK 封装预警消息发送工具

第二步:增加配置文件

以下以yml配置文件的配置方式为例

exception:
  notice:
    enable: 启用开关 false或不配置的话本项目不会生效
    projectName: 指定异常信息中的项目名,不填的话默认取 spring.application.name的值
    included-trace-package: 追踪信息的包含的包名,配置之后只通知此包下的异常信息
    period: 异常信息发送的时间周期 以秒为单位 默认值5,异常信息通知并不是立即发送的,默认设置了5s的周期
    exclude-exceptions:
      - 需要排除的异常通知,注意 这里是异常类的全路径,可多选
    ## 钉钉配置
    ding-talk:
      web-hook: 钉钉机器人的webHook地址,可依次点击钉钉软件的头像,机器人管理,选中机器人来查看
      at-mobiles:
        - 钉钉机器人发送通知时 需要@的钉钉用户账户,可多选
      msg-type: 消息文本类型 目前支持 text markdown

2、企业微信配置

第一步:创建企业微信群 并在群中添加自定义机器人

对于不太了解企业微信机器人配置的同学可以参考:企业微信机器人

第二步:增加配置文件

以下以yml配置文件的配置方式为例

exception:
  notice:
    enable: 启用开关 false或不配置的话本项目不会生效
    projectName: 指定异常信息中的项目名,不填的话默认取 spring.application.name的值
    included-trace-package: 追踪信息的包含的包名,配置之后只通知此包下的异常信息
    period: 异常信息发送的时间周期 以秒为单位 默认值5,异常信息通知并不是立即发送的,默认设置了5s的周期
    exclude-exceptions:
      - 需要排除的异常通知,注意 这里是异常类的全路径,可多选
    ## 企业微信配置
    we-chat:
      web-hook: 企业微信webhook地址
      at-phones: 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人 当msg-type=text时才会生效
      at-user-ids: userid的列表,提醒群中的指定成员(@某个成员),@all表示提醒所有人 当msg-type=text时才会生效
      msg-type: 消息格式 企业微信支持 (text)、markdown(markdown)、图片(image)、图文(news)四种消息类型 本项目中有 text和markdown两种可选

3、邮箱配置

这里以qq邮箱为例

第一步:项目中引入邮箱相关依赖

  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-mail</artifactId>
  </dependency>

第二步:增加配置文件

exception:
  notice:
    enable: 启用开关 false或不配置的话本项目不会生效
    projectName: 指定异常信息中的项目名,不填的话默认取 spring.application.name的值
    included-trace-package: 追踪信息的包含的包名,配置之后只通知此包下的异常信息
    period: 异常信息发送的时间周期 以秒为单位 默认值5,异常信息通知并不是立即发送的,默认设置了5s的周期,主要为了防止异常过多通知刷屏
    exclude-exceptions:
      - 需要排除的异常通知,注意 这里是异常类的全路径,可多选
    ## 邮箱配置
    mail:
      from: 发送人地址
      to: 接收人地址
      cc: 抄送人地址
spring:
 mail:
   host: smtp.qq.com  邮箱server地址
   username: [email protected]  server端发送人邮箱地址
   password: 邮箱授权码

邮箱授权码可以按以下方法获取

打开QQ邮箱网页→设置→账户→POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务→开启POP3/SMTP服务,然后就能看到授权码了

注意:钉钉,企业微信和邮箱配置支持单独和同时启用

配置好了配置文件,接下来可以写个例子测试一下了


三、测试

我这里只演示钉钉预警测试,邮箱和微信企业就不再演示了。

接口

@RestController
public class TestController {

    @RequestMapping(value = "/queryUser")
    public void queryUser(@RequestParam("userId") String userId) throws IllegalAccessException {
        throw new IllegalAccessException("监控报警: 用户不存在id=" + userId);
    }
}

配置类

exception:
  notice:
    enable: true
    ## 钉钉配置
    ding-talk:
      web-hook: https://oapi.dingtalk.com/robot/send?access_token=881bada83653fa8af8e08dcd18a9fb403c55b1dbfe5bf239b6f72fdf8e17d5c5
      # 钉钉机器人发送通知时 需要@的钉钉用户账户,可多选
      at-mobiles: 15990000149
      msg-type: text

接口请求

localhost:8084/queryUser?userId=1212

钉钉预警

钉钉机器人实现异常预警通知功能-LMLPHP

git地址 spring-boot-exception-notice

注意: 本工具仅支持集成在springboot+mvc项目中,同时需要jdk版本1.8+



感谢

本项目基本上是基于该项目的,所以非常感谢,不用自己从头造轮子: exception-notice-spring-boot-starter

03-30 22:28