关于java异常处理与开发体验和用户体验的思考(上)

简介: 关于java异常处理与开发体验和用户体验的思考

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
     * @throws ExecutionException
     */
    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.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个字符"



相关文章
|
10天前
|
Java API Maven
如何使用Java开发抖音API接口?
在数字化时代,社交媒体平台如抖音成为生活的重要部分。本文详细介绍了如何用Java开发抖音API接口,从创建开发者账号、申请API权限、准备开发环境,到编写代码、测试运行及注意事项,全面覆盖了整个开发流程。
53 10
|
15天前
|
Java 编译器
探索Java中的异常处理机制
【10月更文挑战第35天】在Java的世界中,异常是程序运行过程中不可避免的一部分。本文将通过通俗易懂的语言和生动的比喻,带你了解Java中的异常处理机制,包括异常的类型、如何捕获和处理异常,以及如何在代码中有效地利用异常处理来提升程序的健壮性。让我们一起走进Java的异常世界,学习如何优雅地面对和解决问题吧!
|
3天前
|
安全 Java 程序员
Java中的异常处理:从新手到专家
在Java编程的世界里,异常处理是每个开发者必须面对的挑战。本文将带你从基础的异常概念出发,逐步深入到高级处理技巧,让你在遇到代码中的“意外”时,能够从容应对,甚至化险为夷。
|
8天前
|
Java 数据库连接 开发者
Java中的异常处理:从基础到高级
【10月更文挑战第42天】在Java的世界中,异常处理是维护程序稳定性和健壮性的关键。本文将带你深入了解Java的异常处理机制,从基本的try-catch语句出发,逐步探索更复杂的异常处理策略。我们将通过实际代码示例来演示如何捕获和处理异常,以及如何自定义异常类型来满足特定需求。无论你是Java新手还是有经验的开发者,这篇文章都将帮助你更好地理解和应用Java的异常处理。
|
15天前
|
Java 开发者
Java中的异常处理:从基础到高级
【10月更文挑战第35天】在Java的世界里,异常处理是维护程序健壮性的关键。本文将深入浅出地探讨Java的异常处理机制,从基本的try-catch语句到自定义异常类的实现,带领读者理解并掌握如何在Java中优雅地处理错误和异常。我们将通过实际代码示例,展示如何捕获、处理以及预防潜在的运行时错误,确保程序即使在面临意外情况时也能保持稳定运行。
33 7
|
16天前
|
SQL 安全 Java
安全问题已经成为软件开发中不可忽视的重要议题。对于使用Java语言开发的应用程序来说,安全性更是至关重要
在当今网络环境下,Java应用的安全性至关重要。本文深入探讨了Java安全编程的最佳实践,包括代码审查、输入验证、输出编码、访问控制和加密技术等,帮助开发者构建安全可靠的应用。通过掌握相关技术和工具,开发者可以有效防范安全威胁,确保应用的安全性。
34 4
|
14天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
18天前
|
Java
Java 异常处理下篇:11 个异常处理最佳实践
本文深入探讨了 Java 异常处理的最佳实践,包括早抛出晚捕获、只捕获可处理的异常、不要忽略捕获的异常、抛出具体检查性异常、正确包装自定义异常、记录或抛出异常但不同时执行、避免在 `finally` 块中抛出异常、避免使用异常进行流程控制、使用模板方法处理重复的 `try-catch`、尽量只抛出与方法相关的异常以及异常处理后清理资源。通过遵循这些实践,可以提高代码的健壮性和可维护性。
|
18天前
|
缓存 监控 Java
如何运用JAVA开发API接口?
本文详细介绍了如何使用Java开发API接口,涵盖创建、实现、测试和部署接口的关键步骤。同时,讨论了接口的安全性设计和设计原则,帮助开发者构建高效、安全、易于维护的API接口。
47 4
|
19天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
40 2
下一篇
无影云桌面