因工作需要,最近需要做webapi接口,领导强烈要求使用RESTful的软件架构风格,所以研究了一下RESTful,本文小结了RESTful的设计规范,也并不全面,主要是总结了下项目所需的规范。
1、URI规范说明
URI 表示资源,资源一般对应服务器端领域模型中的实体类。
URI规范
- 不用大写;
- 用中杠-不用下杠_;
- 参数列表要encode;
- URI中的名词表示资源集合,使用复数形式。
高级别的模式
http(s)://server.com/app-name/{version}/{domain}/{rest-convention}
这里,{version}代表api的版本信息。{domain}是一个你可以用来定义任何技术的区域(例如:安全-允许指定的用户可以访问这个区域。)或者业务上的原因。(例如:同样的功能在同一个前缀之下。)
资源集合 vs 单个资源
URI表示资源的两种方式:资源集合、单个资源。
资源集合:
/categories //所有分类
/categories/1/products //id为1的分类下的所有商品
单个资源:
/categories/1 //id为1的分类
/categories/1;2;3 //id为1,2,3的分类
避免层级过深的URI
/在url中表达层级,用于按实体关联关系进行对象导航,一般根据id导航。
过深的导航容易导致url膨胀,不易维护,如 GET /categories/1/areas/3/products/4,尽量使用查询参数代替路径中的实体导航,如GET /products?category=1&area=3;
2、Request
HTTP方法
通过标准HTTP方法对资源CRUD
GET(查询):
GET /categories //所有分类
GET /categories/1 //id为1的分类
GET /categories/1/products //id为1的分类下的所有商品
POST(创建单个资源。POST一般向“资源集合”型uri发起):
POST /products //新增商品
POST /categories/1/properties //id为1的分类下创建属性
PUT(更新单个资源(全量),客户端提供完整的更新后的资源。与之对应的是 PATCH,PATCH 负责部分更新,客户端提供要更新的那些字段。PUT/PATCH一般向“单个资源”型uri发起):
PUT /products/1 //修改id为1的商品
PUT /categories/1 //修改id为1的分类
DELETE(删除):
DELETE /products/1 //删除id为1的商品
DELETE /categories/1/products/1;2;3 //删除id为1的分类下id为1,2,3的商品
DELETE /categories/1/properties //删除id为1的分类下所有属性
安全性和幂等性
- 安全性:不会改变资源状态,可以理解为只读的;
- 幂等性:执行1次和执行N次,对资源状态改变的效果是等价的。
| 安全性 | 幂等性 |
GET | √ | √ |
POST | × | × |
PUT | × | √ |
DELETE | × | √ |
安全性和幂等性均不保证反复请求能拿到相同的response。以 DELETE 为例,第一次DELETE返回200表示删除成功,第二次返回404提示资源不存在,这是允许的。
复杂查询
查询可以捎带以下参数:
| 示例 | 备注 |
过滤条件 | ?type=1&age=16 | 允许一定的uri冗余,如 /zoos/1 与 /zoos?id=1 |
排序 | ?sort=age,desc | |
投影 | ?whitelist=id,name,email | |
分页 | ?limit=10&offset=3 | |
3、代码实现
此处使用c#语言实现,vs创建webapi项目,
本次项目是做一个定时任务的管理系统,要为任务的增、删、查、改、启动、停止、禁用、启用提供接口,
通过路由达到 /tasks/1 这种效果,如:
[HttpGet]
[Route("tasks/{id:long}")]
[ProducesResponseType(typeof(Model.Task), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> GetTaskById(long id)
{
if (id <= 0)
{
return BadRequest();
}
var task = await _taskRepository.GetTaskAsync(id);
if (task != null)
{
return Ok(task);
}
return NotFound();
}
URI一般使用名词,一些操作以子资源对待,而子资源可以用数据库表的字段表示,如任务的启动、停止可以这么写,这里任务的运行状态为"RunStatus":
/// <summary>
/// 启动任务
/// </summary>
[Route("tasks/{id}/runstatus")]
[HttpPut]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> StartTask(long id)
{
await _taskRepository.StartTaskAsync(id);
return Ok();
}
/// <summary>
/// 停止任务
/// </summary>
[Route("tasks/{id}/runstatus")]
[HttpDelete]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> StopTask(long id)
{
await _taskRepository.StopTaskAsync(id);
return NoContent();
}
Controller部分代码
[Produces("application/json")]
[Route("api/v1")]
public class TasksController : Controller
{
private ITaskRepository _taskRepository;
public TasksController(ITaskRepository taskRepository)
{
_taskRepository = taskRepository;
}
/// <summary>
/// 获取任务分页列表
/// </summary>
[Route("tasks")]
[HttpGet]
[ProducesResponseType(typeof(PageListModel<Model.Task>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Tasks(string name, string group, [FromQuery]int pageIndex = 1, [FromQuery]int pageSize = 10)
{
var list = await _taskRepository.GetTaskPageList(name, group, pageIndex - 1, pageSize);
return Ok(list);
}
/// <summary>
/// 添加任务
/// </summary>
[Route("tasks")]
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.Created)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> CreateTask([FromBody]Model.Task task)
{
await _taskRepository.AddTask(task);
return CreatedAtAction(nameof(GetTaskById), new { id = task._id }, null);
}
/// <summary>
/// 获取任务详情
/// </summary>
[HttpGet]
[Route("tasks/{id:long}")]
[ProducesResponseType(typeof(Model.Task), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> GetTaskById(long id)
{
if (id <= 0)
{
return BadRequest();
}
var task = await _taskRepository.GetTaskAsync(id);
if (task != null)
{
return Ok(task);
}
return NotFound();
}
/// <summary>
/// 更新任务
/// </summary>
[Route("tasks")]
[HttpPut]
[ProducesResponseType((int)HttpStatusCode.Created)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> UpdateTask([FromBody]Model.Task task)
{
await _taskRepository.UpdateTaskAsync(task);
return CreatedAtAction(nameof(GetTaskById), new { id = task._id }, null);
}
/// <summary>
/// 删除任务
/// </summary>
[Route("tasks/{id:long}")]
[HttpDelete]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> DeleteTask(long id)
{
await _taskRepository.DeleteTaskAsync(id);
return NoContent();
}
/// <summary>
/// 启动任务
/// </summary>
[Route("tasks/{id}/runstatus")]
[HttpPut]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> StartTask(long id)
{
await _taskRepository.StartTaskAsync(id);
return Ok();
}
/// <summary>
/// 停止任务
/// </summary>
[Route("tasks/{id}/runstatus")]
[HttpDelete]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> StopTask(long id)
{
await _taskRepository.StopTaskAsync(id);
return NoContent();
}
/// <summary>
/// 启用任务
/// </summary>
[Route("tasks/{id}/status")]
[HttpPut]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> EnableTask(long id)
{
await _taskRepository.EnableTaskAsync(id);
return Ok();
}
/// <summary>
/// 禁用任务
/// </summary>
[Route("tasks/{id}/status")]
[HttpDelete]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> DisableTask(long id)
{
await _taskRepository.DisableTaskAsync(id);
return NoContent();
}
}
运行效果截图(此部分界面使用Swagger,相关知识请自行学习)
4、参考文档
- Restful API 的设计规范
- RESTful 接口规范