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文件
这是一个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个字符"