# openapi-example **Repository Path**: conncity/openapi-example ## Basic Information - **Project Name**: openapi-example - **Description**: Open API是系统将自身核心能力对外提供的重要方式,良好的API设计不仅让外部更易用,也能帮助理清系统边界;同时也是一个公司技术水平直接的外部体现,需要体现我们专业性。openapi-example项目提供了一个简单的会员服务,包括会员和卡券两个功能,该项目主要是为了描述一个典型的微服务+Open API的项目结构。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 3 - **Created**: 2021-12-18 - **Last Updated**: 2021-12-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Open API 设计原则与开发实践 ## 概述 Open API是系统将自身核心能力对外提供的重要方式,良好的API设计不仅让外部更易用,也能帮助理清系统边界; 同时也是一个公司技术水平直接的外部体现,需要体现我们专业性。 因为要和外部交互,所以API一定要体现明确的业务含义,有两个基本要求: 1. 有明确业务含义的URL:每个功能一个URL,不同功能的URL不能共用。
例如:
1) inv.increase(10)与inv.decrease(10)优于inv.change(+/-10);
2) product.activate()与product.deactivate()优于product.setStatus(0/1);
3) execute(action: String, data: Object) , action有submit,audit,close...
需要每个action都提供一个Open API,修改为submit(id: OrderId), audit(id: OrderId, opinion: String), close(id: OrderId)。
PS: 如果内部状态转移比较复杂,请考虑使用状态机实现。 2. 强类型参数和返回值,推荐采用DTO(Data Transfer Object, 数据传输对象)。
DTO字段根据API需要确定,与业务实体字段个数不一致,类型也可能不同。
注意:与外界交互使用简单类型和DTO,不要对外暴露领域模型对象,暴露领域模型对象等于领域模型与技术实现耦合,例如序列化、getter与setter。 ## 术语 - OCP: 开闭原则,classes should be closed for modification but open for extension. - DIP: 依赖反转原则,六边形架构必须 - SRP: 单一职责原则,职责明确不含糊,没有二义性 - REP: 重用发布等价原则,对于一个可重用(能供其它用户或系统使用)的元素(组件,类,类群等),应该承诺新版本能够兼容旧版本,否则用户将拒绝使用该元素。 - SAP: 越抽象越稳定,对外发布的是抽象的(接口);保证服务使用方稳定。 - SDP: 朝着稳定的方向进行依赖会比较稳定;服务使用方仅依赖稳定抽象的服务契约。 - Conway's Law: 康威定律,系统架构等同于组织架构,微服务划分跟团队划分保持一致。 - Postel's Law: 又名Robustness Principle,be conservative in what you do, be liberal in what you accept from others. 严于律己 宽以待人 ## Open API设计原则 1. 有明确业务价值(Valuable) - API定义来源于需求分析,不能随意定义,对外提供的API都需要有需求来源,并且合理; - API需要体现明确业务含义,强调业务表述能力,能够真正产生客户价值。 2. 高内聚、低耦合 - API保证单一职责(SRP); - API抽象性,对外不能暴露内部实现细节,比如元数据、实体结构 - API参数使用强类型,不能使用万能接口。强类型有利于限定能力边界,实现契约式设计。 3. 健壮稳定(Robust) - 支持限流:对单用户单API访问数和固定时间API访问总数可以进行控制,超过阈值(Threshold)进行拦截,保证突发流量下的稳定性; - 支持熔断:后端服务压力大时引发熔断,通过不同策略返回默认值、返回缓存结果、抛出异常; - 分页查询:查询时需要指定页码和页大小,页大小有上限(Threshold),不指定时走默认值,利用分页限制每次API调用传输内容的大小; - 幂等性:通过在客户端传入traceId和当前时间戳,并在服务端进行验证来保证API调用幂等性,多次调用返回同样结果; - 无状态:API服务无状态,可以根据压力弹性扩展(Scalability); - 压力测试:在上线前要进行API压力测试(Load Test),明确可支撑的并发能力以及接口服务等级,并据此设置API治理策略。 - 遵守鲁棒性原则(Postel's Law):对外提供能力时,应尽可能最小且符合要求。 4. 向下兼容(Compatibility) - 遵守开闭原则(OCP),API对扩展开放,对修改关闭。可以新增接口,但不允许修改接口。枚举类型的枚举值只能增加,不能减少; - 遵守发布重用等价原则(REP); - 版本化管理(Version),不同版本的接口可以共存,彼此使用版本号隔离,在URL中体现版本信息,同一版本传入传出参数结构不能变化; 5. 安全设计(Security) - 数据脱敏:敏感数据需要经过脱敏处理,对于必须提供的敏感数据,通过数据映射方式,提供映射后的值。 - API访问需要经过认证授权,开放平台对传输的数据进行加签,接口提供方对请求进行验签,验证请求的合法性,只有通过验证的才能发起调用; - 为确保交互数据的传输安全,Open API的访问必须通过**HTTPS**协议; - 其他安全机制包括密码加密、黑白名单、身份认证限制时效、验证失败冻结等,这些功能一般由统一网关负责。 6. 标准化(Standardization) - 采用标准HTTP Restful风格和JSON数据格式,尽可能便于对接; - 统一进行本地化和国际化的处理,时区、排序等需一致; - 统一数据格式和命名规范,包括时间格式、租户id,组织id、返回格式等; - 统一异常处理机制、容错机制; - API遵循统一的流程规范,统一通过用友云开放平台对外发布和运营。 7. 易用性(Ease of development) - 面向开发者,提供完善的API文档,包括请求说明,入参出参说明,以及请求值、正确和错误返回值的示例等; - 提供 API 测试功能,开发者可以测试接口的连通性以及正确性; - 通过开放平台入口提供所有API的快速检索和学习开发。 8. Restful - URL格式 **https://api.diwork.com/版本/产品线/领域/业务操作** - 请求方式: GET 代表获取;POST 代表新增;PUT 代表更新;DELETE 代表删除,PUT和DELETE可通过POST实现。 - 请求简单数据(如id)放在URL路径中,复杂数据放在Http Body中,聚合对象通过聚合根访问(如order/1001/details); - 使用Http状态码识别服务状态,在调用API时,要求只要服务端接收到http请求,就不能返回除200以外的 HTTP 响应状态码; - 返回值DTO包含合适的字段集合,不要过多返回字段,可以在返回DTO中冗余详细状态码,针对错误时,不同的错误提供不通的错误码,便于快速定位错误; - 如果记录数量很多,服务器不可能都将它们返回给用户,API应该提供参数,过滤返回结果。查询谓词 limit,offset,page,sortby,order,pagesize,放到URL参数中。 9. 组合性(Composability) - 通过Facade模式,暴露粗粒度服务接口。!注意,粗粒度服务接口不违反SRP。 - 粒度服务接口可以由多个内部服务共同合作完成。!注意,合作完成一个任务不等同于服务编排,不要涉及事务。 ## 常见问题 1. Open API与Web UI所需功能相似,可以使用相同的HTTP适配器吗? - 不能!
![微服务典型架构](images/ms-base.png) - 由于Open API对外提供服务,要严格遵守OCP和鲁棒性原则、涉及兼容性和多版本、涉及数据脱敏,这些方面对Web UI影响并不大,Web UI可以随着微服务一同升级(非浏览器内核的客户端除外)。 - 两者入参和返回值结构一般不同,Open API强调提供最小集合的能力和数据,Web UI一般需要更多的数据。 - 熔断、限流等策略不同,阈值(Threshold)不同。 2. 版本化管理(Version)的版本由谁负责(API Gateway or Microservices)解析实现? - 看场景。
版本化URL格式1 https://api.diwork.com/版本/产品线/领域/业务操作
版本化URL格式2 https://api.diwork.com/产品线/领域/版本/业务操作
格式1时,一般由API Gateway负责路由到不同版本的微服务实例,这时每个版本最少有一个微服务实例。 - 方式1
格式2时,一般由微服务自身实现多个版本兼容,同一个微服务实例中有多个版本的API实现。 - 方式2
方式1会增加运维的复杂度,当资源不容易申请时,不要采用该模式;对于代码开发,会增加多个版本分支。
方式2对运维和开发,易于实现。格式1也可以由API Gateway组织成格式2路由到微服务实例(方式2)。
变更较小时,方式2合适;变更较大时,方式1合适。大版本间采用方式1,小版本间采用方式2。 ## 在微服务中实现 微服务(Microservices)可以通过消息、RPC、REST方式对外提供能力,而通过开放平台对外部ISV等提供能力时,需要通过Open API来实现。 Open API与Microservices之间的关系是: 1. Open API为Microservices的一类适配器,同消息、数据库访问、WEB等一样,围绕核心领域模型(Domain-Model)与外部交互。 2. Microservices的领域模型(Domain-Model)提供业务核心能力,该能力可以为各个适配器复用,包括Open API。 该项目提供了一个简单的会员服务,包括会员和卡券两个功能,该项目主要是为了描述一个典型的微服务+Open API的项目结构。 ``` member (会员服务) ├── adapters (适配器) │   ├── dto │   ├── repository (数据访问适配器) │   │   └── orm (数据库访问适配器) │   │   └── dao │   └── openapi (Open API适配器) │   ├── aop │   ├── controller │   └── dto ├── config (如果domainmodel与port&adapters分为多个Module,编译后为多个jar,这时需要增加一个装配(assemble)Module,该Module装配domainmodel与adapters组件,config位于装配模块中) ├── domainmodel (核心领域模型) │   ├── dto │   ├── entities │   │   ├── coupon │   │   └── member │   ├── exceptions │   ├── repositories (DIP,需要外部提供实现) │   ├── services │   │   └── impl │   └── vo ├── exceptions └── libs(一般引入外部类库) ├── key └── util ``` ## Open API说明及测试 通过引入Swagger,将自动形成Open API说明及测试网页,[项目测试地址](http://imeta.yonyouup.com/openapi/),Swagger核心要素有: |要素|说明|示例| |---|---|---| |@Api|Open API接口声明|@Api(tags = "会员档案服务接口")| |@ApiOperation|Open API方法声明|@ApiOperation(value = "根据手机号码查找会员", httpMethod = "GET")| |@ApiParam|参数声明|@ApiParam(name = "phone", value = "手机号码", required = true)| |@ApiResponse|返回值声明|@ApiResponse(code = 200, message = "会员信息", response = Member.class)| |@ApiModel|模型实体声明|@ApiModel(description = "会员档案")| |@ApiModelProperty|模型属性声明|@ApiModelProperty(value = "会员名称")| # OpenAPI 说明文档编写 ## OpenAPI 相关代码 ```java @Api(tags = "会员积分") @RestController @RequestMapping("/member/points") public class MemberPointController { @Autowired MemberService memberService; @PostMapping("/v1/history/query") @ApiOperation(value = "会员积分变更明细查询", httpMethod = "POST") public ApiResponseDto> inbound(@RequestBody MemberPointQueryConditionDto conditionDto, HttpServletRequest request) { ServiceContext serviceContext = createServiceContext(request); MemberId memberId = conditionDto.getMemberId(); TimeRange timeRange = conditionDto.getTimeRange(); PageQueryResult result = memberService.queryMemberPointHistory(conditionDto, serviceContext); return new ApiResponseDto(true).setData(result); } } ``` ```java @ApiModel(description = "会员积分变更明细查询条件") public class MemberPointQueryConditionDto { @ApiModelProperty(value = "会员ID") @lombok.Setter private String memberId; @ApiModelProperty(value = "变更时间范围") @lombok.Setter private TimeRangeDto changeTime; @ApiModelProperty(value = "分页信息") @lombok.Getter @lombok.Setter private PageInfo pageInfo; } ``` ```java @ApiModel(description = "优惠券") @lombok.Data public class MemberPointHistoryDto { @ApiModelProperty(value = "启用金额") private String memberId; @ApiModelProperty(value = "启用金额") private Integer changePoints; @ApiModelProperty(value = "启用金额") private LocalDateTime changeTime; } ``` ```java @ApiModel(description = "分页查询结果") @lombok.Data public class PageQueryResult { @ApiModelProperty(value = "数据集合") private List data; @ApiModelProperty(value = "当前页") private Integer pageIndex; @ApiModelProperty(value = "页大小") private Integer pageSize; @ApiModelProperty(value = "总页数") private Integer pageCount; } ``` ```java @ApiModel(description = "返回结果") @lombok.Data public class ApiResponseDto { @ApiModelProperty(value = "状态码,200表示成功,非200表示错误码") private String code; @ApiModelProperty(value = "提示信息") private String message; @ApiModelProperty(value = "返回数据") private T data; } ``` ## OpenAPI 说明文档示例 - 名称: 会员积分变更明细查询 - 场景描述: 查询一个会员在一定时间范围内的积分变更情况。 - 发布地址: https://yourdomain.com/member/points/v1/history/query - 请求参数 |名称|类型|数组|位置|描述|默认值|必填| |---|---|---|---|---|---|---| |mid|string|否|Body|会员ID| |是| |+changeTime|object|否|Body|变更日期范围| |是| |   fromTime|datetime|否|Body|从时间| |是| |   toTime|datetime|否|Body|到时间| |是| - 请求示例 ```json { "mid": "199", "changeTime": { "fromTime": "2015-01-21 00:00:00", "toTime": "2016-01-21 23:59:59" } } ``` - 返回值 |名称|类型|数组|描述|说明| |---|---|---|---|---| |code|int|否|状态码|200表示成功,非200表示失败,999的表示未知异常,业务错误码参见错误码| |+data|object|否|查询结果|| |   +recordList|object|是|数据集合|| |      mid|string|否|会员ID|| |      changePoints|int|否|变更的积分|| |      changeTime|datetime|否|变更时间|| |   pageIndex|int|否|当前页|分页信息| |   pageSize|int|否|页大小|分页信息| |   pageCount|int|否|总页数|分页信息| - 返回值示例 ```json { "data": "200", "data": { "recordList": [{ "mid": "199", "changePoints": "10", "changeTime": "2015-08-12 18:21:02" }], "pageIndex": 1, "pageSize": 10, "pageCount":1 } } ```