# openapi-example
**Repository Path**: jonathanzyf/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**: 3
- **Forks**: 3
- **Created**: 2020-11-05
- **Last Updated**: 2024-11-06
## 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适配器吗? - 不能!

- 由于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
}
}
```