软工后端API复盘
返回格式
我这次软工作业选用的API格式是这样的
{
# 返回状态码
code: integer
# 返回值
data: object
}
但其实更好的话还是要写完整来:
{
# 返回状态码
code: integer
# 返回信息描述
message: string
# 返回值
data: object
}
先说code
吧
code
code顾名思义,就是状态码,我在这边是这么设计的
参数名 | 值 | 说明 |
---|---|---|
SUCCESS | 0 | 操作成功 |
SYSTEM_BUSY | -1 | 系统繁忙,此时请开发者稍候再试 |
MISSING_PARAMETERS | 1001 | 缺少相关参数 |
FAIL_TO_UPDATE_DISH_STAR | 2001 | 更新菜品星级失败 |
FAIL_TO_UPDATE_USER_INFO | 2002 | 更新用户信息失败 |
FAIL_TO_UPDATE_MARKED_WINDOW | 2003 | 更新收藏窗口失败 |
FAIL_TO_SAVE_DISH_STAR | 3001 | 增添菜品星级失败 |
FAIL_TO_SAVE_FEEDBACK | 3002 | 增添反馈信息失败 |
INVALID_CODE | 40029 | code 无效 |
你看到之后什么感觉,我反正日后看到是看都不想看,太糟糕了!
这样虽然能够照常满足业务,但状态码太凌乱了!
我们应该参考HTTP的状态码来设计,比如设计成这样:(四位数)
1000-1999 表示参数错误
2000-2999 表示用户错误
3000-3999 表示接口异常
......
这样前端开发人员在得到返回值后,根据状态码就可以知道,大概什么错误,再根据message相关的信息描述,可以快速定位
而我的设计则是按CRUD分类,是不太好的(40029是为了匹配微信小程序的code)
message
这个字段是我没有的,大概作用就是在发生错误时,友好的进行提示。
一般的设计是和code状态码一起设计,如:
//返回状态码
public enum ResultCode {
private Integer code;
private String message;
ResultCode(Integer code, String message){
this.code = code;
this. message = message;
}
public Integer code(){
return this code;
}
public String message() {
return this.message;
}
/*成功状态码*/
SUCCESS(1, "成功"),
/*参数错误: 1001-1999 */
PARAM_IS_INVALID(1001, "参数无效"),
PARAM_IS_BLANK(1002, "参数为空"),
PARAM_TYPE_BIND_ERROR(1003, "参数类型错误"),
PARAM_NOT_COMPLETE(1004, "参数缺失"),
/*用户错误: 2001-2999*/
USER_NOT_LOGGED_IN(2001, "用户未登录,访问的路径需要验证,请登录"),
USER_LOGIN_ERROR(2002, "账号不存在或密码错误"),
USER_ACCOUNT_FORBIDDEN (2003, "账号已被禁用"),
USER_NOT_EXIST(2004, "用户不存在"),
USER_HAS_EXISTED (2005, "用户已存在")
}
这样状态码和信息就会一一对应,比较好维护
data
按照上面来说的话,我们整个Result的格式应该是这样:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result implements Serializable {
private Integer code;
private String message;
private Object data;
}
但我是这么设计的,巧妙地用了个泛型,但是没有序列化:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JsonObject<T> {
private Integer code;
private T data;
}
控制层controller
假设我们在controller层处理业务请求,并返回给前端,以order订单为例
@RestController
@RequestMapping ("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping ("{id}")
public Result get0rder(@PathVariable("id") Integer id) {
Order order = orderService.get0rderById(id);
Result result = new Result(ResultCode.SUCCESS, order);
return result;
}
}
在获得order对象之后,我们是用的Result构造方法进行包装赋值,然后进行返回。但是构造方法这样的包装很麻烦,或许我们可以优化一下。
美观优化
我们可以在Result类中,加入静态方法
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result implements Serializable {
// 。。。
//返回成功
public static Result success() {
Result result = new Result();
result.setResultCode(ResultCode.SUCCESS):
return result;
}
//返回成功
public static Result success(Obiect data) {
Result result = new Result();
result.setResultCode(ResultCode.SUCCESS):
result.setData(data);
return result;
}
//返回失败
public static Result failure(ResultCode resultCode) {
Result result = new Result():
result.setResultCode(resultCode);
return result;
}
//返回失败
public static Result failure (ResultCode resultCode, object data) {
Result result = new Result();
result.setResultCode(resultCode);
result.setData(data);
return result;
}
}
这时候controller就可以这么优化
@RestController
@RequestMapping ("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping ("{id}")
public Result get0rder(@PathVariable("id") Integer id) {
if(id == null){
return Result.failure(ResultCode.PARAM_IS_VALID);
}
Order order = orderService.get0rderById(id);
return Result.success(order);
}
}
是不是美多了!!!虽然代码更优雅了,但是,还有问题!
- 每个方法的返回对象都是Result封装对象,没有实际业务含义,多余
- 在业务代码中,成功时调用Result.success(),异常错误调用Result.failure(),多余
- 上面判断id是否为null,其实我们可以使用hibernate validate做校验,没有必要在方法体中做判断,多余
持续优化
根据奥卡姆剃刀原则,咱们该切就切!
@RestController
@RequestMapping ("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping ("{id}")
public Order get0rder(@PathVariable("id") Integer id) {
return orderService.get0rderById(id);
}
}
这样很直观,而且更贴近业务了,但状态和异常要怎么做呢?
我们需要做几个事情:
- 定义一个注解
@ResponseResult
,表示这个接口返回的值需要包装一下
@Retention(RUNTIME)
@Target({TYPE,METHOD})
@Documented
public @interface ResponseResult{
}
- 做一个拦截器,拦截请求,判断此请求是否此请求返回的值需要包装,其实就是运行的时候,解析
@ResponseResult
注解
// 请求拦截器
@S1f4j
@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
// 标记名称
public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
// 请求的方法
if(handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod) handler;
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
// 判断是否在类对象上面加了注解
if(clazz.isAnnotationPresent(ResponseResult,class)){
//设置此请求返回体,需要包装,住下传递,在ResponseBodyAdvice接口进行判断
request.setAttribute(RESPONSE_RESULT_ANN,
clazz.getAnnotation(ResponseResult.class));
}else if(method.isAnnotationPresent(ResponseResult.class)){//方法体上是否有注解
//设置此请求返回体,需要包装,住下传递,在ResponseBodyAdvice接口进行判断
request.setAttribute(RESPONSE_RESULT_ANN,
method.getAnnotation(ResponseResult.class))
}
}
return true;
}
}
这边就是获取此请求,是否需要返回值包装,设置一个属性标记
- 核心就是要实现接口
ResponseBodyAdvice
和@ControllerAdvice
,判断是否需要包装返回值,如果需要,就把Controller接口的返回值进行重写。
我们接着重写返回体:
@S1f4j
@Component
public class ResponseResultHandler implements ResponseBodyAdvice<Object>{
// 标记名称
public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";
//是否请求 包含了 包装注解 标记 没有就直接返回 不再要重写返回体
@Override
public boolean supports(MethodParameter returnType,Class<? extends
HttpMessageConverter<?>> converterType) {
ServletRequestAttributes sra = ((ServietRequestAttributes)
RequestContextholder.getRequestAttributes());
HttpServletRequest request = sra.getRequest();
// 判断请求是否有包装标记
ResponseResult responseResultAnn = (ResponseResult)request.getAttribute(RESPONSE_RESULT_ANN) ;
return responseResultAnn == null ? false: true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType
selectedContentType,Class<? extends
HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
log.info("进入返回体,重写格式处理中.....");
// 异常处理
if(body instanceof ErrorResult){
log.info("返回值 异常 做包装 处理中.....");
ErrorResult errorResult = (ErrorResult)body;
return Result.failure(errorResult.getCode(),errorResult.getMessage,errorResult.getErrors())
}
return Result.success(body);
}
}
重写controller:
@RestController
@RequestMapping ("/orders")
@ResponseResult
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping ("{id}")
public Order get0rder(@PathVariable("id") Integer id) {
return orderService.get0rderById(id);
}
}
在controller上或者方法体上加上@ResponseResult
注解,就ok拉!
还想优化?
比如,每次请求都要反射一下,获取请求的方法是否需要包装,其实这里可以做个缓存,不需要每次都做解析