美文网首页java 设计
异常处理与开发体验和用户体验

异常处理与开发体验和用户体验

作者: 梦想实现家_Z | 来源:发表于2019-12-12 19:14 被阅读0次

    1.接口调用

    理想情况来说,客户端想要的是目标接口成功响应的结果。比如查询用户信息,服务器只需要返回我想要的用户信息给我就可以了。类似:

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

    当然,以上也只是停留在理想上。正常接口大多数情况下,会正常响应给出用户信息。但是在非正常情况下呢,比如访问量剧增,服务器响应不过来了;或者因为用户的查询参数不合理,导致查询不出任何结果;亦或者程序的代码不够健壮,在某种情况下才会报错;此时,有一些用户就不能顺利地获取用户信息,那么他们得到的响应数据是什么呢?页面的表现形式是怎样的呢?假如是编码问题,如何才能让开发人员快速的定位到报错位置呢?

    2.统一响应数据结构

    为了避免程序没有给出正确的响应数据导致客户端不知道如何与用户交互,我们需要协商出一个统一的响应数据结构。也就是说,我们需要为异常情况考虑一个合理的交互方式,让用户知道,我们的服务,只是暂时有一些问题(ps:这里的问题不仅仅是指服务器的问题,还包括用户操作正确性的问题)。也能让开发人员通过响应数据了解到程序的出错信息,包括出错位置,异常类型,甚至包括修正方式。

    成功示例

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

    失败示例

    {
      "message":"服务拥堵,请稍后再试!",
      "data":null
    }
    

    这样的数据结构设计貌似能让用户感觉到我们满满的诚意,客户端开发人员可以在data为空的时候,把“message”的数据展示给用户,告知用户,我们的服务当前的一个状态,并指示用户正确的操作方式。

    可是细想一下,对于客户端开发人员来说,data为null的时候一定就是异常的响应嘛?这样的判断显然过于武断。比如某些添加,或者修改的接口,data完全没有必要返回任何数据。所以我们还需要给出某个标识让客户端知道我们确实是处理成功了。

    成功示例

    {
      //错误码
      "code":0,
      "message":"处理成功",
      "data":{
        "name":"zouwei",
        "age":26
      }
    }
    

    失败示例

    {
      //错误码
      "code":10001,
      "message":"输入的手机号码还未注册!",
      "data":null
    }
    

    现在,客户端开发人员可以根据协商好的错误码区分当前的请求是否被成功处理,而不是通过判断data=null来确定是否成功,避免潜在的编码风险,提高开发体验度。

    假如服务器代码已经非常健壮的话,上面的数据结构是完全没有问题的,可是还是过于理论。因为在实际场景中,没有人能保证代码没有任何潜在的问题,这类问题就不属于用户造成的问题了,而是在编码过程中因为各种原则造成的纰漏或者不够健壮,这类问题是可以避免的。比如常见的NullPointerException,NumberFormatException等,这类异常一旦发生,响应数据应该怎么定义,因为这一类异常不需要事先声明,所以不能准确地对这一类异常定性,那么可以做一个默认的code,归类为“未知错误”。为了能让开发人员在开发阶段能尽快地定位到异常类型和位置,可以考虑添加一个字断展示异常堆栈。

    示例

    {
      //错误码
      "code":-1,
      //异常堆栈,只有在开发和测试环境打开
      "error":"java.lang.NullPointerException",
      "message":"未知错误!",
      "data":null
    }
    

    (ps:堆栈信息字段只是简单表示,实际情况会包含异常位置,异常详细信息等)

    上述的error字段仅仅在开发和测试阶段出现,线上环境需要去掉。可通过配置化的方式实现这个功能。

    3.java服务端实现

    依赖

    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")
        }
    

    统一的响应数据结构

    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";
        /** 响应码 */
        private String code;
        /** 异常信息 */
        private String error;
        /** 用户提示 */
        private String message;
        /** 参数验证错误 */
        private List<EagleException.ValidMessage> validMessage;
        /** 响应数据 */
        private T data;
    
        private CommonResponse() {}
    
        private CommonResponse(String code, String error, String message) {
            this();
            this.code = code;
            this.message = message;
            this.error = error;
        }
    
        /**
         * 成功响应
         *
         * @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();
        }
        /**
         * 用户行为导致的错误
         *
         * @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);
        }
    
        /**
         * 正常响应
         *
         * @param data
         * @param <T>
         * @return
         */
        public static <T> CommonResponse<T> successInstance(T data) {
            return new CommonResponse<>(data);
        }
    
        /**
         * 正常响应
         *
         * @param <T>
         * @return
         */
        public static <T> CommonResponse<T> successInstance() {
            return (CommonResponse<T>) successInstance(StringUtils.EMPTY);
        }
    
        /**
         * 用户行为导致的错误
         *
         * @param e
         * @param <T>
         * @return
         */
        public static <T> CommonResponse<T> exceptionInstance(EagleException e, String error) {
            return new CommonResponse<>(e, error);
        }
    }
    

    统一异常类型

    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 {
    
        /** 参数验证异常 */
        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;
        }
    
        /**
         * 创建异常
         *
         * @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);
        }
    
        /**
         * 参数验证错误
         *
         * @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());
                            // 参数验证错误
                            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这个类实现了异常提示信息配置化,缓存化,国际化

    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";
        /** 构建一个本地缓存 */
        private static final LoadingCache<String, ExceptionTips> CACHE =
                CacheBuilder.newBuilder()
                        // 初始化100个
                        .initialCapacity(100)
                        // 最大10000
                        .maximumSize(10000)
                        // 30分钟没有读写操作数据就过期
                        .expireAfterAccess(30, TimeUnit.MINUTES)
                        // 只有当内存不够的时候才会value才会被回收
                        .softValues()
                        .build(
                                new CacheLoader<String, ExceptionTips>() {
                                    // 如果get()没有拿到缓存,直接点用load()加载缓存
                                    @Override
                                    public ExceptionTips load(String key) throws IOException {
                                        return getTips(key);
                                    }
    
                                    /**
                                     * 在调用getAll()的时候,如果没有找到缓存,就会调用loadAll()加载缓存
                                     *
                                     * @param keys
                                     * @return
                                     * @throws Exception
                                     */
                                    @Override
                                    public Map<String, ExceptionTips> loadAll(
                                            Iterable<? extends String> keys) throws Exception {
                                        // 暂不支持
                                        return super.loadAll(keys);
                                    }
                                });
    
        /**
         * 设置指定的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;
        }
    
        /**
         * 没有获取到缓存时单独调用
         *
         * @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类型的文件,数据结构如下:

    UNKNOWN_ERROR:
      code: -1
      tips: "未知错误"
    VALID_ERROR:
      code: -2
      tips: "参数验证错误"
    USER_NO_EXIST:
      code: 11023
      tips: "用户不存在"
    USER_REPEAT_REGIST:
      code: 11024
      tips: "重复注册"
    USER_NAME_NOT_NULL:
      code: 11025
      tips: "用户名不能为空"
    USER_NAME_LENGTH_LIMIT:
      code: 11026
      tips: "用户名不能长度要5到10个字符"
    

    响应数据结构和异常类型统一后,我们需要统一处理controller的返回数据,全部包装成CommonResponse类型的数据。

    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已经返回了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;
    }
    

    其次,我们还需要统一处理异常

    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;
        /**
         * 用户行为导致的错误
         *
         * @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;
        }
    
        /**
         * 处理参数验证异常
         *
         * @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);
        }
    
        /**
         * 发送请求
         *
         * @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;
            }
        }
    }
    

    为了解决有一些异常需要额外处理的,例如调用第三方接口,接口返回异常并告知费用不够需要充值,这个时候就需要额外通知到相关人员及时充值。为此,特地添加一个接口:

    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);
    }
    

    为了满足返回的异常信息可配置化,通过配置决定不同的环境返回指定的字段信息

    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中配置示例(根据环境配置):

    exceptionTipsStack:
      #异常堆栈是否需要显示
      showStack: true
      #开发提示信息是否需要显示
      showMessage: true
    

    为了保证返回的数据是指定的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调整为:

    exceptionTipsStack:
      #异常堆栈是否需要显示
      showStack: true
      #开发提示信息是否需要显示
      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", "用户重复注册了,正常提示");
        }
    
        /**
         * 对于用户来说,不应该直接看到NoSuchAlgorithmException,因为这并不是用户造成的,所以应该使用未知错误
         *
         * @return
         */
        @GetMapping("/unknownException")
        public String unknownException() throws EagleException {
            final MessageDigest md;
            try {
                md = MessageDigest.getInstance("MD4");
            } catch (final NoSuchAlgorithmException e) {
                throw EagleException.unknownException("显然是因为程序没有获取MD5算法导致的异常,这是完全可以避免的");
            }
            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;
        }
    }
    

    测试结果:

    url: /test/user_repeat

    {
    code: "11023",
    error: "用户重复注册了,正常提示{EagleException(code=11023, tipsKey=USER_REPEAT_REGIST, tips=重复注册, 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: "重复注册",
    validMessage: null,
    data: null
    }
    

    url: /test/unknownException

    {
    code: "-1",
    error: "显然是因为程序没有获取MD5算法导致的异常,这是完全可以避免的{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: "参数验证错误",
    validMessage: [
    {
    fieldName: "test.age",
    fieldValue: 10,
    code: "-2",
    tips: "年龄必须大于18岁",
    tipsKey: "年龄必须大于18岁",
    defaultMessage: "年龄必须大于18岁"
    }
    ],
    data: null
    }
    

    url:/test/valid4Post
    结果同上

    至此,关于异常处理的相关思考和实现阐述完毕。小伙伴们可以依据类似的思考方式实现符合自身实际情况的异常处理方式。

    欢迎有过类似思考的小伙伴一起讨论。

    相关文章

      网友评论

        本文标题:异常处理与开发体验和用户体验

        本文链接:https://www.haomeiwen.com/subject/xyutnctx.html