前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >微服务的异常处理

微服务的异常处理

原创
作者头像
3号攻城狮
发布2018-05-27 13:50:32
3.1K3
发布2018-05-27 13:50:32
举报

背景

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

自定义异常消息结构

代码语言:txt
复制
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 + "]";
    }  
}

关键字段解释

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

使用案例

代码语言:txt
复制
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;
    }
}

相关工具方法

代码语言:txt
复制
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;
    }
}

自定义异常

代码语言:txt
复制
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的子类

代码语言:txt
复制
throw new ArgumentException(FundCode.SETTLEMENT_NONE_RECEIPTS.build(billId));

全局异常处理

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

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

代码语言:txt
复制
/**
 * 统一异常处理
 */
@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方法省略

代码语言:txt
复制
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;
}

常量

代码语言:txt
复制
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";
}

异常处理类

代码语言:txt
复制
/**
 * 监控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配置

代码语言:txt
复制
@Configuration
public class RabbitConfig {

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

    @Bean
    public Queue exceptionQue() {
        return new Queue(QUEUE_API_MONITOR);
    }
}
代码语言:txt
复制
@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());
    }
}

异常消息持久化

代码语言:txt
复制
@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);
        }
    }
}

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 自定义异常消息结构
  • 关键字段解释
  • 使用案例
  • 相关工具方法
  • 自定义异常
  • 抛出自定义异常
  • 全局异常处理
  • 处理异常消息
    • 自定义异常收集类 set/get方法省略
      • 常量
        • 异常处理类
          • MQ配置
          • 异常消息持久化
          相关产品与服务
          云数据库 Redis
          腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档