java 如何正确使用接口返回对象Result

2023-02-12,,,,

1. Result的使用

Result的使用,是java项目中开发接口的必备,它经常被我们用作接口的返回对象,方便前端或者其他程序的远程调用后处理业务。它一般包括以下几个属性:

code:一般根据系统设定,在接口调用中返回代表含义不同的编码(例如:code=2000代表接口调用正常,code=1000代表无权限 等等),而且项目中会有一个集中设定code的配置文件,方便根据编码查询错误具体的错误信息
message:调用接口时,返回的信息。一般接口正常调用返回"成功",调用失败返回失败原因,或者直接返回异常信息
data:调用接口后返回的数据,一般为使用 泛型T。例如:查询接口,data中即存放查询出来的数据。

Result.java

public class Result<T> implements Serializable {
// 标识代码,2000表示成功,其它数值表示出错
private int code;
// 返回的数据
private T data;
// 提示信息,供报错时使用
private String message;
// 返回时间
private String timestamp;
// 有参构造
public Result(int code, String message, T data) {
this.timestamp = String.valueOf(System.currentTimeMillis());
this.code = code;
this.data = data;
this.message = message;
this.traceId = traceId;
} // sucess、error两个方法方便Result的使用
public static <T> Result<T> success(T data) {
return new Result(ResultCode.OK.getCode(), ResultCode.OK.getMessage(), data);
}
public static <T> Result<T> error(ResultCode code) {
return new Result(code.getCode(), code.getMessage(), null);
}
}

ResultCode.java 用于定于各种状态码 以及 状态码的具体业务含义的描述

public enum ResultCode  {
UNAUTHORIZED(1000, "未经授权,无法访问"),
OK(2000, "成功"),
; private int code;
private String message; ResultCode(int code, String message) {
this.code = code;
this.message = message;
} public int getCode() {
return code;
} public String getMessage() {
return message;
}
}

2. 存在的问题

当我们在享受Result给我们带来的便利的同时,也在许多场景困惑于它带来的"麻烦"。java的远程调用遵循:使用起来要像在本地方法调用的感觉一样。

举个栗子

一个根据id查询用户的例子,使用Result作为返回结果

// 提供服务方
public interface UserService {
Result<User> getUserById(Long userId);
} // 调用例子
public User testGetUser(Long userId) {
String userKey = "userid-" + userId;
// 先查缓存,如果命中则返回缓存中的user
// cacheManager.get(123, userKey);
// ... try{
Result<User> reslut = userService.getUserById(userId);
if(result.isSuccess()) {
cacheManager.put(123, userKey, result.getData());
return reslut.getData();
}
// 否则清空缓存对象,代表用户不存在
cacheManager.put(123, userKey, NullCacheObject.getInstance(), 3600);
return null;
} catch(Exception e) {
// TODO log
throw new DemoException("getUserById error, userId:" + userId, e);
}
}

从result.isSuccess为fasle的地方思考,我们并分不清报错的实际含义:是因为网络异常、DB错误等系统错误导致、或是因为用户不存在等业务错误导致,从Result中取得的错误码,还要去服务方提供的状态码文档中去查找分析,如果是服务端系统异常,那么15行将导致后续1小时对该用户的请求都认为用户不存在。

严谨点的写法:

// 调用例子
public User testGetUser(Long userId) {
String userKey = "userid-" + userId;
// 先查缓存,如果命中则返回缓存中的user
// cacheManager.get(123, userKey);
// ... try{
Result<User> reslut = userService.getUserById(userId);
if(result.isSuccess()) {
cacheManager.put(123, userKey, result.getData());
return reslut.getData();
}
try{
if("USER_NOT_FOUND".equals(result.getCode())) {
// 清空缓存对象,代表用户不存在
cacheManager.put(123, userKey, NullCacheObject.getInstance(), 3600);
} else {
// 可能是SYSTEM_ERROE、DB_ERROR等系统异常
throw new DemoException("getUserById error, userId:" + userId + ",result=" + result );
}
} catch(DemoExpcetion e) {
throw e;
}
} catch(Exception e) {
// TODO log
throw new DemoException("getUserById error, userId:" + userId, e);
}
return null;
}

代码变得复杂起来。

下面是接口提供方直接将系统异常抛出,将业务异常封装的写法

public interface UserService{
User getUserById(Long userId) throws DemoAppException;
} // 调用例子
public User testGetUser(Long userId) {
String userKey = "userid-" + userId;
// 先查缓存,如果命中则返回缓存中的user
// cacheManager.get(123, userKey);
// ... try{
User user = userService.getUserById(userId);
if(user != null) {
cacheManager.put(123, userKey, user);
return user;
}
} catch(Exception e) {
// TODO log
throw new DemoException("getUserById error, userId:" + userId, e);
}
return null;
}

这样代码会比较简介,而且符合我们的调用习惯:是远程调用异常处理起来像本地异常一样,不然会导致异常的处理很混乱、复杂。


3. 总结

业务异常 与 系统异常要分清并不同化处理

业务异常:例如:无权限、无此用户等,调用方会根据返回的不同异常code处理不同的业务,无权限会提醒"请分配权限",无此用户会提醒"先注册,再使用系统",此类异常封装为固定code是有价值所在,方便调用方。
系统异常:例如:网络超时、DB异常等问题,对于此类异常掉用方并无能力处理此类异常,即使我们将此类异常用ResultCode封装起来后,其实调用方并不会根据我们返回的不同code去具体处理业务,而是用来日志定位、问题追踪。对于这类问题如果直接根据有无业务结果来判断,直接抛异常比Result封装异常后理解和使用上的更直接。同时也减少调用的资源开支。这个时候,我们在开发过程中就要能够准确地预测掉用方在接收到我们封装的异常后,会如何处理。

--该文章参考自《阿里淘系-2021技术人的百宝黑皮书》

java 如何正确使用接口返回对象Result的相关教程结束。

《java 如何正确使用接口返回对象Result.doc》

下载本文的Word格式文档,以方便收藏与打印。