微服务的异常处理

背景

不加班的周末,整理了一下项目上的异常处理方案,和小伙伴们共享,里面不成熟的代码或解决方式.QAQ,评论区走起

自定义异常消息结构

public final class Code {
    private String code;
    private String msg;    
    private Code(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public static Code create(String code, String msg) {
        return new Code(code, msg);
    }
    public String getCode() {
        return this.code;
    }
    public String getMsg() {
        return this.msg;
    }
    public String getString() {
        return "[" + this.code + "][" + this.msg + "]";
    }  
}

关键字段解释

code:异常编码,与数据中持久化的消息模版对应编码一致
msg:如果消息模版中没有维护可以手工输入消息模版

使用案例

public enum FundCode {
    
    /*
    * 该方式适用于将消息模版维护在db(数据库/redis)
    * */
    SETTLEMENT_INVALID_RECEIPT("FUND1200"), // 回款[%s]尚未确认或已处理完成,处理失败
    SETTLEMENT_NONE_RECEIPTS("FUND1201"), // 没有可用的回款,处理失败
    ;
    /*
    * 该方式适用于零时使用,不够灵活
    * */
    /*SETTLEMENT_INVALID_RECEIPT("FUND1200","回款[%s]尚未确认或已处理完成,处理失败")
    SETTLEMENT_NONE_RECEIPTS("FUND1201","没有可用的回款,处理失败")*/
    private String code;
    private String msg;
    FundCode(String code) {
        this.code = code;
    }
    public Code build(Object... args) {
        //通过 code从redis中读取消息模版
        this.msg = CacheUtil.getMessage(this.code);
        if (args.length > 0) {
            this.msg = CodeUtil.format(this.msg, args);
        }
        return Code.create(this.code, this.msg);
    }
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
}

相关工具方法

public class CodeUtil {
    public CodeUtil() {
    }
    public static final String format(String context, Object... args) {
        if(args.length > 0) {
            try {
                context = String.format(context, args);
            } catch (MissingFormatArgumentException var3) {
                var3.printStackTrace();
            } catch (Exception var4) {
                var4.printStackTrace();
            }
        }
        return context;
    }
}

自定义异常

public class BaseException extends RuntimeException {
    protected Code code;
    public BaseException() {
    }
    public BaseException(Code code) {
        super(code.toString());
        this.code = code;
    }
    public BaseException(Code code, Throwable throwable) {
        super(code.toString(), throwable);
        this.code = code;
    }
    public BaseException(String message) {
        super(message);
    }
    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }
    public String getCode() {
        return this.code != null?this.code.getCode():"";
    }
    public String getDebugMsg() {
        return this.code != null?this.code.getDebugMsg():"";
    }
    public String getMsg() {
        return this.code != null?this.code.getMsg():"";
    }
    public Object getData() {
        return this.code != null?this.code.getData():null;
    }
    public String getString() {
        return "[" + this.getCode() + "] " + this.getMsg();
    }

抛出自定义异常

ArgumentException为继承BaseException的子类

throw new ArgumentException(FundCode.SETTLEMENT_NONE_RECEIPTS.build(billId));

全局异常处理

此处可以拦截各种类型的异常,但是要注意拦截的顺序,按照基础Exception的顺序,越是后面的异常拦截要靠前,

我们将拦截到的异常消息封装,然后统一在api-gateway中解析处理.

/**
 * 统一异常处理
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    private static final int CUSTOM_ERROR_HTTP_STATUS = 403;            // 自定义异常发生时HTTP状态码
    private static final int SERVER_INTERNAL_ERROR_HTTP_STATUS = 500;   // 服务器内部异常

    /**
     * 自定义异常
     * @param be
     * @param response
     * @return
     */
    @ExceptionHandler(BaseException.class)
    @ResponseBody
    public Map baseExceptionHandler(BaseException be, HttpServletResponse response) {
        response.setStatus(CUSTOM_ERROR_HTTP_STATUS);
        logger.warn("Expected exception occurred", be);
        Map<String, Object> data = new HashMap();
        data.put("code", be.getCode());
        data.put("message", be.getMsg());
        data.put("debugMsg", be.getDebugMsg());
        data.put("data", be.getData());
        data.put("exception", ExceptionUtil.getStackTrace(be));
        return data;
    }

    /**
     * 非自定义异常
     *
     * @param e
     * @param response
     * @return
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Map exceptionHandler(Exception e, HttpServletResponse response) {
        logger.error("Unexpected exception", e);

        response.setStatus(SERVER_INTERNAL_ERROR_HTTP_STATUS);
        Map<String, Object> data = new HashMap();
        data.put("code", "500");
        data.put("message", "系统异常,请稍后再试(我们已通知平台管理员)");
        data.put("data", "");
        data.put("exception", ExceptionUtil.getStackTrace(e));
        return data;
    }
}

处理异常消息

自定义异常收集类 set/get方法省略

public class SysApiAudit{
    private String persistType;
    private String direction;
    private String clientId;
    private Long requestTime;
    private Long responseTime;
    private Long costTime;
    private String requestMethod;
    private String requestContentType;
    private String requestUrl;
    private String requestUri;
    private String requestQueryParams;
    private String requestBody;
    private String remoteAddress;
    private String characterEncoding;
    private Integer responseStatus;
    private String responseContentType;
    private String responseBody;
    private String customRespCode;
    private String stacktrace;
    private String instanceId;
    private String ouOrgId;
    private String accessToken;
    private String userName;
    private String userRoleCode;
    private String userLastName;
    private String debugMsg;
    private String userAgent;
}

常量

public interface GatewayConstant {

    String ORIGIN_KEY = "Access-Control-Allow-Origin";
    String ORIGIN_VALUE = "*";
    String ALLOW_METHODS_KEY = "Access-Control-Allow-Methods";
    String ALLOW_METHODS_VALUE = "POST, GET, OPTIONS, PUT, DELETE";
    String MAX_AGE_KEY = "Access-Control-Max-Age";
    String MAX_AGE_VALUE = "3600";
    String ALLOW_HEADERS_KEY = "Access-Control-Allow-Headers";
    String ALLOW_HEADERS_VALUE = "x-requested-with";
    String CHARACTER_ENCODE = "UTF-8";
    String CONTENT_TYPE = "application/json; charset=utf-8";
    String CONTENT_TYPE_JSON_UTF8 = "application/json; charset=utf-8";
    String CONTENT_TYPE_JSON = "application/json";
    int ERROR_STATUS = 500;

    String CUSTOMER_EXCEPTION_FILED_CODE = "code";
    String CUSTOMER_EXCEPTION_FILED_MESSAGE = "message";
    String CUSTOMER_EXCEPTION_FILED_TIMESTAMP = "timestamp";
    String CUSTOMER_EXCEPTION_FILED_STATUS = "status";
    String CUSTOMER_EXCEPTION_FILED_ERROR = "error";
    String CUSTOMER_EXCEPTION_FILED_EXCEPTION = "exception";

    // 请求信息, 作为 HttpServletRequest中attribute的Key值
    String C_REQUEST_UID = "C_REQUEST_UID"; // 请求唯一ID
    String C_REQUEST_START_TIME = "C_REQUEST_START_TIME"; // 请求入站时间
    String C_REQUEST_END_TIME = "C_REQUEST_START_TIME"; // 请求处理结束时间
    String C_IS_DO_API_AUDIT = "C_IS_DO_DATA_AUDIT"; // 是否做API数据审计
    String C_REQUEST_USER_INFO = "C_REQUEST_USER_INFO"; // 请求中用户信息

    String C_FILED_EXCEPTION = "exception";
    String C_FILED_INSTANCE_ID = "instanceId";
    String C_FILED_DEBUG_MSG = "debugMsg";

    String API_AUDIT_DIRECTION_INTO  ="INTO";

    String UTF8 = "utf8";
}

异常处理类

/**
 * 监控Filter
 * 异常监控: 记录参数和返回数据, MQ预警
 * 超时监控: 记录请求数据
 * API监控: 监控指定API出入站数据
 */
@Component
public class MonitorFilter extends ZuulFilter {
    private static final int FATAL_EXP_STATUS_CODE = 500;  // 内部严重异常对应的HTTP状态码
    private static final int CUSTOM_EXP_STATUS_CODE = 403; // 自定义异常(可控异常)对应的HTTP状态码
    private static final int MAX_STACKTRACE_LENGTH = 3000;  // stacktrace最大截取长度
    private static final String UTF8 = "utf8";
    private static final Logger logger = LoggerFactory.getLogger(MonitorFilter.class);
    @Value("${hcloud.gateway.performanceThreshold:3000}")
    private long performanceThreshold; // 判定API出现性能问题的COST, 默认3s
    @Autowired
    private RabbitSender rabbitSender;
    @Override
    public String filterType() {
        return POST_TYPE;
    }
    @Override
    public int filterOrder() {
        return 0;
    }
    @Override
    public boolean shouldFilter() {
        return true;
    }
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        int responseStatusCode = ctx.getResponseStatusCode();
        String persistType = getPersistType(ctx); // 持久化类型
        boolean isPersistData = (persistType != null);
        boolean isExceptionOccurred = false;
        if (isPersistData &&
                (PersistType.FATAL_ERROR.equals(persistType) || PersistType.CUSTOM_ERROR.equals(persistType))) {
            isExceptionOccurred = true;
        }

        if (isPersistData) {
            SysApiAudit apiAudit = new SysApiAudit();
            apiAudit.setPersistType(persistType);
            apiAudit.setDirection(GatewayConstant.API_AUDIT_DIRECTION_INTO);
            apiAudit.setUserAgent(request.getHeader("user-agent"));
            fillApiAuditRequestInfo(apiAudit, ctx);
            fillApiAuditResponseInfo(apiAudit, ctx);
            try {
                String responseContentType = getResponseContentType(ctx);
                if (!StringUtil.isEmpty(responseContentType) &&
                        responseContentType.contains(GatewayConstant.CONTENT_TYPE_JSON)) {

                    InputStream responseStream = ctx.getResponseDataStream();
                    String responseFullBody = IOUtils.toString(responseStream, UTF8);
                    String responseRealBody; // 需剔除exception stacktrace
                    JSONObject responseJson = JSON.parseObject(responseFullBody);
                    if (isExceptionOccurred) {
                        String stacktrace = responseJson.getString(GatewayConstant.C_FILED_EXCEPTION);
                        String instanceId = responseJson.getString(GatewayConstant.C_FILED_INSTANCE_ID);
                        if (!StringUtil.isEmpty(stacktrace) && stacktrace.length() > MAX_STACKTRACE_LENGTH) {
                            stacktrace = stacktrace.substring(0, MAX_STACKTRACE_LENGTH);
                        }
                        try{
                            apiAudit.setDebugMsg(responseJson.get(GatewayConstant.C_FILED_DEBUG_MSG).toString());
                            responseJson.remove(GatewayConstant.C_FILED_DEBUG_MSG);
                        } catch (Exception e) {
                        }
                        responseJson.remove(GatewayConstant.C_FILED_EXCEPTION); // 删除异常信息
                        responseJson.remove(GatewayConstant.C_FILED_INSTANCE_ID); // 删除实例ID
                        responseRealBody = responseJson.toJSONString();
                        apiAudit.setInstanceId(instanceId);
                        apiAudit.setStacktrace(stacktrace);
                    } else {
                        responseRealBody = responseFullBody;
                    }

                    apiAudit.setCustomRespCode(responseJson.getString(GatewayConstant.CUSTOMER_EXCEPTION_FILED_CODE));
                    apiAudit.setResponseBody(responseRealBody);

                    // responseDataStream为IO流,仅能读取一次,需要重新将数据写入
                    ctx.setResponseDataStream(new ByteArrayInputStream(responseRealBody.getBytes(UTF8)));
                }
                apiAudit.setResponseContentType(responseContentType);
            } catch (IOException e) {
                logger.error("Parse response body failed, request uid:{}",
                        request.getAttribute(GatewayConstant.C_REQUEST_UID));
            }

            if (isExceptionOccurred) {
                logger.warn("apiAudit:{}", apiAudit);
            }
            rabbitSender.handleApiMonitorData(apiAudit);
        }

        // 将自定义异常的response标记为200
        if (responseStatusCode == CUSTOM_EXP_STATUS_CODE) {
            ctx.setResponseStatusCode(200);
        }
        return null;
    }

    /**
     * 获取API数据持久化类型, 如果不需要持久化将返回空
     *
     * @return
     */
    private String getPersistType(RequestContext ctx) {
        String persistType = null; // 持久化类型

        HttpServletRequest request = ctx.getRequest();
        int responseStatusCode = ctx.getResponseStatusCode();
        long requestEndTime = new Date().getTime();

        // 第一优先级: 异常持久化
        if (responseStatusCode == CUSTOM_EXP_STATUS_CODE) {
            persistType = PersistType.CUSTOM_ERROR;

        } else if (responseStatusCode == FATAL_EXP_STATUS_CODE) {
            persistType = PersistType.FATAL_ERROR;
        }

        // 第二优先级: 出现性能问题进行持久化
        long startTime = Long.parseLong(request.getAttribute(GatewayConstant.C_REQUEST_START_TIME).toString());
        long cost = requestEndTime - startTime;
        if (persistType == null && cost > performanceThreshold) {
            persistType = PersistType.PERFORMANCE_ISSUE;
        }

        // 低优先级: API需要进行数据监控
        boolean isDoApiAudit = request.getAttribute(GatewayConstant.C_IS_DO_API_AUDIT) != null;
        if (persistType == null && isDoApiAudit) {
            persistType = PersistType.API_AUDIT;
        }

        return persistType;
    }

    private void fillApiAuditResponseInfo(SysApiAudit apiAudit, RequestContext ctx) {
        HttpServletRequest request = ctx.getRequest();
        long startTime = Long.parseLong(request.getAttribute(GatewayConstant.C_REQUEST_START_TIME).toString());
        long requestEndTime = new Date().getTime();
        long cost = requestEndTime - startTime;
        apiAudit.setCostTime(cost);
        apiAudit.setRequestTime(startTime);
        apiAudit.setResponseTime(requestEndTime);
        apiAudit.setResponseStatus(ctx.getResponseStatusCode());
    }

    private void fillApiAuditRequestInfo(SysApiAudit apiAudit, RequestContext ctx) {
        HttpServletRequest request = ctx.getRequest();
        Map queryParams = ctx.getRequestQueryParams();
        String bodyParams = getRequestBody(request);
        if (queryParams != null && !queryParams.isEmpty())
            apiAudit.setRequestQueryParams(JSON.toJSONString(queryParams));
        apiAudit.setRequestBody(bodyParams);
        apiAudit.setRequestContentType(request.getContentType());
        apiAudit.setRequestUrl(request.getRequestURL().toString());
        apiAudit.setRequestUri(request.getRequestURI());
        apiAudit.setRemoteAddress(request.getRemoteAddr());
        apiAudit.setCharacterEncoding(request.getCharacterEncoding());
        apiAudit.setRequestMethod(request.getMethod());

        Object userDetailsObj = request.getAttribute(GatewayConstant.C_REQUEST_USER_INFO);
        if (userDetailsObj != null && userDetailsObj instanceof CustomUserDetails) {
            CustomUserDetails userDetails = (CustomUserDetails) userDetailsObj;
            apiAudit.setClientId(userDetails.getClientId());
            apiAudit.setAccessToken(userDetails.getAccessToken());
            apiAudit.setUserName(userDetails.getUserName());
            apiAudit.setUserLastName(userDetails.getLastName());
            apiAudit.setUserRoleCode(userDetails.getUserRoleCode());
            apiAudit.setOuOrgId(userDetails.getOuOrgId());
        }
    }

    private String getRequestBody(HttpServletRequest request) {
        String method = request.getMethod();
        String bodyParams = null;
        if (method.equals(HttpMethod.POST.toString()) || method.equals(HttpMethod.PUT.toString())) {
            String contentType = request.getContentType();
            if (!StringUtil.isEmpty(contentType) &&
                    (contentType.equals(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ||
                            contentType.contains(GatewayConstant.CONTENT_TYPE_JSON))) {
                try {
                    bodyParams = IOUtils.toString(request.getInputStream(), UTF8);
                } catch (IOException e) {
                    logger.error("Parse request body failed, request uid:{}",
                            request.getAttribute(GatewayConstant.C_REQUEST_UID));
                }
            }
        }
        return bodyParams;
    }

    /**
     * 获取Response Content-Type, 当前Filter在SendResponseFilter之前执行
     * 但是Response的Headers在SendResponseFilter中才会组装
     * 本方法使用SendResponseFilter中初始化Response Headers方式获取contentType
     *
     * @param context
     * @return
     * @see org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
     */
    private String getResponseContentType(RequestContext context) {
        String contentType = null;
        List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
        if (zuulResponseHeaders != null) {
            for (Pair<String, String> it : zuulResponseHeaders) {
                if ("Content-Type".equals(it.first())) {
                    contentType = it.second();
                }
            }
        }
        return contentType;
    }
}

MQ配置

@Configuration
public class RabbitConfig {

    public static final String QUEUE_API_MONITOR = "queue.gateway.apiMonitor";

    @Bean
    public Queue exceptionQue() {
        return new Queue(QUEUE_API_MONITOR);
    }
}
@Component
public class RabbitSender {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public void handleApiMonitorData(SysApiAudit apiAudit) {
     String data = new JSONWriter().write(apiAudit);
     this.rabbitTemplate.convertAndSend(RabbitConfig.QUEUE_API_MONITOR, data.getBytes());
    }
}

异常消息持久化

@Component
public class RabbitReceivers {

    private Logger logger = LoggerFactory.getLogger(RabbitReceivers.class);

    @Value("${spring.profiles:none}")
    private String profile;

    private List<String> ignoreAlarmEnvs = Arrays.asList("default", "dev", "uat"); // 忽略预警的环境

    @Autowired
    private SysApiAuditMapper apiAuditMapper;

    @RabbitListener(queues = "queue.gateway.apiMonitor")
    @RabbitHandler
    public void handleApiMonitor(byte[] data, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException{
        JSONObject jsonData = null;
        try {
            jsonData = JSON.parseObject(new String(data));
            SysApiAudit apiAudit = (SysApiAudit)BeanParser.parse(jsonData, SysApiAudit.class);

            String userAgent = apiAudit.getUserAgent();
            UASparser uasParser = new UASparser(OnlineUpdater.getVendoredInputStream());
            UserAgentInfo userAgentInfo = uasParser.parse(userAgent);

            apiAudit.setOsFamily(userAgentInfo.getOsFamily());
            apiAudit.setOsVersion(userAgentInfo.getOsName());
            apiAudit.setUaFamily(userAgentInfo.getUaFamily());
            apiAudit.setBrowserVersion(userAgentInfo.getBrowserVersionInfo());
            apiAudit.setDevice(userAgentInfo.getDeviceIcon());

            String responseBody = apiAudit.getResponseBody();
            if (responseBody != null && responseBody.length() > 3000) {
                apiAudit.setResponseBody(responseBody.substring(1, 2900));
            }

            String stacktrace = apiAudit.getStacktrace();
            if (stacktrace != null && stacktrace.length() > 3000) {
                apiAudit.setStacktrace(stacktrace.substring(1, 2900));
            }

            apiAuditMapper.insert(apiAudit);

            if (!ignoreAlarmEnvs.contains(profile)) {
                // mail alert
                if ("FATAL_ERROR".equals(apiAudit.getPersistType())) {
                    JSONObject ex = new JSONObject();
                    ex.put("PERSIST_TYPE", apiAudit.getPersistType());
                    ex.put("URL", apiAudit.getRequestUrl());
                    ex.put("USER", apiAudit.getUserName() + " " + apiAudit.getUserLastName());
                    ex.put("REQUEST_DATE", DateUtil.date2Str24H(new Date(apiAudit.getRequestTime())));
                    ex.put("REQUEST_CONTENT_TYPE", apiAudit.getRequestContentType());
                    ex.put("REQUEST_METHOD", apiAudit.getRequestMethod());
                    ex.put("PARAMS", apiAudit.getRequestBody());
                    ex.put("QUERY_PARAMS", apiAudit.getRequestQueryParams());
                    ex.put("RESP_CODE", apiAudit.getCustomRespCode());
                    ex.put("RESP_BODY", apiAudit.getResponseBody());
                    ex.put("STACKTRACE", apiAudit.getStacktrace());
                    ex.put("TOKEN", apiAudit.getAccessToken());
                    ex.put("OU_ORG_ID", apiAudit.getOuOrgId());
                    ex.put("DEBUG_MSG", apiAudit.getDebugMsg());

                    JSONObject subjectTokens = new JSONObject();
                    subjectTokens.put("USER", apiAudit.getUserLastName());
                    subjectTokens.put("ENV", profile);
                    MailClient.build()
                            .templateCode("SYSEXCEPTIONAPIAUDIT")
                            .subjectToken(subjectTokens.toJSONString())
                            .bodyToken(ex.toJSONString()).send();
                }
                // todo 非工作时间对于FATAL_ERROR需要进行短信预警
            }
        } catch (Exception e) {
            logger.error("Handle api monitor data failed, data:{}", jsonData);
        } finally {
            channel.basicAck(deliveryTag, false);
        }
    }
}

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android 研究

Retrofit解析7之相关类解析

上篇文章讲解了Call接口、CallAdapter接口、Callback接口、Converter接口、Platform类、ExecutorCallAdapter...

1251
来自专栏androidBlog

Android Hook 机制之简单实战

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gdutxiaoxu/article/de...

5491
来自专栏Hongten

python开发_pprint()

1094
来自专栏Java成神之路

Spring_总结_04_高级配置(二)之条件注解@Conditional

在上一节,我们了解到 Profile 为不同环境下使用不同的配置提供了支持,那么Profile到底是如何实现的呢?其实Profile正是通过条件注解来实现的。

903
来自专栏java技术学习之道

事物在Controller层的探索

1123
来自专栏码匠的流水账

聊聊HystrixCircuitBreaker

hystrix-core-1.5.12-sources.jar!/com/netflix/hystrix/HystrixCircuitBreaker.java

751
来自专栏菩提树下的杨过

java调用.net asmx / wcf

一、先用asmx与wcf写二个.net web service: 1.1 asmx web服务:asmx-service.asmx.cs 1 using Sy...

2295
来自专栏闻道于事

深入分析Spring Boot2,解决 java.lang.ArrayStoreException异常

将某个项目从Spring Boot1升级Spring Boot2之后出现如下报错,查了很多不同的解决方法都没有解决:

8712
来自专栏猿天地

高性能NIO框架Netty-对象传输

上篇文章高性能NIO框架Netty入门篇我们对Netty做了一个简单的介绍,并且写了一个入门的Demo,客户端往服务端发送一个字符串的消息,服务端回复一个字符串...

3108
来自专栏Golang语言社区

Golang Stub初体验

序言 对于领域对象的UT测试来说,基础设施层(infra)的操作函数都应该被打桩。对于Golang来说,大家通常会想到GoMock。GoMock是由Golang...

4179

扫码关注云+社区

领取腾讯云代金券