我是靠谱客的博主 自由哈密瓜,最近开发中收集的这篇文章主要介绍原来阿里华为等大厂都是这么设计微服务接口的!接口的响应要明确接口的处理结果考虑接口变迁的版本策略接口明确同步/异步总结,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

接口设计需考虑到:

  • 命名
  • 参数列表
  • 包装结构体
  • 接口粒度
  • 版本策略
  • 幂等性实
  • 同步/异步处理

微服务架构下,如果接口设计思路和调用方理解不一致,就会导致很多问题。

接口的响应要明确接口的处理结果

某下单接口响应体包含

  • success
  • code
  • info
  • message
  • 二级嵌套对象data结构体

有时下单操作的响应结果是:
success=true、message=OK,貌似代表下单成功
但info却提示订单存在风险,code是个1001错误码,data中能看到订单状态是Cancelled,订单ID是-1,好像又表示下单失败。

有时下单接口又返回:
success=false,message=非法用户ID,似乎表示下单失败
但data里的orderStatus是Created、info是空、code是0。
下单到底是成功还是失败呢?

于是,我们开始好奇了

  • 结构体的code和HTTP响应状态码,是什么关系
  • success到底代表下单成功还是失败
  • info和message的区别是什么
  • data中永远都有数据吗?什么时候应该去查询data?

混乱原因是该下单服务本身并非真正执行下单,只是做一些预校验和预处理,真正的下单操作,需要下单服务内部调用另一个订单服务来处理;订单服务处理完成后,会返回订单状态和ID。

一切正常时,下单后的订单状态就是Created,订单ID是一个大于0数字。而结构体中的message和success,其实是下单服务的处理异常信息和处理成功与否的结果,code、info是调用订单服务的结果。

  • 刚才的第一个调用,下单服务自己没问题,success是true,message是OK,但调用订单服务时却因为订单风险问题被拒绝,所以code是5001,info是Risk order detected,data中的信息是订单服务返回的,所以最终订单状态是Cancelled
  • 第二个调用,因为用户ID非法,所以下单服务在校验了参数后直接就返回了success是false,message是Illegal userId。因为请求未到订单服务,所以info、code、data都是默认值,而订单状态的默认值又是Created。因此,第二次下单肯定失败了,但订单状态却是Created。

可见混乱的接口定义和实现方式,无法让调用者分清到底该怎么处理。为将接口设计更合理,需考虑:

  • 对外隐藏内部实现
    虽然下单服务调用订单服务进行真正的下单操作,但是直接接口其实是下单服务提供的,下单服务不该“直接”暴露其背后订单服务的状态码、错误描述
  • 设计接口结构时,明确每个字段的含义,以及客户端的处理方式。

遵循以上规则,现在调整返回结构体,去掉外层的info,即不再把订单服务的调用结果告知客户端:

@Data
public class APIResponse<T> {
    private boolean success;
    private T data;
    private int code;
    private String message;
}

明确接口设计:

  • 若出现非200状态码,即代表请求没有到下单服务,可能网络超时。这时,肯定无法拿到服务端的响应体,客户端可以给予友好提示,比如让用户重试,不需要继续解析响应结构体
  • 若响应码是200,解析响应体查看success,为false代表下单请求处理失败,可能是因为收单服务参数验证错误,也可能因为订单服务下单操作失败。再根据下单服务定义的错误码表和code,做相应处理。比如友好提示,或是让用户重新填写相关信息,其中友好提示的文字内容可从message获取
  • success为true时,才需继续解析响应体中的data结构体,其代表业务数据,通常有如下情况:
    • 通常情况下,success为true时订单状态是Created,获取orderId属性可以拿到订单号。
      特殊情况下,比如收单服务内部处理不当,或是订单服务出现了额外的状态,虽然success为true,但订单实际状态不是Created,这时可以给予友好的错误提示。

明确接口的设计逻辑,即可实现下单服务的服务端和客户端来模拟

首先,实现服务端的逻辑:

客户端按流程图逻辑实现,模拟下单场景:

  • error==1 模拟一个不存在的URL,请求无法到收单服务,会得到404的HTTP状态码,直接进行友好提示,这是第一层处理
  • error==2 模拟userId参数为空,下单服务因缺少userId参数提示非法用户,把响应体中的message展示给用户
  • error3 模拟userId1,因为用户有风险,下单服务调用订单服务出错。处理方式和之前没有任何区别,因为下单服务会屏蔽订单服务的内部错误。

但在服务端可以看到如下错误信息:

[WARN ] [.c.a.d.APIThreeLevelStatusController:36  ] - 用户 1 调用订单服务失败,原因是 Risk order detected

error==0的用例模拟正常用户,下单成功。这时可以解析data结构体提取业务结果,作为兜底,需要判断订单状态,如果不是Created则给予友好提示,否则查询orderId获得下单的订单号,这是第三层处理。

客户端的实现代码如下:

@GetMapping("client")
public String client(@RequestParam(value = "error", defaultValue = "0") int error) {
   String url = Arrays.asList("http://localhost:45678/apiresposne/server?userId=2",
        "http://localhost:45678/apiresposne/server2",
        "http://localhost:45678/apiresposne/server?userId=",
        "http://localhost:45678/apiresposne/server?userId=1").get(error);

    //第一层,先看状态码,如果状态码不是200,不处理响应体
    String response = "";
    try {
        response = Request.Get(url).execute().returnContent().asString();
    } catch (HttpResponseException e) {
        log.warn("请求服务端出现返回非200", e);
        return "服务器忙,请稍后再试!";
    } catch (IOException e) {
        e.printStackTrace();
    }

    //状态码为200的情况下处理响应体
    if (!response.equals("")) {
        try {
            APIResponse<OrderInfo> apiResponse = objectMapper.readValue(response, new TypeReference<APIResponse<OrderInfo>>() {
            });
            //第二层,success是false直接提示用户
            if (!apiResponse.isSuccess()) {
                return String.format("创建订单失败,请稍后再试,错误代码: %s 错误原因:%s", apiResponse.getCode(), apiResponse.getMessage());
            } else {
                //第三层,往下解析OrderInfo
                OrderInfo orderInfo = apiResponse.getData();
                if ("Created".equals(orderInfo.getStatus()))
                    return String.format("创建订单成功,订单号是:%s,状态是:%s", orderInfo.getOrderId(), orderInfo.getStatus());
                else
                    return String.format("创建订单失败,请联系客服处理");
            }
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
    return "";
}

改造后代码明确了接口每一个字段的含义,以及对于各种情况服务端的输出和客户端的处理步骤,对齐了客户端和服务端的处理逻辑。那么现在,你能回答前面那4个让人疑惑的问题了吗?

为简化服务端代码,可把包装API响应体APIResponse的工作交由框架自动完成,这样直接返回DTO OrderInfo即可。对于业务逻辑错误,可以抛出一个自定义异常:

在ServerException中包含错误码和错误消息:

然后,定义一个 @RestControllerAdvice 完成自动包装响应体:

  • 通过实现ResponseBodyAdvice接口的beforeBodyWrite 处理成功请求的响应体转换
  • 实现一个 @ExceptionHandler 来处理业务异常时,ServerException到ServerResponse的转换。

实现一个 @NoAPIResponse 注解。某些 @RestController 接口不希望实现自动包装的话,可以标记这个注解:

在ResponseBodyAdvice#supports方法
此组件是否支持给定的controller方法返回类型和所选的HttpMessageConverter类型

我们排除了标记有该注解的方法或类的自动响应体包装。
对于刚才我们实现的测试客户端client方法无需包装为APIResponse,即可标记该注解

这样我们的业务逻辑中就不需要考虑响应体的包装,代码会更简洁。

考虑接口变迁的版本策略

接口不可能一成不变,需根据业务需求不断变化内部逻辑。

若是大功能调整或重构,涉及参数定义的变化或参数废弃,导致接口无法向前兼容,这时接口就需版本概念。

版本策略最好一开始就考虑

既然接口总是要变迁,最好一开始就确定版本策略。
比如有如下实现策略:

URL Path

@GetMapping("/v1/api/user")
public int right1(){
    return 1;
}

QueryString的version参数

@GetMapping(value = "/api/user", params = "version=2")
public int right2(@RequestParam("version") int version) {
    return 2;
}

请求头中的X-API-VERSION参数

@GetMapping(value = "/api/user", headers = "X-API-VERSION=3")
public int right3(@RequestHeader("X-API-VERSION") int version) {
    return 3;
}

客户端即可在配置中处理相关版本控制的参数,有可能实现版本的动态切换。

方案对比选型

  • URL Path最直观、最不易出错
  • QueryString不易携带,不太推荐作为公开API的版本策略
  • HTTP头较无侵入性,若仅仅是部分接口需要进行版本控制,可考虑

版本实现方式

某项目需针对商品、商店和用户实现REST接口。
虽然大家约定通过URL Path方式实现API版本控制,但实现不一:

@GetMapping("/api/item/v1")
@GetMapping("/api/v1/shop")
@GetMapping("/v1/api/merchant")

显然,商品、商店和商户的接口开发没有按一致URL格式实现接口的版本控制,这时/api/v1/user/api/user/v1,这到底是一个接口还是两个?

相比于在每个接口的URL Path中设置版本号,更理想的方式是在框架层面实现统一。使用Spring按下面方式自定义RequestMappingHandlerMapping

首先,创建一个注解来定义接口的版本。@APIVersion自定义注解应用于方法或Controller上:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface APIVersion {
    String[] value();
}

然后,定义一个APIVersionHandlerMapping类继承RequestMappingHandlerMapping

RequestMappingHandlerMapping的作用,是根据类或方法上的**@RequestMapping来生成RequestMappingInfo**的实例。

重写registerHandlerMethod方法的实现,从**@APIVersion**自定义注解中读取版本信息,拼接上原有的、不带版本号的URL Pattern,构成新的RequestMappingInfo,来通过注解的方式为接口增加基于URL的版本号:

要通过实现WebMvcRegistrations接口,来生效自定义的APIVersionHandlerMapping:

这样,就实现了在Controller上或接口方法上通过注解,来实现统一的Pattern进行版本号控制:

@GetMapping(value = "/api/user")
@APIVersion("v4")
public int right4() {
    return 4;
}

使用框架来明确API版本的指定策略,不仅实现了标准化,更实现了强制的API版本控制。对上面代码略做修改,我们就可以实现不设置@APIVersion接口就给予报错提示。

接口明确同步/异步

某文件上传服务,上传接口特慢,因为该接口在内部需两步操作

  • 首先上传原图
  • 压缩后上传缩略图

如果每一步都耗时5s,则该接口返回至少需10s。
于是把接口改为异步,每步操作都限定超时时间,即分别把上传原文件和上传缩略图的操作提交到线程池,然后等待一定时间。
上传接口的请求和响应比较简单,传入二进制文件,传出原文件和缩略图下载地址。
这种实现ok吗?
接口命名虽然是同步上传操作,但其内部通过线程池进行异步上传,并因为设置了较短超时所以接口整体响应挺快。但一旦遇到超时,接口就不能返回完整的数据,不是无法拿到原文件下载地址,就是无法拿到缩略图下载地址,接口的行为变得不可预测。
所以,这种优化接口响应速度的方式并不可取,更合理的方式是,让上传接口要么完全同步处理,要么完全异步处理:

  • 同步处理,接口一定是同步上传原文件和缩略图的,调用方可自己选择调用超时,如果来得及可以一直等到上传完成,如果等不及可以结束等待,下一次重试
  • 异步处理,接口是两段式,上传接口本身只是返回一个任务ID,然后异步做上传操作,上传接口响应很快,客户端需要之后再拿着任务ID调用任务查询接口查询上传的文件URL

同步上传接口的实现,超时选择留给客户端:

接口的入参和出参DTO的命名,推荐使用接口名+Request和Response后缀

异步的上传文件接口不再返回文件URL,而是返回一个任务ID:

@Data
public class AsyncUploadRequest {
    private byte[] file;
}

@Data
public class AsyncUploadResponse {
    private String taskId;
}

把上传任务提交到线程池处理,但不会同步等待任务完成,而是完成后把结果写入一个HashMap,任务查询接口通过查询这个HashMap获得文件URL:

文件上传查询接口则以任务ID作为入参,返回两个文件的下载地址,因为文件上传查询接口是同步的,所以直接命名为syncQueryUploadTask:

改造后的FileService提供很明确的:

  • 同步上传接口syncUpload
  • 异步上传接口asyncUpload,搭配syncQueryUploadTask查询上传结果

使用方可以根据业务性质选择合适的方法:

  • 如果是后端批处理使用,那么可使用同步上传,多等待一些时间问题不大
  • 如果是面向用户的接口,那么接口响应时间不宜过长,可以调用异步上传接口,然后定时轮询上传结果,拿到结果再显示

总结

第一,针对响应体的设计混乱、响应结果的不明确问题,服务端需要明确响应体每一个字段的意义,以一致的方式进行处理,并确保不透传下游服务的错误。

第二,针对接口版本控制问题,主要就是在开发接口之前明确版本控制策略,以及尽量使用统一的版本控制策略两方面。

第三,针对接口的处理方式,我认为需要明确要么是同步要么是异步。如果API列表中既有同步接口也有异步接口,那么最好直接在接口名中明确。

一个良好的接口文档不仅需说明如何调用接口,更需要补充接口使用的最佳实践以及接口的SLA标准。
太多接口文档只给参数定义,但诸如幂等性、同步异步、缓存策略等看似内部实现相关的一些设计,其实也会影响调用方对接口的使用策略,最好也体现在接口文档。

对于服务端出错时是否返回200响应码,从RESTful设计原则看,应该尽量利用HTTP状态码表达错误,但现实都不是这么绝对。

如果认为HTTP 状态码是协议层面的契约,那当这个错误已经不涉及HTTP协议时(即服务端已收到请求进入服务端业务处理后产生的错误),不一定需要硬套协议本身的错误码。
但涉及非法URL、非法参数、没有权限等无法处理请求的情况,还是应该使用正确的响应码来应对。

最后

以上就是自由哈密瓜为你收集整理的原来阿里华为等大厂都是这么设计微服务接口的!接口的响应要明确接口的处理结果考虑接口变迁的版本策略接口明确同步/异步总结的全部内容,希望文章能够帮你解决原来阿里华为等大厂都是这么设计微服务接口的!接口的响应要明确接口的处理结果考虑接口变迁的版本策略接口明确同步/异步总结所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(50)

评论列表共有 0 条评论

立即
投稿
返回
顶部