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

#spring-boot

2019-05-26 09:41:51

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

spring boot 拦截全局异常

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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 源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
    }
}
最后更新于