異常處理與開發(fā)體驗和用戶體驗

1.接口調(diào)用

理想情況來說,客戶端想要的是目標接口成功響應(yīng)的結(jié)果。比如查詢用戶信息,服務(wù)器只需要返回我想要的用戶信息給我就可以了。類似:

{
  "name":"zouwei",
  "age":26,
  ......
}

當然,以上也只是停留在理想上。正常接口大多數(shù)情況下,會正常響應(yīng)給出用戶信息。但是在非正常情況下呢,比如訪問量劇增,服務(wù)器響應(yīng)不過來了;或者因為用戶的查詢參數(shù)不合理,導(dǎo)致查詢不出任何結(jié)果;亦或者程序的代碼不夠健壯,在某種情況下才會報錯;此時,有一些用戶就不能順利地獲取用戶信息,那么他們得到的響應(yīng)數(shù)據(jù)是什么呢?頁面的表現(xiàn)形式是怎樣的呢?假如是編碼問題,如何才能讓開發(fā)人員快速的定位到報錯位置呢?

2.統(tǒng)一響應(yīng)數(shù)據(jù)結(jié)構(gòu)

為了避免程序沒有給出正確的響應(yīng)數(shù)據(jù)導(dǎo)致客戶端不知道如何與用戶交互,我們需要協(xié)商出一個統(tǒng)一的響應(yīng)數(shù)據(jù)結(jié)構(gòu)。也就是說,我們需要為異常情況考慮一個合理的交互方式,讓用戶知道,我們的服務(wù),只是暫時有一些問題(ps:這里的問題不僅僅是指服務(wù)器的問題,還包括用戶操作正確性的問題)。也能讓開發(fā)人員通過響應(yīng)數(shù)據(jù)了解到程序的出錯信息,包括出錯位置,異常類型,甚至包括修正方式。

成功示例

{
  "message":"處理成功",
  "data":{
    "name":"zouwei",
    "age":26
  }
}

失敗示例

{
  "message":"服務(wù)擁堵,請稍后再試!",
  "data":null
}

這樣的數(shù)據(jù)結(jié)構(gòu)設(shè)計貌似能讓用戶感覺到我們滿滿的誠意,客戶端開發(fā)人員可以在data為空的時候,把“message”的數(shù)據(jù)展示給用戶,告知用戶,我們的服務(wù)當前的一個狀態(tài),并指示用戶正確的操作方式。

可是細想一下,對于客戶端開發(fā)人員來說,data為null的時候一定就是異常的響應(yīng)嘛?這樣的判斷顯然過于武斷。比如某些添加,或者修改的接口,data完全沒有必要返回任何數(shù)據(jù)。所以我們還需要給出某個標識讓客戶端知道我們確實是處理成功了。

成功示例

{
  //錯誤碼
  "code":0,
  "message":"處理成功",
  "data":{
    "name":"zouwei",
    "age":26
  }
}

失敗示例

{
  //錯誤碼
  "code":10001,
  "message":"輸入的手機號碼還未注冊!",
  "data":null
}

現(xiàn)在,客戶端開發(fā)人員可以根據(jù)協(xié)商好的錯誤碼區(qū)分當前的請求是否被成功處理,而不是通過判斷data=null來確定是否成功,避免潛在的編碼風(fēng)險,提高開發(fā)體驗度。

假如服務(wù)器代碼已經(jīng)非常健壯的話,上面的數(shù)據(jù)結(jié)構(gòu)是完全沒有問題的,可是還是過于理論。因為在實際場景中,沒有人能保證代碼沒有任何潛在的問題,這類問題就不屬于用戶造成的問題了,而是在編碼過程中因為各種原則造成的紕漏或者不夠健壯,這類問題是可以避免的。比如常見的NullPointerException,NumberFormatException等,這類異常一旦發(fā)生,響應(yīng)數(shù)據(jù)應(yīng)該怎么定義,因為這一類異常不需要事先聲明,所以不能準確地對這一類異常定性,那么可以做一個默認的code,歸類為“未知錯誤”。為了能讓開發(fā)人員在開發(fā)階段能盡快地定位到異常類型和位置,可以考慮添加一個字斷展示異常堆棧。

示例

{
  //錯誤碼
  "code":-1,
  //異常堆棧,只有在開發(fā)和測試環(huán)境打開
  "error":"java.lang.NullPointerException",
  "message":"未知錯誤!",
  "data":null
}

(ps:堆棧信息字段只是簡單表示,實際情況會包含異常位置,異常詳細信息等)

上述的error字段僅僅在開發(fā)和測試階段出現(xiàn),線上環(huán)境需要去掉??赏ㄟ^配置化的方式實現(xiàn)這個功能。

3.java服務(wù)端實現(xiàn)

依賴

ext {//依賴版本
        springBootVersion = "2.2.2.RELEASE"
        lombokVersion = "1.18.10"
        guavaVersion = "28.1-jre"
        commonsLangversion = "3.9"
    }

    dependencies {
      annotationProcessor("org.projectlombok:lombok:$lombokVersion")
        compileOnly("org.projectlombok:lombok:$lombokVersion")
        compile("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
        compile("org.springframework.boot:spring-boot-configuration-processor:$springBootVersion")
        compile("org.apache.commons:commons-lang3:$commonsLangversion")
        compile("com.google.guava:guava:$guavaVersion")
        compile("org.yaml:snakeyaml:1.25")
        compile("org.hibernate.validator:hibernate-validator:6.1.0.Final")
    }

統(tǒng)一的響應(yīng)數(shù)據(jù)結(jié)構(gòu)

import com.zx.eagle.common.exception.EagleException;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;

import java.util.List;

/** @author zouwei */
@Data
public class CommonResponse<T> {

    /** 成功CODE */
    private static final String DEFAULT_SUCCESS_CODE = "0";
    /** 成功MESSAGE */
    private static final String DEFAULT_SUCCESS_MSG = "SUCCESS";
    /** 響應(yīng)碼 */
    private String code;
    /** 異常信息 */
    private String error;
    /** 用戶提示 */
    private String message;
    /** 參數(shù)驗證錯誤 */
    private List<EagleException.ValidMessage> validMessage;
    /** 響應(yīng)數(shù)據(jù) */
    private T data;

    private CommonResponse() {}

    private CommonResponse(String code, String error, String message) {
        this();
        this.code = code;
        this.message = message;
        this.error = error;
    }

    /**
     * 成功響應(yīng)
     *
     * @param data
     */
    private CommonResponse(T data) {
        this(DEFAULT_SUCCESS_CODE, StringUtils.EMPTY, DEFAULT_SUCCESS_MSG);
        this.data = data;
    }

    /** @param e */
    private CommonResponse(EagleException e, String error) {
        this();
        this.code = e.getCode();
        this.message = e.getTips();
        this.error = error;
        this.validMessage = e.getValidMessages();
    }
    /**
     * 用戶行為導(dǎo)致的錯誤
     *
     * @param code
     * @param error
     * @param message
     * @param <T>
     * @return
     */
    public static <T> CommonResponse<T> exceptionInstance(
            String code, String error, String message) {
        return new CommonResponse<>(code, error, message);
    }

    /**
     * 正常響應(yīng)
     *
     * @param data
     * @param <T>
     * @return
     */
    public static <T> CommonResponse<T> successInstance(T data) {
        return new CommonResponse<>(data);
    }

    /**
     * 正常響應(yīng)
     *
     * @param <T>
     * @return
     */
    public static <T> CommonResponse<T> successInstance() {
        return (CommonResponse<T>) successInstance(StringUtils.EMPTY);
    }

    /**
     * 用戶行為導(dǎo)致的錯誤
     *
     * @param e
     * @param <T>
     * @return
     */
    public static <T> CommonResponse<T> exceptionInstance(EagleException e, String error) {
        return new CommonResponse<>(e, error);
    }
}

統(tǒng)一異常類型

import com.zx.eagle.common.cache.ExceptionTipsCache;
import lombok.Data;

import java.util.List;
import java.util.Objects;

/** @author zouwei */
@Data
public class EagleException extends Exception {

    /** 參數(shù)驗證異常 */
    private static final String VALID_ERROR = "VALID_ERROR";
    /** 默認的未知錯誤 */
    private static final String DEFAULT_TPIPS_KEY = "UNKNOWN_ERROR";

    /** 錯誤碼 */
    private String code;

    /** 用戶提示Key */
    private String tipsKey;

    /** 用戶提示 */
    private String tips;

    /** 驗證異常提示 */
    private List<ValidMessage> validMessages;

    private EagleException(String message) {
        super(message);
    }

    private EagleException(String code, String tipsKey, String tips, String message) {
        this(message);
        this.code = code;
        this.tipsKey = tipsKey;
        this.tips = tips;
    }

    /**
     * 創(chuàng)建異常
     *
     * @param tipsKey
     * @param message
     * @return
     */
    public static EagleException newInstance(String tipsKey, String message) {
        ExceptionTipsCache.ExceptionTips tips = ExceptionTipsCache.get(tipsKey);
        return new EagleException(tips.getCode(), tipsKey, tips.getTips(), message);
    }

    /**
     * 未知異常
     *
     * @param message
     * @return
     */
    public static EagleException unknownException(String message) {
        return newInstance(DEFAULT_TPIPS_KEY, message);
    }

    /**
     * 參數(shù)驗證錯誤
     *
     * @param validMessages
     * @return
     */
    public static EagleException validException(List<ValidMessage> validMessages) {
        ExceptionTipsCache.ExceptionTips tips = ExceptionTipsCache.get(VALID_ERROR);
        final String validCode = tips.getCode();
        EagleException eagleException =
                new EagleException(validCode, VALID_ERROR, tips.getTips(), tips.getTips());
        validMessages.forEach(
                msg -> {
                    ExceptionTipsCache.ExceptionTips tmpTips = null;
                    try {
                        tmpTips = ExceptionTipsCache.get(msg.getTipsKey());
                    } catch (Exception e) {
                        msg.setTips(msg.getDefaultMessage());
                        // 參數(shù)驗證錯誤
                        msg.setCode(validCode);
                    }
                    if (Objects.nonNull(tmpTips)) {
                        msg.setTips(tmpTips.getTips());
                        msg.setCode(tmpTips.getCode());
                    }
                });
        eagleException.setValidMessages(validMessages);
        return eagleException;
    }

    @Data
    public static class ValidMessage {
        private String fieldName;
        private Object fieldValue;
        private String code;
        private String tips;
        private String tipsKey;
        private String defaultMessage;
    }
}

考慮到用戶提示信息需要避免直接硬編碼,建議配置化,所以用到了ExceptionTipsCache這個類實現(xiàn)了異常提示信息配置化,緩存化,國際化

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.yaml.snakeyaml.Yaml;

import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;

/** @author zouwei */
@Slf4j
public class ExceptionTipsCache {
    /** 指定的classpath文件夾 */
    private static String classpath;

    /** 默認的文件夾 */
    private static final String DEFAULT_DIR = "config/tips";

    /** 默認的國際化 */
    private static final String DEFAULT_I18N = "zh-cn";

    /** 提示文件后綴 */
    private static final String TIPS_FILE_SUFFIX = "_tips";
    /** 構(gòu)建一個本地緩存 */
    private static final LoadingCache<String, ExceptionTips> CACHE =
            CacheBuilder.newBuilder()
                    // 初始化100個
                    .initialCapacity(100)
                    // 最大10000
                    .maximumSize(10000)
                    // 30分鐘沒有讀寫操作數(shù)據(jù)就過期
                    .expireAfterAccess(30, TimeUnit.MINUTES)
                    // 只有當內(nèi)存不夠的時候才會value才會被回收
                    .softValues()
                    .build(
                            new CacheLoader<String, ExceptionTips>() {
                                // 如果get()沒有拿到緩存,直接點用load()加載緩存
                                @Override
                                public ExceptionTips load(String key) throws IOException {
                                    return getTips(key);
                                }

                                /**
                                 * 在調(diào)用getAll()的時候,如果沒有找到緩存,就會調(diào)用loadAll()加載緩存
                                 *
                                 * @param keys
                                 * @return
                                 * @throws Exception
                                 */
                                @Override
                                public Map<String, ExceptionTips> loadAll(
                                        Iterable<? extends String> keys) throws Exception {
                                    // 暫不支持
                                    return super.loadAll(keys);
                                }
                            });

    /**
     * 設(shè)置指定的classpath
     *
     * @param classpath
     */
    public static void setClasspath(String classpath) {
        ExceptionTipsCache.classpath = StringUtils.isBlank(classpath) ? DEFAULT_DIR : classpath;
    }

    /**
     * @param key
     * @return
     */
    public static ExceptionTips get(String key) {
        try {
            return CACHE.get(key);
        } catch (Exception e) {
            throw new RuntimeException("沒有找到指定的配置:" + key);
        }
    }

    /**
     * 加載默認yaml進緩存
     *
     * @return
     * @throws IOException
     */
    public static Map<String, Map<String, String>> loadTips() throws IOException {
        return loadTips(null, DEFAULT_I18N);
    }

    /**
     * 加載默認yaml進緩存
     *
     * @param directory
     * @throws IOException
     */
    public static Map<String, Map<String, String>> loadTips(String directory) throws IOException {
        return loadTips(directory, DEFAULT_I18N);
    }
    /**
     * 加載指定yaml進緩存
     *
     * @param directory
     * @param i18n
     * @throws IOException
     */
    public static Map<String, Map<String, String>> loadTips(String directory, String i18n)
            throws IOException {
        classpath = StringUtils.isBlank(directory) ? DEFAULT_DIR : directory;
        StringJoiner sj = new StringJoiner("/");
        sj.add(classpath);
        sj.add(i18n + TIPS_FILE_SUFFIX);
        ClassPathResource resource = new ClassPathResource(sj.toString());
        return doLoadTips(i18n, resource.getInputStream());
    }

    /**
     * 添加緩存
     *
     * @param i18n
     * @param inputStream
     */
    private static Map<String, Map<String, String>> doLoadTips(
            String i18n, InputStream inputStream) {
        Yaml yaml = new Yaml();
        Map<String, Map<String, String>> map = yaml.loadAs(inputStream, Map.class);
        map.forEach(
                (k, v) -> {
                    String code = String.valueOf(v.get("code"));
                    String tips = String.valueOf(v.get("tips"));
                    CACHE.put(i18n + ":" + k, new ExceptionTips(i18n, k, code, tips));
                });
        return map;
    }

    /**
     * 沒有獲取到緩存時單獨調(diào)用
     *
     * @param key
     * @return
     * @throws IOException
     */
    private static ExceptionTips getTips(String key) throws IOException {
        if (StringUtils.isBlank(key)) {
            throw new RuntimeException("錯誤的key值,請按照\"zh-cn:USER_NO_EXIST\"格式輸入");
        }
        String[] keys = StringUtils.splitByWholeSeparatorPreserveAllTokens(key, ":");
        if (ArrayUtils.isNotEmpty(keys) && keys.length > 2) {
            throw new RuntimeException("錯誤的key值,請按照\"zh-cn:USER_NO_EXIST\"格式輸入");
        }
        String i18n = DEFAULT_I18N;
        String k;
        if (ArrayUtils.isNotEmpty(keys) && keys.length < 2) {
            k = keys[0];
        } else {
            i18n = keys[0];
            k = keys[1];
        }
        Map<String, Map<String, String>> map = loadTips(classpath, i18n);
        Map<String, String> v = map.get(k);
        String code = String.valueOf(v.get("code"));
        String tips = String.valueOf(v.get("tips"));
        return new ExceptionTips(i18n, k, code, tips);
    }

    @Data
    @AllArgsConstructor
    public static class ExceptionTips {
        private String i18n;
        private String key;
        private String code;
        private String tips;
    }
}

默認會加載resources里面的config/tips/zh-cn_tips文件


image-20191212162332026.png

這是一個yml類型的文件,數(shù)據(jù)結(jié)構(gòu)如下:

UNKNOWN_ERROR:
  code: -1
  tips: "未知錯誤"
VALID_ERROR:
  code: -2
  tips: "參數(shù)驗證錯誤"
USER_NO_EXIST:
  code: 11023
  tips: "用戶不存在"
USER_REPEAT_REGIST:
  code: 11024
  tips: "重復(fù)注冊"
USER_NAME_NOT_NULL:
  code: 11025
  tips: "用戶名不能為空"
USER_NAME_LENGTH_LIMIT:
  code: 11026
  tips: "用戶名不能長度要5到10個字符"

響應(yīng)數(shù)據(jù)結(jié)構(gòu)和異常類型統(tǒng)一后,我們需要統(tǒng)一處理controller的返回數(shù)據(jù),全部包裝成CommonResponse類型的數(shù)據(jù)。

import com.zx.eagle.annotation.IgnoreResponseAdvice;
import com.zx.eagle.vo.CommonResponse;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.Objects;

/** @author zouwei */
@RestControllerAdvice
public class CommonResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(
            MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        boolean ignore = false;
        IgnoreResponseAdvice ignoreResponseAdvice =
                returnType.getMethodAnnotation(IgnoreResponseAdvice.class);
        if (Objects.nonNull(ignoreResponseAdvice)) {
            ignore = ignoreResponseAdvice.value();
            return !ignore;
        }
        Class<?> clazz = returnType.getDeclaringClass();
        ignoreResponseAdvice = clazz.getDeclaredAnnotation(IgnoreResponseAdvice.class);
        if (Objects.nonNull(ignoreResponseAdvice)) {
            ignore = ignoreResponseAdvice.value();
        }
        return !ignore;
    }

    @Override
    public Object beforeBodyWrite(
            Object body,
            MethodParameter returnType,
            MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType,
            ServerHttpRequest request,
            ServerHttpResponse response) {
        if (Objects.isNull(body)) {
            return CommonResponse.successInstance();
        }
        if (body instanceof CommonResponse) {
            return body;
        }
        CommonResponse commonResponse = CommonResponse.successInstance(body);
        return commonResponse;
    }
}

很明顯,并不是所有的返回對象都需要包裝的,比如controller已經(jīng)返回了CommonResponse,那么就不需要包裝

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** @author zouwei */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface IgnoreResponseAdvice {
    /**
     * 是否需要被CommonResponseAdvice忽略
     *
     * @return
     */
    boolean value() default true;
}

其次,我們還需要統(tǒng)一處理異常

import com.google.common.collect.Lists;
import com.zx.eagle.common.config.ExceptionTipsStackConfig;
import com.zx.eagle.common.exception.EagleException;
import com.zx.eagle.common.exception.handler.ExceptionNotifier;
import com.zx.eagle.common.vo.CommonResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/** @author zouwei */
@Slf4j
@RestControllerAdvice
public class ExceptionResponseAdvice {

    @Autowired private ExceptionTipsStackConfig exceptionStack;

    @Autowired(required = false)
    private List<ExceptionNotifier> exceptionNotifierList;
    /**
     * 用戶行為導(dǎo)致的錯誤
     *
     * @param e
     * @return
     */
    @ExceptionHandler(EagleException.class)
    public CommonResponse handleEagleException(
            EagleException e, HttpServletRequest request, HttpServletResponse response) {
        String massage = handleExceptionMessage(e);
        CommonResponse commonResponse =
                CommonResponse.exceptionInstance(e.getCode(), massage, e.getTips());
        sendNotify(e, request, response);
        return commonResponse;
    }

    /**
     * 處理未知錯誤
     *
     * @param e
     * @return
     */
    @ExceptionHandler(RuntimeException.class)
    public CommonResponse handleRuntimeException(
            RuntimeException e, HttpServletRequest request, HttpServletResponse response) {
        String error = handleExceptionMessage(e);
        EagleException unknownException = EagleException.unknownException(error);
        CommonResponse commonResponse =
                CommonResponse.exceptionInstance(
                        unknownException.getCode(), error, unknownException.getTips());
        sendNotify(unknownException, request, response);
        return commonResponse;
    }

    /**
     * 處理參數(shù)驗證異常
     *
     * @param e
     * @param request
     * @param response
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public CommonResponse handleValidException(
            ConstraintViolationException e,
            HttpServletRequest request,
            HttpServletResponse response) {
        String error = handleExceptionMessage(e);
        Set<ConstraintViolation<?>> set = e.getConstraintViolations();
        Iterator<ConstraintViolation<?>> iterator = set.iterator();
        List<EagleException.ValidMessage> list = Lists.newArrayList();
        while (iterator.hasNext()) {
            EagleException.ValidMessage validMessage = new EagleException.ValidMessage();
            ConstraintViolation<?> constraintViolation = iterator.next();
            String message = constraintViolation.getMessage();
            Path path = constraintViolation.getPropertyPath();
            Object fieldValue = constraintViolation.getInvalidValue();
            String tipsKey = constraintViolation.getMessageTemplate();
            validMessage.setTipsKey(tipsKey);
            validMessage.setFieldName(path.toString());
            validMessage.setFieldValue(fieldValue);
            validMessage.setDefaultMessage(message);
            list.add(validMessage);
        }
        EagleException validException = EagleException.validException(list);
        sendNotify(validException, request, response);
        return CommonResponse.exceptionInstance(validException, error);
    }

    /**
     * 發(fā)送請求
     *
     * @param exception
     * @param request
     * @param response
     */
    private void sendNotify(
            EagleException exception, HttpServletRequest request, HttpServletResponse response) {
        if (!CollectionUtils.isEmpty(exceptionNotifierList)) {
            for (ExceptionNotifier notifier : exceptionNotifierList) {
                if (notifier.support(exception.getTipsKey())) {
                    notifier.handle(exception, request, response);
                }
            }
        }
    }
    /**
     * 處理異常信息
     *
     * @param e
     * @return
     */
    private String handleExceptionMessage(Exception e) {
        String massage = e.getMessage();
        String stackInfo = toStackTrace(e);
        String messageStackInfo = massage + "{" + stackInfo + "}";
        // 無論是否讓客戶端顯示堆棧信息,后臺都要記錄
        log.error(messageStackInfo);
        if (exceptionStack.isShowMessage() && exceptionStack.isShowStack()) {
            return messageStackInfo;
        } else if (exceptionStack.isShowMessage()) {
            return massage;
        } else if (exceptionStack.isShowStack()) {
            return stackInfo;
        }
        return StringUtils.EMPTY;
    }

    /**
     * 獲取異常堆棧信息
     *
     * @param e
     * @return
     */
    private static String toStackTrace(Exception e) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        try {
            e.printStackTrace(pw);
            return sw.toString();
        } catch (Exception e1) {
            return StringUtils.EMPTY;
        }
    }
}

為了解決有一些異常需要額外處理的,例如調(diào)用第三方接口,接口返回異常并告知費用不夠需要充值,這個時候就需要額外通知到相關(guān)人員及時充值。為此,特地添加一個接口:

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 異常通知器
 *
 * @author zouwei
 */
public interface ExceptionNotifier {

    /**
     * 是否支持處理該異常
     *
     * @param exceptionKey
     * @return
     */
    boolean support(String exceptionKey);

    /**
     * 處理該異常
     *
     * @param e
     * @param request
     */
    void handle(EagleException e, HttpServletRequest request, HttpServletResponse response);
}

為了滿足返回的異常信息可配置化,通過配置決定不同的環(huán)境返回指定的字段信息

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/** @author zouwei */
@Data
@Component
@ConfigurationProperties(prefix = "exception-tips-stack")
public class ExceptionTipsStackConfig {
    /** 是否顯示堆棧信息 */
    private boolean showStack = false;
    /** 是否顯示exception message */
    private boolean showMessage = false;
}

application.yaml中配置示例(根據(jù)環(huán)境配置):

exceptionTipsStack:
  #異常堆棧是否需要顯示
  showStack: true
  #開發(fā)提示信息是否需要顯示
  showMessage: true

為了保證返回的數(shù)據(jù)是指定的json格式,需要配置HttpMessageConverter

import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/** @author zouwei */
@Configuration
public class CustomWebConfigure implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.clear();
        converters.add(new MappingJackson2HttpMessageConverter());
    }
}

4.測試

先將application.yaml調(diào)整為:

exceptionTipsStack:
  #異常堆棧是否需要顯示
  showStack: true
  #開發(fā)提示信息是否需要顯示
  showMessage: true

編寫TestController:

import com.zx.eagle.exception.EagleException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.*;

/** @author zouwei */
@Validated
@RestController
@RequestMapping("/test")
public class TestController {

    /**
     *
     * @return
     * @throws EagleException
     */
    @GetMapping("/user_repeat")
    public String userRepeat() throws EagleException {
        throw EagleException.newInstance("USER_REPEAT_REGIST", "用戶重復(fù)注冊了,正常提示");
    }

    /**
     * 對于用戶來說,不應(yīng)該直接看到NoSuchAlgorithmException,因為這并不是用戶造成的,所以應(yīng)該使用未知錯誤
     *
     * @return
     */
    @GetMapping("/unknownException")
    public String unknownException() throws EagleException {
        final MessageDigest md;
        try {
            md = MessageDigest.getInstance("MD4");
        } catch (final NoSuchAlgorithmException e) {
            throw EagleException.unknownException("顯然是因為程序沒有獲取MD5算法導(dǎo)致的異常,這是完全可以避免的");
        }
        return "success";
    }


    @GetMapping("/valid")
    public String validException(
            @NotNull(message = "USER_NAME_NOT_NULL")
                    @Length(min = 5, max = 10, message = "USER_NAME_LENGTH_LIMIT")
                    String username,
            @NotNull(message = "年齡不能為空")
                    @Min(value = 18, message = "年齡必須大于18歲")
                    @Max(value = 70, message = "年齡不能超過70歲")
                    int age)
            throws EagleException {
        // return "success";
        throw EagleException.newInstance("USER_NO_EXIST", "用戶不存在,這個地方要注意");
    }

    @PostMapping("/valid4Post")
    public String validException2(@Valid @RequestBody User user, BindingResult result) {
        return "success";
    }

    @Data
    private static class User {

        @Length(min = 5, max = 10, message = "USER_NAME_LENGTH_LIMIT")
        private String username;

        @Min(value = 18, message = "年齡必須大于18歲")
        @Max(value = 70, message = "年齡不能超過70歲")
        private int age;
    }
}

測試結(jié)果:

url: /test/user_repeat

{
code: "11023",
error: "用戶重復(fù)注冊了,正常提示{EagleException(code=11023, tipsKey=USER_REPEAT_REGIST, tips=重復(fù)注冊, validMessages=null) at com.zx.eagle.common.exception.EagleException.newInstance(EagleException.java:50) at com.zx.eagle.common.controller.InsuranceController.userRepeat(InsuranceController.java:23)",
message: "重復(fù)注冊",
validMessage: null,
data: null
}

url: /test/unknownException

{
code: "-1",
error: "顯然是因為程序沒有獲取MD5算法導(dǎo)致的異常,這是完全可以避免的{EagleException(code=-1, tipsKey=UNKNOWN_ERROR, tips=未知錯誤, validMessages=null) at com.zx.eagle.common.exception.EagleException.newInstance(EagleException.java:50) at com.zx.eagle.common.exception.EagleException.unknownException(EagleException.java:60) at com.zx.eagle.common.controller.InsuranceController.unknownException(InsuranceController.java:37) ",
message: "未知錯誤",
validMessage: null,
data: null
}

url:/test/valid?username=z2341d&age=10

{
code: "-2",
error: "test.age: 年齡必須大于18歲{javax.validation.ConstraintViolationException: test.age: 年齡必須大于18歲 at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:117) }",
message: "參數(shù)驗證錯誤",
validMessage: [
{
fieldName: "test.age",
fieldValue: 10,
code: "-2",
tips: "年齡必須大于18歲",
tipsKey: "年齡必須大于18歲",
defaultMessage: "年齡必須大于18歲"
}
],
data: null
}

url:/test/valid4Post
結(jié)果同上

至此,關(guān)于異常處理的相關(guān)思考和實現(xiàn)闡述完畢。小伙伴們可以依據(jù)類似的思考方式實現(xiàn)符合自身實際情況的異常處理方式。

歡迎有過類似思考的小伙伴一起討論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容