关于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个字符"



相关文章
|
5天前
|
人工智能 自然语言处理 Java
Spring AI,Spring团队开发的新组件,Java工程师快来一起体验吧
文章介绍了Spring AI,这是Spring团队开发的新组件,旨在为Java开发者提供易于集成的人工智能API,包括机器学习、自然语言处理和图像识别等功能,并通过实际代码示例展示了如何快速集成和使用这些AI技术。
Spring AI,Spring团队开发的新组件,Java工程师快来一起体验吧
|
6天前
|
前端开发 Java 程序员
【前端学java】Java中的异常处理(16)
【8月更文挑战第11天】Java中的异常处理
11 1
【前端学java】Java中的异常处理(16)
|
2天前
|
数据采集 供应链 JavaScript
分享基于Java开发的Java毕业设计实战项目题目
这篇文章分享了67套基于Java开发的毕业设计实战项目题目,覆盖了互联网、企业管理、电子政务、Java基础项目、ERP系统、校园相关、医疗以及其他细分行业等多个领域,并推荐了使用IDEA、Vue和Springboot的技术栈。
|
1天前
|
前端开发 JavaScript Java
Ajax进行异步交互:提升Java Web应用的用户体验
Ajax 技术允许在不重载整个页面的情况下与服务器异步交换数据,通过局部更新页面内容,极大提升了 Java Web 应用的响应速度和用户体验。本文介绍 Ajax 的基本原理及其实现方式,包括使用 XMLHttpRequest 对象发送请求、处理响应数据,并在 Java Web 应用中集成 Ajax。此外,还探讨了 Ajax 如何通过减少页面刷新、实时数据更新等功能改善用户体验。
7 3
|
1天前
|
Java 持续交付 项目管理
Maven是一款基于Apache许可的项目管理和构建自动化工具,在Java开发中极为流行。
Maven是一款基于Apache许可的项目管理和构建自动化工具,在Java开发中极为流行。它采用项目对象模型(POM)来描述项目,简化构建流程。Maven提供依赖管理、标准构建生命周期、插件扩展等功能,支持多模块项目及版本控制。在Java Web开发中,Maven能够自动生成项目结构、管理依赖、自动化构建流程并运行多种插件任务,如代码质量检查和单元测试。遵循Maven的最佳实践,结合持续集成工具,可以显著提升开发效率和项目质量。
11 1
|
1天前
|
Java API 数据库
详细介绍如何使用Spring Boot简化Java Web开发过程。
Spring Boot简化Java Web开发,以轻量级、易用及高度可定制著称。通过预设模板和默认配置,开发者可迅速搭建Spring应用。本文通过创建RESTful API示例介绍其快速开发流程:从环境准备、代码编写到项目运行及集成数据库等技术,展现Spring Boot如何使Java Web开发变得更高效、简洁。
8 1
|
1天前
|
Java 开发者
Java中的异常处理机制探究
【8月更文挑战第20天】在Java编程中,异常处理是确保程序健壮性和稳定性的重要环节。本文将深入探讨Java的异常处理机制,包括异常的分类、如何捕获和处理它们,以及在实际开发中如何有效地利用这些机制来提高代码质量和维护性。我们将一起探索异常处理的最佳实践,并讨论如何在不牺牲性能的前提下,构建更加健壮的Java应用程序。
|
2天前
|
分布式计算 Java API
Java 8带来了流处理与函数式编程等新特性,极大提升了开发效率
Java 8带来了流处理与函数式编程等新特性,极大提升了开发效率。流处理采用声明式编程模型,通过filter、map等操作简化数据集处理,提高代码可读性。Lambda表达式支持轻量级函数定义,配合Predicate、Function等接口,使函数式编程无缝融入Java。此外,Optional类及新日期时间API等增强功能,让开发者能更优雅地处理潜在错误,编写出更健壮的应用程序。
8 1
|
2天前
|
Java 程序员
Java中的异常处理:理解与实践
在Java编程的世界中,异常处理是保持程序稳健运行的关键。本文将通过浅显易懂的语言和生动的比喻,带你了解Java的异常处理机制,从基础概念到高级应用,一步步深入探讨。我们将一起学习如何优雅地处理那些不请自来的错误信息,确保你的代码像经验丰富的舞者一样,即使在遇到绊脚石时也能从容不迫地继续前行。
|
5天前
|
Java
Java中的异常处理:不仅仅是try-catch
在Java的世界里,异常处理是代码健壮性的守护神。它不仅仅是try-catch的简单运用,而是一种深入语言核心的设计哲学。本文将带你领略异常处理的艺术,从基础语法到高级技巧,让你的代码在风雨中屹立不倒。