6.1 RESTful API 设计原则


文档摘要

6.1 RESTful API 设计原则 第六章:Web API 开发 6.1 RESTful API 设计原则 在现代Web应用程序开发中,API(应用程序编程接口)扮演着至关重要的角色。它们是不同系统之间沟通的桥梁,使得数据和服务能够跨平台、跨语言地进行交互。特别是在微服务架构和前后端分离的趋势下,构建高效、可维护、易于理解的API显得尤为重要。RESTful API 作为一种架构风格,因其简洁、可扩展和易于被开发者接受的特点,成为了Web API 设计的首选方案。 6.1.1 理解 RESTful 架构风格 REST (Representational State Transfer) 并非一种具体的协议或标准,而是一种架构风格,它定义了一组用于构建分布式系统的约束条件和最佳实践。

## 6.1 RESTful API 设计原则 ## 第六章:Web API 开发 ### 6.1 RESTful API 设计原则 在现代Web应用程序开发中,API(应用程序编程接口)扮演着至关重要的角色。它们是不同系统之间沟通的桥梁,使得数据和服务能够跨平台、跨语言地进行交互。特别是在微服务架构和前后端分离的趋势下,构建高效、可维护、易于理解的API显得尤为重要。RESTful API 作为一种架构风格,因其简洁、可扩展和易于被开发者接受的特点,成为了Web API 设计的首选方案。 #### 6.1.1 理解 RESTful 架构风格 REST (Representational State Transfer) 并非一种具体的协议或标准,而是一种架构风格,它定义了一组用于构建分布式系统的约束条件和最佳实践。RESTful API 遵循这些原则,旨在创建可伸缩、松耦合、易于演进的Web服务。理解 RESTful 的核心思想是构建符合互联网特性的应用,充分利用 HTTP 协议的语义,实现资源的高效管理和交互。 RESTful 架构风格强调以下六大核心原则(有时也认为是五大原则,将代码按需执行(Code-On-Demand)视为可选原则,但在实际Web API开发中通常不涉及,因此我们主要关注前五个核心原则): 1. **客户端-服务器 (Client-Server)** 2. **无状态 (Stateless)** 3. **可缓存 (Cacheable)** 4. **分层系统 (Layered System)** 5. **统一接口 (Uniform Interface)** 下面我们将逐一深入解析这些原则,并结合 ASP.NET Web API 代码示例进行说明。 #### 6.1.2 客户端-服务器 (Client-Server) **原则详解:** 客户端-服务器架构的核心思想是关注点分离。客户端和服务器端必须是独立演化的,互不依赖。客户端负责用户界面和用户体验,服务器端负责数据存储、业务逻辑和资源管理。这种分离使得两端可以独立开发、部署和扩展,提高了系统的灵活性和可维护性。 * **客户端职责:** 负责发起请求,处理用户交互,渲染服务器返回的表示。客户端不应关心数据存储的细节,也不应包含业务逻辑。 * **服务器职责:** 负责接收和处理客户端请求,管理资源,执行业务逻辑,并返回资源的表示。服务器不应关心客户端的用户界面和用户体验。 **ASP.NET 代码实践:** 在 ASP.NET Web API 中,Controller 类扮演着服务器端的角色,而前端应用程序(如 React, Angular, Vue.js 等)或移动应用则充当客户端。 ```csharp // 服务器端 (ASP.NET Web API Controller) [ApiController] [Route("api/products")] public class ProductsController : ControllerBase { private readonly IProductRepository _productRepository; public ProductsController(IProductRepository productRepository) { _productRepository = productRepository; } [HttpGet] public async Task>> GetProducts() { var products = await _productRepository.GetProductsAsync(); return Ok(products); } } // 客户端 (假设是 JavaScript 代码) async function fetchProducts() { const response = await fetch('/api/products'); const products = await response.json(); console.log(products); // ... 处理 products 数据,例如渲染到页面上 } fetchProducts(); ``` **代码解释:** * `ProductsController` 是服务器端 Controller,负责处理 `/api/products` 路径的 GET 请求,从 `_productRepository` 获取商品数据并返回。 * 客户端 JavaScript 代码使用 `fetch` API 发送 GET 请求到 `/api/products`,接收服务器返回的 JSON 格式的商品数据,并在客户端进行处理。 **Mermaid 图示:** ```mermaid graph TD subgraph Client A[Client Application] --> B(Request: GET /api/products) end subgraph Server B --> C[ProductsController] C --> D{Product Repository} D --> E[Database] E --> D D --> F(Response: JSON Products) F --> C end C --> A style A fill:#f9f,stroke:#333,stroke-width:2px style E fill:#ccf,stroke:#333,stroke-width:2px ``` **图示解释:** * 客户端应用发起请求 (`Request: GET /api/products`)。 * 请求到达服务器端的 `ProductsController`。 * `ProductsController` 调用 `Product Repository` 从数据库获取数据。 * 数据库返回数据给 `Product Repository`。 * `Product Repository` 返回商品数据给 `ProductsController`。 * `ProductsController` 将数据封装成 JSON 格式响应 (`Response: JSON Products`) 返回给客户端。 * 客户端应用接收并处理响应数据。 **总结:** 客户端和服务器端职责明确分离,各自独立演化,符合客户端-服务器原则。 #### 6.1.3 无状态 (Stateless) **原则详解:** 无状态原则要求服务器端不应该存储任何关于客户端请求的状态信息。每个请求都必须包含服务器处理请求所需的所有信息。服务器将请求视为独立的事务,处理完成后不保留任何上下文信息。客户端负责维护自己的状态,并在后续请求中传递必要的状态信息。 * **优点:** * **可伸缩性:** 服务器无需维护会话状态,可以更容易地进行水平扩展,负载均衡器可以将请求路由到任何服务器实例。 * **可靠性:** 服务器故障不会影响客户端状态,客户端可以重新发送请求到其他服务器实例。 * **可见性:** 每个请求都是自包含的,易于监控和调试。 * **实现无状态:** * 避免使用服务器端会话 (Session) 或 Cookie 来存储客户端状态。 * 将所有必要的状态信息包含在请求头、请求体或 URI 中。 * 使用身份验证令牌 (如 JWT) 在客户端存储身份验证信息,并在每个请求中发送。 **ASP.NET 代码实践:** 在 ASP.NET Web API 中,默认情况下 Controller 是无状态的。我们应该避免在 Controller 中使用 Session 或静态变量来存储请求状态。 ```csharp // 无状态的 Controller [ApiController] [Route("api/orders")] public class OrdersController : ControllerBase { private readonly IOrderService _orderService; public OrdersController(IOrderService orderService) { _orderService = orderService; } [HttpPost] public async Task CreateOrder([FromBody] CreateOrderRequest request) { // request 对象包含了创建订单所需的所有信息 var orderId = await _orderService.CreateOrderAsync(request); return CreatedAtAction(nameof(GetOrder), new { id = orderId }, null); } [HttpGet("{id}")] public async Task> GetOrder(int id) { var order = await _orderService.GetOrderAsync(id); if (order == null) { return NotFound(); } return Ok(order); } } public class CreateOrderRequest { public int CustomerId { get; set; } public List OrderItems { get; set; } // ... 其他订单信息 } ``` **代码解释:** * `CreateOrder` Action 接收 `CreateOrderRequest` 对象,该对象包含了创建订单所需的所有信息,例如 `CustomerId` 和 `OrderItems`。服务器端不需要维护任何关于客户端会话或之前请求的状态。 * `GetOrder` Action 通过 URI 参数 `{id}` 获取订单 ID,服务器端根据 ID 查询订单信息,不依赖于之前的任何请求状态。 **Mermaid 图示:** ```mermaid graph TD subgraph Client A[Request 1: POST /api/orders] --> B(Request 2: GET /api/orders/123) end subgraph Server C[OrdersController] --> D{OrderService} E[OrdersController] --> F{OrderService} end A --> C B --> E style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#f9f,stroke:#333,stroke-width:2px ``` **图示解释:** * 客户端发送两个独立的请求:`POST /api/orders` 创建订单和 `GET /api/orders/123` 获取订单。 * 服务器端的 `OrdersController` 处理每个请求时,都将其视为独立的事务,不依赖于之前的请求。 * 每个请求都包含了服务器处理请求所需的所有信息。 **总结:** 服务器端不存储客户端状态,每个请求都是独立的,符合无状态原则。 #### 6.1.4 可缓存 (Cacheable) **原则详解:** 可缓存原则要求响应可以被客户端或中间代理(如 CDN, 反向代理)缓存,以减少服务器负载,提高响应速度。缓存可以减少网络延迟,提高用户体验,并降低服务器成本。 * **缓存控制:** * 服务器应该在响应头中包含缓存控制信息,例如 `Cache-Control` 和 `Expires`,指示响应是否可以被缓存,以及缓存的时长。 * 客户端和中间代理应该根据响应头中的缓存控制信息来决定是否缓存响应。 * **缓存失效:** * 缓存应该有失效机制,避免客户端一直使用过时的数据。 * 可以使用 `ETag` 或 `Last-Modified` 响应头进行条件请求,只有当资源发生变化时才返回新的响应。 **ASP.NET 代码实践:** ASP.NET Web API 提供了多种方式来控制缓存行为。 ```csharp // 可缓存的 GET 请求 [ApiController] [Route("api/products")] public class ProductsController : ControllerBase { private readonly IProductRepository _productRepository; public ProductsController(IProductRepository productRepository) { _productRepository = productRepository; } [HttpGet] [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)] // 启用客户端和服务器端缓存 60 秒 public async Task>> GetProducts() { var products = await _productRepository.GetProductsAsync(); return Ok(products); } [HttpGet("{id}")] [ResponseCache(Duration = 30, Location = ResponseCacheLocation.Client, VaryByHeader = "Accept-Encoding")] // 仅客户端缓存 30 秒,并根据 Accept-Encoding 头部进行区分 public async Task> GetProduct(int id) { var product = await _productRepository.GetProductAsync(id); if (product == null) { return NotFound(); } return Ok(product); } } ``` **代码解释:** * `[ResponseCache]` Attribute 用于配置响应缓存行为。 * `Duration = 60` 设置缓存时长为 60 秒。 * `Location = ResponseCacheLocation.Any` 表示允许客户端和服务器端缓存。 * `Location = ResponseCacheLocation.Client` 表示只允许客户端缓存。 * `VaryByHeader = "Accept-Encoding"` 表示根据 `Accept-Encoding` 请求头进行缓存区分,例如针对压缩和非压缩版本分别缓存。 **Mermaid 图示:** ```mermaid graph TD subgraph Client A[Client Request] --> B{Cache?} B -- Cache Miss --> C[Send Request to Server] B -- Cache Hit --> D[Return Cached Response] end subgraph Server C --> E[Server Processing] E --> F[Response with Cache-Control Headers] end C --> E E --> F F --> B D --> A F --> A style A fill:#f9f,stroke:#333,stroke-width:2px ``` **图示解释:** * 客户端发起请求,首先检查本地缓存。 * 如果缓存命中 (`Cache Hit`),则直接返回缓存的响应,无需请求服务器。 * 如果缓存未命中 (`Cache Miss`),则发送请求到服务器。 * 服务器处理请求,并返回包含 `Cache-Control` 头部信息的响应。 * 客户端根据 `Cache-Control` 头部信息决定是否缓存响应。 **总结:** 通过配置 `ResponseCache` Attribute,ASP.NET Web API 响应可以被客户端和中间代理缓存,提高了性能和效率,符合可缓存原则。 #### 6.1.5 分层系统 (Layered System) **原则详解:** 分层系统架构允许系统由多层组件构成,每一层只与相邻的层交互。客户端无需知道它是否直接连接到最终服务器,或者连接到中间的代理服务器。中间层可以提高系统的可伸缩性、安全性,并简化系统复杂性。 * **层次隔离:** 每一层只与直接相邻的层交互,降低了层与层之间的耦合性。 * **透明性:** 客户端无需关心中间层的存在,请求和响应的处理对客户端是透明的。 * **可扩展性:** 可以在中间层添加新的功能,例如负载均衡、缓存、安全认证等,而无需修改客户端和后端服务器。 **ASP.NET 代码实践:** 在 ASP.NET Web API 应用中,可以构建多层架构,例如: * **表示层 (Presentation Layer):** Controller 类,负责接收和处理 HTTP 请求,返回 HTTP 响应。 * **业务逻辑层 (Business Logic Layer):** Service 类,包含业务逻辑,处理业务规则和流程。 * **数据访问层 (Data Access Layer):** Repository 类,负责数据持久化和访问数据库。 ```csharp // 表示层 (Controller) [ApiController] [Route("api/customers")] public class CustomersController : ControllerBase { private readonly ICustomerService _customerService; public CustomersController(ICustomerService customerService) { _customerService = customerService; } [HttpGet("{id}")] public async Task> GetCustomer(int id) { var customer = await _customerService.GetCustomerAsync(id); if (customer == null) { return NotFound(); } return Ok(customer); // 返回 DTO 对象,与领域模型解耦 } } // 业务逻辑层 (Service) public interface ICustomerService { Task GetCustomerAsync(int id); } public class CustomerService : ICustomerService { private readonly ICustomerRepository _customerRepository; private readonly IMapper _mapper; // 使用 AutoMapper 进行 DTO 转换 public CustomerService(ICustomerRepository customerRepository, IMapper mapper) { _customerRepository = customerRepository; _mapper = mapper; } public async Task GetCustomerAsync(int id) { var customerEntity = await _customerRepository.GetCustomerByIdAsync(id); if (customerEntity == null) { return null; } return _mapper.Map(customerEntity); // 将领域模型转换为 DTO } } // 数据访问层 (Repository) public interface ICustomerRepository { Task GetCustomerByIdAsync(int id); } public class CustomerRepository : ICustomerRepository { private readonly AppDbContext _dbContext; public CustomerRepository(AppDbContext dbContext) { _dbContext = dbContext; } public async Task GetCustomerByIdAsync(int id) { return await _dbContext.Customers.FindAsync(id); } } // DTO (Data Transfer Object) public class CustomerDto { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } // ... 其他需要暴露给客户端的属性 } // 领域模型 (Domain Model) - Customer 类可能包含更多业务逻辑和属性,但 DTO 只暴露客户端需要的 public class Customer { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } // ... 更多领域相关的属性和方法 } ``` **代码解释:** * **表示层 (CustomersController):** 负责接收 HTTP 请求,调用 `ICustomerService` 获取数据,并将 `CustomerDto` 对象作为响应返回。 * **业务逻辑层 (CustomerService):** 包含业务逻辑,调用 `ICustomerRepository` 获取领域模型 `Customer`,并使用 AutoMapper 将其转换为 `CustomerDto`。 * **数据访问层 (CustomerRepository):** 负责访问数据库,获取 `Customer` 领域模型实体。 * **DTO (CustomerDto):** 数据传输对象,用于在层之间传递数据,避免将领域模型直接暴露给客户端,实现层与层之间的解耦。 **Mermaid 图示:** ```mermaid graph TD subgraph Client A[Client Request] end subgraph Presentation Layer B[CustomersController] end subgraph Business Logic Layer C[CustomerService] end subgraph Data Access Layer D[CustomerRepository] end subgraph Database E[Database] end A --> B B --> C C --> D D --> E style A fill:#f9f,stroke:#333,stroke-width:2px style E fill:#ccf,stroke:#333,stroke-width:2px ``` **图示解释:** * 客户端请求首先到达表示层 (`CustomersController`)。 * 表示层调用业务逻辑层 (`CustomerService`)。 * 业务逻辑层调用数据访问层 (`CustomerRepository`)。 * 数据访问层与数据库 (`Database`) 交互。 * 响应数据按照相反的路径返回给客户端。 **总结:** 通过分层架构,将系统划分为表示层、业务逻辑层和数据访问层,实现了层与层之间的隔离和解耦,提高了系统的可维护性和可扩展性,符合分层系统原则。 #### 6.1.6 统一接口 (Uniform Interface) **原则详解:** 统一接口是 RESTful 架构风格的核心原则,也是区分 RESTful API 和其他 Web API 的关键所在。统一接口简化了客户端和服务器端的交互,降低了系统的复杂性,提高了系统的可扩展性和独立演化能力。统一接口包含以下四个子原则: 1. **资源标识 (Resource Identification):** 每个资源都应该通过唯一的 URI 进行标识。 2. **资源操作通过表述 (Resource Manipulation through Representations):** 客户端通过表述 (representations) 来操作资源,例如 JSON, XML 等格式。 3. **自描述消息 (Self-Descriptive Messages):** 消息本身应该包含足够的信息来描述如何处理它,例如使用媒体类型 (Media Type) 和超媒体链接 (Hypermedia Links)。 4. **超媒体作为应用状态引擎 (Hypermedia as the Engine of Application State - HATEOAS):** 服务器端应该在响应中提供超媒体链接,引导客户端进行下一步操作,实现应用状态的驱动。 下面我们分别详细解释这四个子原则,并结合 ASP.NET Web API 代码示例进行说明。 ##### 6.1.6.1 资源标识 (Resource Identification) **原则详解:** RESTful API 的核心概念是资源 (Resource)。资源是任何可以被命名的信息。在 Web API 中,资源通常代表应用程序中的实体,例如用户、商品、订单等。每个资源都应该通过唯一的 URI (Uniform Resource Identifier) 进行标识。 * **URI 设计:** * 使用名词而不是动词来表示资源,例如 `/users` 而不是 `/getUsers`。 * 使用复数名词表示资源集合,例如 `/users` 表示用户集合,`/products` 表示商品集合。 * 使用层级结构表示资源之间的关系,例如 `/users/{userId}/orders` 表示用户 ID 为 `{userId}` 的用户的订单集合。 * 避免在 URI 中包含文件扩展名,例如 `/users.json`,应该通过 `Accept` 请求头来协商媒体类型。 **ASP.NET 代码实践:** 在 ASP.NET Web API 中,使用 `[Route]` Attribute 来定义 Controller 和 Action 的 URI 路由。 ```csharp [ApiController] [Route("api/products")] // 资源集合的 URI public class ProductsController : ControllerBase { private readonly IProductRepository _productRepository; public ProductsController(IProductRepository productRepository) { _productRepository = productRepository; } [HttpGet] public async Task>> GetProducts() { // ... 获取所有商品 } [HttpGet("{id}")] // 特定资源的 URI,使用 {id} 占位符 public async Task> GetProduct(int id) { // ... 获取 ID 为 {id} 的商品 } [HttpGet("categories/{categoryId}/products")] // 子资源的 URI,表示属于特定类别的商品 public async Task>> GetProductsByCategory(int categoryId) { // ... 获取类别 ID 为 {categoryId} 的商品 } } ``` **代码解释:** * `[Route("api/products")]` 定义了 `ProductsController` 的基础 URI 为 `/api/products`,表示商品资源集合。 * `[HttpGet]` 表示处理 GET 请求,对应获取资源集合的操作。 * `[HttpGet("{id}")]` 定义了获取特定资源的 URI 路由,`{id}` 是路由参数,用于标识特定商品的 ID。 * `[HttpGet("categories/{categoryId}/products")]` 定义了获取子资源的 URI 路由,表示获取属于特定类别的商品集合。 **Mermaid 图示:** ```mermaid graph TD A[/api/products] --> B[GET: 获取所有商品] A[/api/products/{id}] --> C[GET: 获取特定商品] A[/api/products/categories/{categoryId}/products] --> D[GET: 获取特定类别商品] style A fill:#f9f,stroke:#333,stroke-width:2px ``` **图示解释:** * `/api/products` URI 用于标识商品资源集合。 * `/api/products/{id}` URI 用于标识特定商品资源。 * `/api/products/categories/{categoryId}/products` URI 用于标识特定类别下的商品资源集合。 **总结:** 通过合理设计 URI,清晰地标识资源及其之间的关系,符合资源标识原则。 ##### 6.1.6.2 资源操作通过表述 (Resource Manipulation through Representations) **原则详解:** 客户端和服务器端通过表述 (representations) 来交换资源的状态。表述是资源在特定时刻的状态快照,通常使用标准的数据格式,例如 JSON, XML, HTML 等。客户端通过 HTTP 方法 (GET, POST, PUT, DELETE) 和请求头 (例如 `Content-Type`, `Accept`) 来指定对资源的操作和期望的表述格式。 * **媒体类型协商 (Content Negotiation):** * 客户端通过 `Accept` 请求头告知服务器端期望的响应媒体类型,例如 `Accept: application/json` 表示客户端期望 JSON 格式的响应。 * 服务器端根据客户端的 `Accept` 头和自身支持的媒体类型,返回合适的表述格式,并在 `Content-Type` 响应头中告知客户端实际返回的媒体类型。 * **数据格式:** * 常用数据格式包括 JSON (JavaScript Object Notation), XML (Extensible Markup Language), HTML (HyperText Markup Language) 等。 * JSON 格式简洁易读,适合 Web API 数据交换,是目前最流行的选择。 **ASP.NET 代码实践:** ASP.NET Web API 默认支持 JSON 格式的请求和响应。可以通过配置来支持其他媒体类型,例如 XML。 ```csharp // 资源操作通过表述 [ApiController] [Route("api/products")] public class ProductsController : ControllerBase { private readonly IProductRepository _productRepository; public ProductsController(IProductRepository productRepository) { _productRepository = productRepository; } [HttpGet("{id}")] public async Task> GetProduct(int id) { var product = await _productRepository.GetProductAsync(id); if (product == null) { return NotFound(); } return Ok(_mapper.Map(product)); // 返回 JSON 格式的 ProductDto 表述 } [HttpPost] public async Task CreateProduct([FromBody] CreateProductDto productDto) // 接收 JSON 格式的 CreateProductDto 表述 { var product = _mapper.Map(productDto); await _productRepository.AddProductAsync(product); return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, null); } } ``` **代码解释:** * `GetProduct` Action 返回 `ProductDto` 对象,ASP.NET Web API 默认将其序列化为 JSON 格式的响应。 * `CreateProduct` Action 接收 `CreateProductDto` 对象,客户端需要将请求体设置为 JSON 格式,并设置 `Content-Type: application/json` 请求头。 * ASP.NET Web API 会自动将 JSON 请求体反序列化为 `CreateProductDto` 对象。 **Mermaid 图示:** ```mermaid graph TD subgraph Client A[Request: GET /api/products/123, Accept: application/json] B[Request: POST /api/products, Content-Type: application/json, Body: JSON Product Data] end subgraph Server C[Response: Content-Type: application/json, Body: JSON Product Data] D[Response: Content-Type: application/json, Body: JSON Product ID] end A --> C B --> D style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#f9f,stroke:#333,stroke-width:2px style C fill:#ccf,stroke:#333,stroke-width:2px style D fill:#ccf,stroke:#333,stroke-width:2px ``` **图示解释:** * 客户端发送 GET 请求,并指定 `Accept: application/json`,服务器返回 JSON 格式的商品数据表述。 * 客户端发送 POST 请求,并设置 `Content-Type: application/json`,请求体包含 JSON 格式的商品数据表述,服务器返回 JSON 格式的商品 ID 表述。 **总结:** 通过媒体类型协商和标准数据格式,客户端和服务器端通过表述来交换资源状态,符合资源操作通过表述原则。 ##### 6.1.6.3 自描述消息 (Self-Descriptive Messages) **原则详解:** 自描述消息原则要求消息本身应该包含足够的信息来描述如何处理它。这主要通过以下两个方面来实现: * **媒体类型 (Media Type):** 使用 `Content-Type` 和 `Accept` 头部来指定消息的媒体类型,例如 `application/json`, `application/xml`, `text/html` 等。媒体类型告知客户端如何解析和处理消息体。 * **超媒体链接 (Hypermedia Links):** 在响应中包含超媒体链接,引导客户端进行下一步操作。超媒体链接可以包含资源的 URI、允许的操作、以及相关资源的链接。 **ASP.NET 代码实践:** ASP.NET Web API 默认支持 JSON 格式,并在响应头中设置 `Content-Type: application/json`。可以通过自定义 Action Result 来添加超媒体链接。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U