java

spring boot 全局异常拦截 并通过 webhook 发送到 钉钉机器人

当后端程序由于各种原因出错时,如果将错误信息直接抛给客户端,用户肯定会一脸懵逼,除非当时的用户是你们的开发,本文主要讲述如果拦截全局异常,不把异常抛给用户,同时,如果做过客户端,应该知道,Android和ios都有第三方crash上报平台,如果能在出错后发消息到钉钉则很方便,本文主要解决这两件事。

spring boot 拦截全局异常

拦截异常主要使用 @ControllerAdvice 注解,具体来讲就是,新建一个包,比如 crash,然后新建一个 类 比如 GlobalDefaultExceptionHandler,这个类的代码如下:

import com.alibaba.fastjson.JSON;
import io.sentry.Sentry;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import top.kpromise.user.response.Result;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;

@ControllerAdvice
public class GlobalDefaultExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Map<String, Object> defaultExceptionHandler(HttpServletRequest request, Exception e) {
        e.printStackTrace();
        System.out.println("uri " + request.getRequestURI());
        String params = JSON.toJSONString(request.getParameterMap());
        if (params.equals("{}")) {
            params = getRequestJson(request);
        }
        String title = request.getRequestURI();
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String crashTime = df.format(new Date());
        String text = "#### API 请求失败 @13067856109\n" +
                "> url " + request.getRequestURI() + "\n\n" +
                "> 请求参数 \n" + params + "\n\n" +
                "> 堆栈信息 \n" + e.getMessage() + "\n\n" +
                "> ###### 出错时间 " + crashTime + " \n";
        ArrayList<String> phones = new ArrayList<>();
        phones.add("13067856109");
        new DingTalkMessage(new DingTalkMessageBuilder().markdownMessage(title, text).at(phones)).send();
        Sentry.capture(e);
        return Result.data(null, "系统异常,请联系客服");
    }

    private static String getRequestJsonString(HttpServletRequest request)
            throws Exception {
        String submitMethod = request.getMethod().toUpperCase();
        if (submitMethod.equals("GET")) {
            return new String(request.getQueryString().getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)
                    .replaceAll("%22", "\"");
        } else {
            return getRequestPostStr(request);
        }
    }

    private static String getRequestJson(HttpServletRequest request) {
        try {
            return getRequestJsonString(request);
        } catch (Exception e) {
            return null;
        }
    }

    private static byte[] getRequestPostBytes(HttpServletRequest request)
            throws IOException {
        int contentLength = request.getContentLength();
        if (contentLength < 0) {
            return null;
        }
        byte[] buffer = new byte[contentLength];
        for (int i = 0; i < contentLength; ) {
            int readLen = request.getInputStream().read(buffer, i, contentLength - i);
            if (readLen == -1) {
                break;
            }
            i += readLen;
        }
        return buffer;
    }

    private static String getRequestPostStr(HttpServletRequest request)
            throws Exception {
        byte[] buffer = getRequestPostBytes(request);
        String charEncoding = request.getCharacterEncoding();
        if (charEncoding == null) {
            charEncoding = "UTF-8";
        }
        if (buffer == null) {
            return "{}";
        }
        return new String(buffer, charEncoding);
    }
}

这里,我获取了用户请求的 url,以及参数,然后调用了 钉钉 机器人,总结起来,要拦截全局异常,只需要: 新增一个类,这个类添加 @ControllerAdvice 注解 然后这个类里写一个处理异常的方法,这个方法添加 @ExceptionHandler(Exception.class) 和 @ResponseBody 注解,前者的意思是,这个方法可以处理所有的 Exception ,后者的意思是 重写 返回的数据(没错,这么理解更简单),这就够了。但是这里,我增加了获取用户上传的 json 字符串以及 发送到 钉钉机器人,而且还发送到了自建的sentry 上面。

钉钉机器人文档可参考:https://open-doc.dingtalk.com/microapp/serverapi2/qf2nxq

DingTalkMessage 源码如下:

import okhttp3.*;

import java.io.IOException;

public class DingTalkMessage {

    private static final String URL = "https://oapi.dingtalk.com/robot/send?access_token=" +
            "2ce54522b84e1c7193e9998e9b9a2afd859aa7e635354d55a91cc6xxxxxx";

    private DingTalkMessageBuilder builder;

    public DingTalkMessage(DingTalkMessageBuilder builder) {
        this.builder = builder;
    }

    public void send() {
        String json = builder.build();

        OkHttpClient okHttpClient = new OkHttpClient();
        RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);
        Request request = new Request.Builder().post(body).url(URL).build();

        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                e.getMessage();
            }

            @Override
            public void onResponse(Call call, okhttp3.Response response) {
                try {
                    ResponseBody responseBody = response.body();
                    if (responseBody != null) {
                        System.out.println(responseBody.string());
                    }
                }catch (Exception ignore){

                }
            }
        });
    }
}

DingTalkMessageBuilder 源码如下:

import com.alibaba.fastjson.JSON;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class DingTalkMessageBuilder {

    private final HashMap<String, Object> map;

    public DingTalkMessageBuilder() {
        map = new HashMap<>();
    }

    public DingTalkMessageBuilder markdownMessage(String title, String text) {
        map.put("msgtype", "markdown");
        Map<String, String> contentMap = new HashMap<>();
        contentMap.put("title", title);
        contentMap.put("text", text);
        map.put("markdown", contentMap);
        return this;
    }

    public DingTalkMessageBuilder at(ArrayList<String> phones) {
        Map<String, Object> at = new HashMap<>();
        at.put("atMobiles", phones);
        at.put("isAtAll", false);
        map.put("at", at);
        return this;
    }

    String build() {
        return JSON.toJSONString(map);
    }
}

具体代码请点击下面,如果觉得有用请给个 star 谢谢。

full-stack-trip

Share
Published by
full-stack-trip
Tags: spring-boot

Recent Posts

Android 自定义 View 入门

说来惭愧,工作数年,连基本的自…

4 年 ago

retrofit 同时支持 xml 和 json

retrofit 解析 jso…

4 年 ago

mysql - 存储过程 从入门到放弃

最近有个报表的需求,于是乎用了…

4 年 ago

奶嘴战略 - 你不得不知道的扎心真相(一)

一句:英雄枯骨无人问,戏子家事…

4 年 ago

acme.sh 的简单使用

acme.sh 是纯 shel…

4 年 ago

wrk -更现代化的http压测工具

wrk 是一款更现代化的 ht…

4 年 ago