因工作需要,最近需要做webapi接口,领导强烈要求使用RESTful的软件架构风格,所以研究了一下RESTful,本文小结了RESTful的设计规范,也并不全面,主要是总结了下项目所需的规范。

1、URI规范说明

URI 表示资源,资源一般对应服务器端领域模型中的实体类。

URI规范

  1. 不用大写;
  2. 用中杠-不用下杠_;
  3. 参数列表要encode;
  4. 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. 安全性:不会改变资源状态,可以理解为只读的;
  2. 幂等性:执行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,相关知识请自行学习)

restful接口介绍 restful 接口规范_幂等性

4、参考文档

  1. Restful API 的设计规范
  2. RESTful 接口规范