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



相关文章
|
6月前
|
安全 Java
Java异常处理:程序世界的“交通规则
Java异常处理:程序世界的“交通规则
369 98
|
6月前
|
安全 前端开发 Java
《深入理解Spring》:现代Java开发的核心框架
Spring自2003年诞生以来,已成为Java企业级开发的基石,凭借IoC、AOP、声明式编程等核心特性,极大简化了开发复杂度。本系列将深入解析Spring框架核心原理及Spring Boot、Cloud、Security等生态组件,助力开发者构建高效、可扩展的应用体系。(238字)
|
6月前
|
安全 Java 编译器
驾驭Java异常处理:从新手到专家的优雅之道
驾驭Java异常处理:从新手到专家的优雅之道
295 59
|
7月前
|
消息中间件 人工智能 Java
抖音微信爆款小游戏大全:免费休闲/竞技/益智/PHP+Java全筏开源开发
本文基于2025年最新行业数据,深入解析抖音/微信爆款小游戏的开发逻辑,重点讲解PHP+Java双引擎架构实战,涵盖技术选型、架构设计、性能优化与开源生态,提供完整开源工具链,助力开发者从理论到落地打造高留存、高并发的小游戏产品。
|
8月前
|
Java 数据库 C++
Java异常处理机制:try-catch、throws与自定义异常
本文深入解析Java异常处理机制,涵盖异常分类、try-catch-finally使用、throw与throws区别、自定义异常及最佳实践,助你写出更健壮、清晰的代码,提升Java编程能力。
|
8月前
|
JavaScript 安全 前端开发
Java开发:最新技术驱动的病人挂号系统实操指南与全流程操作技巧汇总
本文介绍基于Spring Boot 3.x、Vue 3等最新技术构建现代化病人挂号系统,涵盖技术选型、核心功能实现与部署方案,助力开发者快速搭建高效、安全的医疗挂号平台。
389 3
|
8月前
|
安全 Java 数据库
Java 项目实战病人挂号系统网站设计开发步骤及核心功能实现指南
本文介绍了基于Java的病人挂号系统网站的技术方案与应用实例,涵盖SSM与Spring Boot框架选型、数据库设计、功能模块划分及安全机制实现。系统支持患者在线注册、登录、挂号与预约,管理员可进行医院信息与排班管理。通过实际案例展示系统开发流程与核心代码实现,为Java Web医疗项目开发提供参考。
380 2
|
7月前
|
存储 Java 关系型数据库
Java 项目实战基于面向对象思想的汽车租赁系统开发实例 汽车租赁系统 Java 面向对象项目实战
本文介绍基于Java面向对象编程的汽车租赁系统技术方案与应用实例,涵盖系统功能需求分析、类设计、数据库设计及具体代码实现,帮助开发者掌握Java在实际项目中的应用。
271 0
|
8月前
|
安全 Oracle Java
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
580 0
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡