明确接口的设计逻辑,即可实现下单服务的服务端和客户端来模拟
首先,实现服务端的逻辑:
客户端按流程图逻辑实现,模拟下单场景:
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头较无侵入性,若仅仅是部分接口需要进行版本控制,可考虑