📋 API设计
一、API设计概述
API(Application Programming Interface)是应用程序接口的简称,它定义了软件组件之间的交互方式。良好的API设计是构建可维护、可扩展系统的基础,尤其在微服务架构中,API设计的质量直接影响到系统的性能、可用性和可维护性。
1.1 API设计的重要性
- 简化集成:良好的API设计可以简化系统集成过程,降低集成成本
- 提高可维护性:清晰的API设计可以提高系统的可维护性,便于团队协作
- 促进复用:标准化的API设计可以促进代码和功能的复用
- 提升用户体验:对于外部API,良好的设计可以提升开发者的使用体验
- 支持演进:良好的API设计可以支持系统的持续演进,避免破坏性变更
- 降低风险:标准化的API设计可以降低系统的复杂性和风险
1.2 API的类型
根据不同的分类标准,API可以分为多种类型:
1.2.1 按访问方式分类
- 公开API:对外部开发者开放的API,通常需要认证和授权
- 内部API:组织内部使用的API,通常用于微服务间通信
- 合作伙伴API:为特定合作伙伴提供的API,通常有特殊的访问控制
1.2.2 按通信协议分类
- REST API:基于HTTP协议的RESTful API
- GraphQL API:基于GraphQL查询语言的API
- gRPC API:基于gRPC框架的高性能API
- WebSocket API:支持双向通信的WebSocket API
- SOAP API:基于SOAP协议的传统Web服务API
1.2.3 按功能分类
- 业务API:提供业务功能的API
- 数据API:提供数据访问功能的API
- 系统API:提供系统级功能的API(如认证、日志等)
- 集成API:用于系统集成的API
1.3 API设计的核心原则
- 简洁性:API应该简洁明了,易于理解和使用
- 一致性:API的命名、格式、参数等应该保持一致
- 可用性:API应该稳定可靠,具有良好的可用性
- 安全性:API应该具有完善的安全机制,保护数据和系统安全
- 可扩展性:API应该具有良好的可扩展性,支持未来的功能扩展
- 向后兼容性:API的变更应该保持向后兼容,避免影响现有用户
- 文档化:API应该有详细的文档,便于开发者使用和理解
二、RESTful API设计
REST(Representational State Transfer)是一种软件架构风格,它定义了一组约束条件和原则,用于创建Web服务。RESTful API是符合REST架构风格的API。
2.1 RESTful API的核心原则
- 资源导向:将系统中的实体抽象为资源,每个资源都有唯一的标识符(URI)
- 无状态:服务器不保存客户端的状态,每个请求都包含完整的信息
- 统一接口:使用统一的接口进行资源操作,包括GET、POST、PUT、DELETE等HTTP方法
- 表现层:资源的表现形式与资源本身分离,可以有多种表现形式(如JSON、XML等)
- 超媒体驱动:API应该包含超媒体链接,引导客户端发现和使用相关资源
- 分层系统:使用分层架构,每层只与相邻层交互
2.2 资源设计
资源是RESTful API的核心概念,合理的资源设计是创建高质量API的基础。
2.2.1 资源识别
识别系统中的关键实体和概念,将其抽象为资源。例如,在电商系统中,常见的资源包括:用户(User)、商品(Product)、订单(Order)、购物车(Cart)等。
2.2.2 资源命名
资源命名应该遵循以下原则:
- 使用名词:资源名称应该使用名词,而不是动词
- 使用复数形式:资源集合应该使用复数形式(如/products)
- 使用小写字母:资源名称应该使用小写字母
- 使用连字符:多个单词组成的资源名称应该使用连字符(-)分隔,而不是下划线(_)或驼峰式命名
- 保持简洁:资源名称应该简洁明了,避免使用复杂或模糊的术语
示例:
| 资源 | URI | 说明 |
|---|---|---|
| 用户集合 | /users | 用户资源的集合 |
| 单个用户 | /users/ | 特定ID的用户资源 |
| 用户的订单 | /users/{id}/orders | 特定用户的订单集合 |
| 商品集合 | /products | 商品资源的集合 |
| 单个商品 | /products/ | 特定ID的商品资源 |
| 订单集合 | /orders | 订单资源的集合 |
| 单个订单 | /orders/ | 特定ID的订单资源 |
| 订单中的商品 | /orders/{id}/items | 特定订单中的商品项集合 |
2.2.3 资源关系
定义资源之间的关系,常见的关系包括:
- 一对一:一个资源与另一个资源存在一对一的关系
- 一对多:一个资源与多个资源存在一对多的关系
- 多对多:多个资源与多个资源存在多对多的关系
表示方法:
- 嵌套URI:使用嵌套URI表示资源之间的关系,如/users/{id}/orders表示用户的订单
- 链接:在资源的表示中包含指向相关资源的链接
2.3 HTTP方法的使用
RESTful API使用HTTP方法来表示对资源的操作,常见的HTTP方法及其用途如下:
| HTTP方法 | 用途 | 幂等性 | 安全性 |
|---|---|---|---|
| GET | 获取资源 | 是 | 是 |
| POST | 创建资源 | 否 | 否 |
| PUT | 更新资源(全部) | 是 | 否 |
| PATCH | 更新资源(部分) | 否 | 否 |
| DELETE | 删除资源 | 是 | 否 |
| HEAD | 获取资源的元数据 | 是 | 是 |
| OPTIONS | 获取资源支持的操作 | 是 | 是 |
说明:
- 幂等性:多次执行相同的操作,结果相同
- 安全性:操作不会修改服务器上的资源
2.3.1 GET方法
GET方法用于获取资源,应该是安全和幂等的。
示例:
#### 3.6.2 嵌套Resolver
嵌套Resolver用于解析嵌套字段的值,特别是当需要从其他数据源获取关联数据时。
**示例(Java + Spring for GraphQL)**:
```java
@Controller
public class UserController {
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
// 查询单个用户
@QueryMapping
public User user(@Argument Long id) {
return userService.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
}
@PostMapping
@ApiOperation("Create a new user")
@ApiResponses(value = {
@ApiResponse(code = 201, message = "Successfully created user"),
@ApiResponse(code = 400, message = "Invalid input"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 403, message = "Forbidden")
})
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User createdUser = userService.createUser(user);
return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
}
@PutMapping("/{id}")
@ApiOperation("Update a user")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Successfully updated user"),
@ApiResponse(code = 400, message = "Invalid input"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 403, message = "Forbidden"),
@ApiResponse(code = 404, message = "User not found")
})
public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User user) {
User updatedUser = userService.updateUser(id, user);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/{id}")
@ApiOperation("Delete a user")
@ApiResponses(value = {
@ApiResponse(code = 204, message = "Successfully deleted user"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 403, message = "Forbidden"),
@ApiResponse(code = 404, message = "User not found")
})
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
@PostMapping
@ApiOperation("Create a new user")
@ApiResponses(value = {
@ApiResponse(code = 201, message = "Successfully created user"),
@ApiResponse(code = 400, message = "Invalid input"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 403, message = "Forbidden")
})
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User createdUser = userService.createUser(user);
return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
}
@PutMapping("/{id}")
@ApiOperation("Update a user")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Successfully updated user"),
@ApiResponse(code = 400, message = "Invalid input"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 403, message = "Forbidden"),
@ApiResponse(code = 404, message = "User not found")
})
public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User user) {
User updatedUser = userService.updateUser(id, user);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/{id}")
@ApiOperation("Delete a user")
@ApiResponses(value = {
@ApiResponse(code = 204, message = "Successfully deleted user"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 403, message = "Forbidden"),
@ApiResponse(code = 404, message = "User not found")
})
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
// 查询所有用户
@QueryMapping
public List<User> users(
@Argument Integer page,
@Argument Integer size,
@Argument String sort) {
Pageable pageable = PageRequest.of(
page != null ? page - 1 : 0,
size != null ? size : 10,
Sort.by(getSortDirection(sort)));
return userService.findAll(pageable).getContent();
}
// 解析用户的订单
@SchemaMapping(typeName = "User", field = "orders")
public List<Order> orders(User user) {
return orderService.findByUserId(user.getId());
}
private Sort.Direction getSortDirection(String sort) {
if (sort == null || sort.isEmpty()) {
return Sort.Direction.ASC;
}
if (sort.endsWith("_DESC")) {
return Sort.Direction.DESC;
}
return Sort.Direction.ASC;
}
}
@Controller
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private UserService userService;
@Autowired
private ProductService productService;
// 查询单个订单
@QueryMapping
public Order order(@Argument Long id) {
return orderService.findById(id)
.orElseThrow(() -> new OrderNotFoundException("Order not found with id: " + id));
}
// 查询所有订单
@QueryMapping
public List<Order> orders(
@Argument Integer page,
@Argument Integer size,
@Argument String sort,
@Argument OrderStatus status,
@Argument Long userId) {
Pageable pageable = PageRequest.of(
page != null ? page - 1 : 0,
size != null ? size : 10,
Sort.by(getSortDirection(sort)));
return orderService.findOrders(pageable, status, userId).getContent();
}
// 创建订单
@MutationMapping
public Order createOrder(@Argument CreateOrderInput input) {
return orderService.createOrder(input);
}
// 更新订单状态
@MutationMapping
public Order updateOrderStatus(@Argument Long id, @Argument OrderStatus status) {
return orderService.updateOrderStatus(id, status);
}
// 删除订单
@MutationMapping
public boolean deleteOrder(@Argument Long id) {
orderService.deleteById(id);
return true;
}
// 解析订单的用户
@SchemaMapping(typeName = "Order", field = "user")
public User user(Order order) {
return userService.findById(order.getUserId())
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + order.getUserId()));
}
// 解析订单的商品项
@SchemaMapping(typeName = "Order", field = "items")
public List<OrderItem> items(Order order) {
return order.getItems();
}
// 解析订单项的商品
@SchemaMapping(typeName = "OrderItem", field = "product")
public Product product(OrderItem orderItem) {
return productService.findById(orderItem.getProductId())
.orElseThrow(() -> new ProductNotFoundException("Product not found with id: " + orderItem.getProductId()));
}
private Sort.Direction getSortDirection(String sort) {
if (sort == null || sort.isEmpty()) {
return Sort.Direction.ASC;
}
if (sort.endsWith("_DESC")) {
return Sort.Direction.DESC;
}
return Sort.Direction.ASC;
}
}
@Controller
public class ProductController {
@Autowired
private ProductService productService;
// 查询单个商品
@QueryMapping
public Product product(@Argument Long id) {
return productService.findById(id)
.orElseThrow(() -> new ProductNotFoundException("Product not found with id: " + id));
}
// 查询所有商品
@QueryMapping
public List<Product> products(
@Argument Integer page,
@Argument Integer size,
@Argument String sort,
@Argument String category,
@Argument Double priceGte,
@Argument Double priceLte) {
Pageable pageable = PageRequest.of(
page != null ? page - 1 : 0,
size != null ? size : 10,
Sort.by(getSortDirection(sort)));
return productService.findProducts(pageable, category, priceGte, priceLte).getContent();
}
private Sort.Direction getSortDirection(String sort) {
if (sort == null || sort.isEmpty()) {
return Sort.Direction.ASC;
}
if (sort.endsWith("_DESC")) {
return Sort.Direction.DESC;
}
return Sort.Direction.ASC;
}
}
// 订阅相关配置
@Configuration
public class SubscriptionConfig {
@Bean
public ExecutorSubscriptionRegistry subscriptionRegistry() {
return new ExecutorSubscriptionRegistry();
}
@Bean
public Publisher<Order> orderCreatedPublisher() {
return new SimplePublisher<>();
}
@Bean
public Publisher<Order> orderStatusUpdatedPublisher() {
return new SimplePublisher<>();
}
}
@Controller
public class SubscriptionController {
@Autowired
private Publisher<Order> orderCreatedPublisher;
@Autowired
private Publisher<Order> orderStatusUpdatedPublisher;
// 订阅订单创建事件
@SubscriptionMapping("orderCreated")
public Publisher<Order> orderCreated() {
return orderCreatedPublisher;
}
// 订阅订单状态更新事件
@SubscriptionMapping("orderStatusUpdated")
public Publisher<Order> orderStatusUpdated() {
return orderStatusUpdatedPublisher;
}
}3.7 GraphQL API的安全设计
GraphQL API的安全设计与REST API类似,但也有一些特殊的考虑因素:
- 认证和授权:使用OAuth 2.0、JWT等机制进行认证和授权
- 查询复杂度控制:限制查询的复杂度,防止恶意查询耗尽服务器资源
- 查询深度控制:限制查询的嵌套深度,防止无限递归查询
- 字段级权限控制:对不同用户授予不同字段的访问权限
- 批量操作限制:限制批量操作的数量,防止DoS攻击
- 输入验证:验证所有输入参数,防止注入攻击
- HTTPS:使用HTTPS加密传输数据
示例(查询复杂度控制):
const { ApolloServer, gql, makeExecutableSchema } = require('apollo-server');
const { createComplexityLimitRule } = require('graphql-validation-complexity');
// 定义Schema
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
orders: [Order!]!
}
type Order {
id: ID!
totalAmount: Float!
items: [OrderItem!]!
}
type OrderItem {
id: ID!
quantity: Int!
product: Product!
}
type Product {
id: ID!
name: String!
price: Float!
}
type Query {
user(id: ID!): User
users: [User!]!
}
`;
// 定义Resolver
const resolvers = {
Query: {
user: (parent, args) => { /* 实现 */ },
users: (parent, args) => { /* 实现 */ },
},
User: { orders: (parent) => { /* 实现 */ } },
Order: { items: (parent) => { /* 实现 */ } },
OrderItem: { product: (parent) => { /* 实现 */ } },
};
// 创建Schema
const schema = makeExecutableSchema({ typeDefs, resolvers });
// 创建查询复杂度限制规则
const complexityLimitRule = createComplexityLimitRule(1000, {
onCost: cost => console.log('Query cost:', cost),
createError: (max, actual) =>
new Error(`Query is too complex: ${actual}. Maximum allowed complexity: ${max}`),
});
// 创建Apollo Server
const server = new ApolloServer({
schema,
validationRules: [complexityLimitRule],
context: ({ req }) => {
// 认证逻辑
const token = req.headers.authorization || '';
const user = authenticateUser(token);
return { user };
},
});
// 启动服务器
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});四、API网关设计
API网关是微服务架构中的重要组件,它作为系统的唯一入口,负责请求路由、负载均衡、认证授权、限流熔断等功能。
4.1 API网关的核心功能
- 请求路由:根据请求路径将请求转发到相应的服务
- 负载均衡:在多个服务实例之间进行负载均衡
- 认证授权:对请求进行认证和授权检查
- 限流熔断:对请求进行限流和熔断处理
- 监控日志:收集请求的监控和日志信息
- 协议转换:在不同协议之间进行转换
- 数据转换:对请求和响应数据进行转换
- 缓存:对频繁请求的响应进行缓存
- API组合:将多个API的响应组合成一个响应
- 安全防护:防止常见的Web攻击,如SQL注入、XSS等
4.2 API网关的架构设计
API网关的架构设计应考虑以下几个方面:
4.2.1 组件设计
API网关通常由以下几个核心组件组成:
- 请求处理器:接收和处理客户端请求
- 路由管理器:管理请求路由规则
- 过滤器链:包含多个过滤器,用于实现各种功能
- 服务发现客户端:与服务注册中心交互,获取服务实例信息
- 负载均衡器:在多个服务实例之间进行负载均衡
- 认证授权管理器:处理认证和授权逻辑
- 限流熔断管理器:处理限流和熔断逻辑
- 监控日志收集器:收集监控和日志信息
- 配置管理器:管理网关的配置信息
4.2.2 流程图
客户端请求 → API网关 → 请求解析 → 路由匹配 → 前置过滤器 → 服务调用 → 后置过滤器 → 响应处理 → 客户端
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
监控日志 配置管理 服务发现 认证授权 限流熔断 负载均衡 数据转换 缓存管理4.2.3 部署模式
API网关的部署模式包括:
- 单实例部署:简单但不适合生产环境
- 集群部署:多实例部署,提高可用性和性能
- 容器化部署:使用Docker容器化部署,便于管理和扩展
- 云原生部署:在Kubernetes等云原生平台上部署,充分利用云原生特性
4.3 API网关的实现方案
API网关的实现方案有多种,包括自研和使用开源框架。以下是几种常见的API网关实现方案:
4.3.1 Spring Cloud Gateway
Spring Cloud Gateway是Spring Cloud生态中的API网关组件,它基于Spring 5、Spring Boot 2和Project Reactor构建,提供了路由、过滤、限流等功能。
核心特性:
- 基于响应式编程模型
- 支持动态路由
- 支持限流熔断
- 支持路径重写
- 支持集成服务发现
- 支持各种过滤器
配置示例:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
default-filters:
- name: Hystrix
args:
name: fallbackCommand
fallbackUri: forward:/fallback
- name: GlobalFilter
args:
name: logFilter
# 集成服务发现
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/代码示例(自定义过滤器):
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从请求头中获取token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
// 验证token
if (token == null || !token.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 解析token并获取用户信息
String jwt = token.substring(7);
// TODO: 验证和解析JWT token
// 将用户信息添加到请求头中
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", "user-123")
.header("X-User-Role", "admin")
.build();
// 继续执行过滤器链
return chain.filter(exchange.mutate().request(request).build());
}
@Override
public int getOrder() {
// 设置过滤器的执行顺序
return -100;
}
}4.3.2 Netflix Zuul
Netflix Zuul是Netflix开源的API网关组件,它基于Servlet实现,提供了路由、过滤、限流等功能。
核心特性:
- 支持动态路由
- 支持过滤
- 支持服务发现
- 支持熔断
- 支持监控
配置示例:
zuul:
routes:
user-service:
path: /api/users/**
serviceId: user-service
stripPrefix: true
order-service:
path: /api/orders/**
serviceId: order-service
stripPrefix: true
ratelimit:
enabled: true
repository: REDIS
policies:
user-service:
limit: 10
quota: 1000
refresh-interval: 60
ignored-headers:
- Set-Cookie
# 集成服务发现
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/代码示例(自定义过滤器):
@Component
public class LoggingFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
@Override
public String filterType() {
// pre: 请求前执行
// route: 路由请求时执行
// post: 请求后执行
// error: 发生错误时执行
return "pre";
}
@Override
public int filterOrder() {
// 过滤器的执行顺序
return 1;
}
@Override
public boolean shouldFilter() {
// 是否启用过滤器
return true;
}
@Override
public Object run() throws ZuulException {
// 获取请求上下文
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 记录请求信息
logger.info("Request URL: {}", request.getRequestURL());
logger.info("Request Method: {}", request.getMethod());
// 可以修改请求上下文
// ctx.setSendZuulResponse(false);
// ctx.setResponseStatusCode(401);
// ctx.setResponseBody("Unauthorized");
return null;
}
}4.3.3 Kong
Kong是一个开源的API网关和服务管理层,它基于Nginx和OpenResty构建,提供了路由、负载均衡、认证授权、限流熔断等功能。
核心特性:
- 支持插件扩展
- 支持动态配置
- 支持负载均衡
- 支持限流熔断
- 支持监控
- 支持可视化管理
配置示例(使用Admin API):
# 添加服务
curl -i -X POST \
--url http://localhost:8001/services/ \
--data 'name=user-service' \
--data 'url=http://user-service:8080'
# 添加路由
curl -i -X POST \
--url http://localhost:8001/services/user-service/routes \
--data 'paths[]=/api/users' \
--data 'strip_path=true'
# 启用认证插件
curl -i -X POST \
--url http://localhost:8001/services/user-service/plugins \
--data 'name=jwt'
# 启用限流插件
curl -i -X POST \
--url http://localhost:8001/services/user-service/plugins \
--data 'name=rate-limiting' \
--data 'config.minute=10' \
--data 'config.policy=local'```
### 4.4 API网关的最佳实践
在设计和实现API网关时,应遵循以下最佳实践:
#### 4.4.1 性能优化
- **缓存常用响应**:对频繁请求的响应进行缓存,减少后端服务的负载
- **请求聚合**:将多个请求合并为一个请求,减少网络往返次数
- **响应压缩**:对响应数据进行压缩,减少传输数据量
- **异步处理**:使用异步处理模式,提高并发处理能力
- **连接池**:使用连接池管理与后端服务的连接
#### 4.4.2 可用性设计
- **集群部署**:多实例部署API网关,提高可用性
- **健康检查**:定期检查后端服务的健康状态,自动剔除不健康的服务实例
- **熔断机制**:当后端服务不可用时,快速失败并返回默认响应
- **降级策略**:当系统负载过高时,关闭部分非核心功能,保证核心功能的可用性
- **限流措施**:对请求进行限流,防止系统被突发流量冲垮
#### 4.4.3 安全设计
- **认证授权**:在API网关层统一处理认证和授权
- **输入验证**:对所有请求参数进行验证,防止注入攻击
- **请求日志**:记录所有请求的日志,便于审计和排查问题
- **安全防护**:防止SQL注入、XSS、CSRF等常见的Web攻击
- **HTTPS**:使用HTTPS加密传输数据
#### 4.4.4 可观测性设计
- **监控指标**:收集API网关的关键指标,如请求量、响应时间、错误率等
- **分布式追踪**:集成分布式追踪系统,追踪请求的完整链路
- **日志聚合**:将API网关的日志聚合到中心化的日志系统
- **告警机制**:设置告警阈值,当指标超过阈值时及时告警
#### 4.4.5 扩展性设计
- **插件化架构**:采用插件化架构,便于功能扩展
- **动态配置**:支持动态更新配置,无需重启API网关
- **自动扩缩容**:支持根据负载自动扩缩容
- **灰度发布**:支持灰度发布新功能,降低发布风险
## 五、API文档与测试
API文档是API设计的重要组成部分,它有助于开发者理解和使用API。API测试则是保证API质量的重要手段。
### 5.1 API文档工具与示例
#### 5.1.1 Swagger/OpenAPI
Swagger/OpenAPI是目前最流行的API文档工具,它提供了API设计、文档生成、测试等功能。
**配置示例(Spring Boot + SpringFox)**:
```java
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.api"))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo())
.securitySchemes(Arrays.asList(apiKey()))
.securityContexts(Arrays.asList(securityContext()));
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("API Documentation")
.description("API Documentation for User Service")
.version("1.0.0")
.build();
}
private ApiKey apiKey() {
return new ApiKey("JWT", "Authorization", "header");
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.any())
.build();
}
List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return Arrays.asList(new SecurityReference("JWT", authorizationScopes));
}
}Controller示例:
@RestController
@RequestMapping("/api/users")
@Api(tags = "User Management")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
@ApiOperation("Get all users")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Successfully retrieved users"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 403, message = "Forbidden")
})
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
@ApiOperation("Get user by ID")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Successfully retrieved user"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 403, message = "Forbidden"),
@ApiResponse(code = 404, message = "User not found")
})
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
return ResponseEntity.ok(user);
}
@PostMapping
@ApiOperation("Create a new user")
@ApiResponses(value = {
@ApiResponse(code = 201, message = "Successfully created user"),
@ApiResponse(code = 400, message = "Invalid input"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 403, message = "Forbidden")
})
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User createdUser = userService.createUser(user);
return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
}
@PutMapping("/{id}")
@ApiOperation("Update a user")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Successfully updated user"),
@ApiResponse(code = 400, message = "Invalid input"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 403, message = "Forbidden"),
@ApiResponse(code = 404, message = "User not found")
})
public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User user) {
User updatedUser = userService.updateUser(id, user);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/{id}")
@ApiOperation("Delete a user")
@ApiResponses(value = {
@ApiResponse(code = 204, message = "Successfully deleted user"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 403, message = "Forbidden"),
@ApiResponse(code = 404, message = "User not found")
})
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}2.3.2 POST方法
POST方法用于创建新资源,不保证幂等性。
示例:
# 创建新用户
POST /users HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com",
"password": "password123"
}
# 为用户创建订单
POST /users/123/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"items": [
{
"productId": 456,
"quantity": 2
}
],
"totalAmount": 99.99
}2.3.3 PUT方法
PUT方法用于更新资源的全部内容,应该是幂等的。
示例:
# 更新用户信息
PUT /users/123 HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"id": 123,
"name": "Jane Doe",
"email": "jane@example.com",
"password": "newpassword123"
}2.3.4 PATCH方法
PATCH方法用于部分更新资源,不保证幂等性。
示例:
# 部分更新用户信息
PATCH /users/123 HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"email": "jane.doe@example.com"
}2.3.5 DELETE方法
DELETE方法用于删除资源,应该是幂等的。
示例:
# 删除用户
DELETE /users/123 HTTP/1.1
Host: api.example.com
# 删除订单
DELETE /orders/456 HTTP/1.1
Host: api.example.com2.4 状态码的使用
RESTful API使用HTTP状态码来表示请求的处理结果,常见的HTTP状态码及其用途如下:
2.4.1 1xx(信息性状态码)
- 100 Continue:服务器已收到请求头,客户端应继续发送请求体
- 101 Switching Protocols:服务器同意客户端的协议切换请求
2.4.2 2xx(成功状态码)
- 200 OK:请求成功,返回请求的资源
- 201 Created:请求成功创建新资源,Location头包含新资源的URI
- 202 Accepted:请求已接受,但尚未处理完成
- 204 No Content:请求成功,但没有内容返回
- 206 Partial Content:请求成功,返回部分资源(用于范围请求)
2.4.3 3xx(重定向状态码)
- 301 Moved Permanently:资源已永久移动到新URI
- 302 Found:资源临时移动到新URI
- 303 See Other:重定向到另一个URI,通常用于POST后的重定向
- 304 Not Modified:资源未修改,可以使用缓存的版本
- 307 Temporary Redirect:临时重定向,保持原HTTP方法
- 308 Permanent Redirect:永久重定向,保持原HTTP方法
2.4.4 4xx(客户端错误状态码)
- 400 Bad Request:请求格式错误或参数无效
- 401 Unauthorized:请求需要认证
- 403 Forbidden:服务器拒绝请求,认证通过但权限不足
- 404 Not Found:请求的资源不存在
- 405 Method Not Allowed:请求的HTTP方法不被允许
- 406 Not Acceptable:服务器无法提供客户端请求的表示形式
- 409 Conflict:请求与服务器当前状态冲突
- 410 Gone:请求的资源已永久删除
- 429 Too Many Requests:客户端发送的请求过多
2.4.5 5xx(服务器错误状态码)
- 500 Internal Server Error:服务器内部错误
- 501 Not Implemented:服务器不支持请求的功能
- 502 Bad Gateway:服务器作为网关或代理,从上游服务器收到无效响应
- 503 Service Unavailable:服务器当前无法处理请求(通常是临时状态)
- 504 Gateway Timeout:服务器作为网关或代理,未及时从上游服务器收到响应
2.5 请求和响应设计
2.5.1 请求设计
- 参数设计:合理设计查询参数、路径参数和请求体参数
- 分页设计:支持分页查询,常见参数包括page(页码)、size(每页数量)
- 排序设计:支持排序,常见参数包括sort(排序字段和方向)
- 过滤设计:支持过滤,使用查询参数指定过滤条件
- 内容类型:明确指定请求的Content-Type
示例:
# 带分页、排序和过滤的请求
GET /products?page=1&size=10&sort=price,desc&category=electronics&price_lte=1000 HTTP/1.1
Host: api.example.com
Accept: application/json2.5.2 响应设计
- 统一格式:使用统一的响应格式,包括状态码、消息和数据
- 分页响应:分页查询的响应应包含分页信息
- 错误处理:错误响应应包含详细的错误信息
- 超媒体链接:在响应中包含相关资源的链接
- 内容协商:支持内容协商,根据Accept头返回不同格式的响应
示例:
// 成功响应示例
{
"code": 200,
"message": "Success",
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2023-01-01T12:00:00Z",
"updatedAt": "2023-01-01T12:00:00Z",
"_links": {
"self": {
"href": "/users/123"
},
"orders": {
"href": "/users/123/orders"
}
}
}
}
// 分页响应示例
{
"code": 200,
"message": "Success",
"data": {
"content": [
{
"id": 1,
"name": "Product 1",
"price": 99.99
},
{
"id": 2,
"name": "Product 2",
"price": 199.99
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 10,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"offset": 0,
"unpaged": false,
"paged": true
},
"totalPages": 5,
"totalElements": 45,
"last": false,
"size": 10,
"number": 0,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"numberOfElements": 10,
"first": true,
"empty": false
}
}
// 错误响应示例
{
"code": 400,
"message": "Bad Request",
"errors": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "password",
"message": "Password must be at least 8 characters long"
}
]
}2.6 RESTful API的安全设计
RESTful API的安全设计是非常重要的,常见的安全措施包括:
- 认证:使用OAuth 2.0、JWT等机制进行认证
- 授权:使用RBAC(基于角色的访问控制)等机制进行授权
- HTTPS:使用HTTPS加密传输数据
- 输入验证:验证所有输入参数,防止注入攻击
- 输出编码:对输出数据进行编码,防止XSS攻击
- 限流:限制API的访问频率,防止DoS攻击
- CSRF防护:防止跨站请求伪造攻击
- CORS配置:合理配置跨域资源共享
示例:
# 使用Bearer Token进行认证
GET /users HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# 使用HTTPS加密传输
GET https://api.example.com/users HTTP/1.12.7 RESTful API的版本控制
API版本控制是确保向后兼容性的重要手段,常见的版本控制方法包括:
- URI路径版本:将版本信息放在URI路径中,如/v1/users、/v2/users
- 查询参数版本:将版本信息作为查询参数,如/users?version=1
- 请求头版本:将版本信息放在请求头中,如Accept: application/vnd.example.v1+json
- 内容协商版本:使用内容协商机制指定版本
示例:
# URI路径版本
GET /v1/users HTTP/1.1
Host: api.example.com
# 查询参数版本
GET /users?version=1 HTTP/1.1
Host: api.example.com
# 请求头版本
GET /users HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.v1+json三、GraphQL API设计
GraphQL是一种用于API的查询语言和运行时,它允许客户端精确地指定需要的数据,避免了REST API中常见的过度获取和获取不足问题。
3.1 GraphQL的核心概念
- Schema:定义API的数据结构和操作类型
- Query:用于获取数据的操作类型,类似于REST的GET
- Mutation:用于修改数据的操作类型,类似于REST的POST、PUT、DELETE
- Subscription:用于订阅实时更新的操作类型
- Type:定义数据的类型,包括标量类型和对象类型
- Field:类型中的字段,表示数据的属性
- Argument:字段可以接受参数,用于过滤、排序等操作
- Resolver:用于解析字段值的函数
3.2 Schema设计
Schema是GraphQL API的核心,它定义了API的数据结构和操作类型。
3.2.1 类型定义
GraphQL支持多种类型,包括:
- 标量类型:Int、Float、String、Boolean、ID等
- 对象类型:自定义的复杂类型
- 接口类型:定义一组字段,其他类型可以实现这些字段
- 联合类型:表示一个值可能是多种类型中的一种
- 枚举类型:定义一组命名的常量值
- 输入类型:用于Mutation操作中的参数类型
- 列表类型:表示一个值是某种类型的列表
- 非空类型:表示一个值不能为null
示例:
# 定义对象类型
type User {
id: ID! # 非空ID类型
name: String! # 非空String类型
email: String! # 非空String类型
age: Int # 可选Int类型
createdAt: String! # 非空String类型
updatedAt: String! # 非空String类型
orders: [Order!]! # 非空Order类型的非空列表
}
# 定义对象类型
type Product {
id: ID! # 非空ID类型
name: String! # 非空String类型
description: String # 可选String类型
price: Float! # 非空Float类型
category: String! # 非空String类型
createdAt: String! # 非空String类型
updatedAt: String! # 非空String类型
}
# 定义对象类型
type Order {
id: ID! # 非空ID类型
userId: ID! # 非空ID类型
items: [OrderItem!]! # 非空OrderItem类型的非空列表
totalAmount: Float! # 非空Float类型
status: OrderStatus! # 非空OrderStatus类型
createdAt: String! # 非空String类型
updatedAt: String! # 非空String类型
user: User! # 非空User类型
}
# 定义对象类型
type OrderItem {
id: ID! # 非空ID类型
orderId: ID! # 非空ID类型
productId: ID! # 非空ID类型
quantity: Int! # 非空Int类型
price: Float! # 非空Float类型
product: Product! # 非空Product类型
}
# 定义枚举类型
enum OrderStatus {
PENDING # 待处理
PAID # 已支付
SHIPPED # 已发货
DELIVERED # 已送达
CANCELLED # 已取消
}
# 定义输入类型
input CreateUserInput {
name: String! # 非空String类型
email: String! # 非空String类型
password: String! # 非空String类型
age: Int # 可选Int类型
}
# 定义输入类型
input UpdateUserInput {
id: ID! # 非空ID类型
name: String # 可选String类型
email: String # 可选String类型
password: String # 可选String类型
age: Int # 可选Int类型
}
# 定义输入类型
input CreateOrderInput {
userId: ID! # 非空ID类型
items: [CreateOrderItemInput!]! # 非空CreateOrderItemInput类型的非空列表
}
# 定义输入类型
input CreateOrderItemInput {
productId: ID! # 非空ID类型
quantity: Int! # 非空Int类型
}3.2.2 操作类型定义
GraphQL支持三种操作类型:Query(查询)、Mutation(变更)和Subscription(订阅)。
示例:
# 定义查询操作类型
type Query {
# 获取所有用户
users(page: Int, size: Int, sort: String): [User!]!
# 获取单个用户
user(id: ID!): User
# 获取所有商品
products(page: Int, size: Int, sort: String, category: String, priceGte: Float, priceLte: Float): [Product!]!
# 获取单个商品
product(id: ID!): Product
# 获取所有订单
orders(page: Int, size: Int, sort: String, status: OrderStatus, userId: ID): [Order!]!
# 获取单个订单
order(id: ID!): Order
}
# 定义变更操作类型
type Mutation {
# 创建用户
createUser(input: CreateUserInput!): User!
# 更新用户
updateUser(input: UpdateUserInput!): User!
# 删除用户
deleteUser(id: ID!): Boolean!
# 创建订单
createOrder(input: CreateOrderInput!): Order!
# 更新订单状态
updateOrderStatus(id: ID!, status: OrderStatus!): Order!
# 删除订单
deleteOrder(id: ID!): Boolean!
}
# 定义订阅操作类型
type Subscription {
# 订阅订单创建事件
orderCreated: Order!
# 订阅订单状态更新事件
orderStatusUpdated: Order!
}3.3 查询设计
GraphQL查询允许客户端精确地指定需要的数据,避免了REST API中常见的过度获取和获取不足问题。
3.3.1 基本查询
基本查询用于获取单个或多个资源。
示例:
# 查询单个用户
query {
user(id: "123") {
id
name
email
age
}
}
# 查询多个用户
query {
users(page: 1, size: 10) {
id
name
email
}
}3.3.2 嵌套查询
GraphQL支持嵌套查询,可以一次性获取相关联的资源。
示例:
# 查询订单及其关联的用户和商品
query {
order(id: "456") {
id
totalAmount
status
createdAt
user {
id
name
email
}
items {
id
quantity
price
product {
id
name
price
category
}
}
}
}3.3.3 参数化查询
GraphQL查询可以接受参数,用于过滤、排序和分页等操作。
示例:
# 带参数的商品查询
query {
products(
page: 1,
size: 10,
sort: "price_DESC",
category: "electronics",
priceLte: 1000
) {
id
name
price
category
}
}
# 带参数的订单查询
query {
orders(
status: PAID,
userId: "123"
) {
id
totalAmount
status
createdAt
items {
id
quantity
product {
id
name
}
}
}
}3.3.4 命名查询和变量
GraphQL支持命名查询和变量,可以提高查询的可重用性和可读性。
示例:
# 命名查询和变量
query GetUserById($userId: ID!) {
user(id: $userId) {
id
name
email
age
orders {
id
totalAmount
status
}
}
}
# 变量值
{
"userId": "123"
}
# 带多个变量的查询
query GetProducts(
$page: Int,
$size: Int,
$sort: String,
$category: String,
$priceRange: PriceRangeInput
) {
products(
page: $page,
size: $size,
sort: $sort,
category: $category,
priceGte: $priceRange.min,
priceLte: $priceRange.max
) {
id
name
price
category
}
}
# 变量值
{
"page": 1,
"size": 10,
"sort": "price_DESC",
"category": "electronics",
"priceRange": {
"min": 100,
"max": 1000
}
}3.4 Mutation设计
Mutation用于修改数据,包括创建、更新和删除操作。
3.4.1 创建操作
创建操作用于创建新资源。
示例:
# 创建用户
mutation {
createUser(input: {
name: "John Doe",
email: "john@example.com",
password: "password123",
age: 30
}) {
id
name
email
age
createdAt
}
}
# 创建订单
mutation {
createOrder(input: {
userId: "123",
items: [
{
productId: "456",
quantity: 2
},
{
productId: "789",
quantity: 1
}
]
}) {
id
totalAmount
status
createdAt
items {
id
quantity
price
product {
id
name
}
}
}
}3.4.2 更新操作
更新操作用于更新现有资源。
示例:
# 更新用户
mutation {
updateUser(input: {
id: "123",
name: "Jane Doe",
email: "jane@example.com"
}) {
id
name
email
updatedAt
}
}
# 更新订单状态
mutation {
updateOrderStatus(id: "456", status: PAID) {
id
status
updatedAt
}
}3.4.3 删除操作
删除操作用于删除现有资源。
示例:
# 删除用户
mutation {
deleteUser(id: "123")
}
# 删除订单
mutation {
deleteOrder(id: "456")
}3.4.4 命名Mutation和变量
与查询类似,Mutation也支持命名和变量。
示例:
# 命名Mutation和变量
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
createdAt
}
}
# 变量值
{
"input": {
"name": "John Doe",
"email": "john@example.com",
"password": "password123",
"age": 30
}
}3.5 Subscription设计
Subscription用于订阅实时更新,当服务器端的数据发生变化时,客户端会收到通知。
示例:
# 订阅订单创建事件
subscription {
orderCreated {
id
totalAmount
status
createdAt
user {
id
name
}
}
}
# 订阅订单状态更新事件
subscription {
orderStatusUpdated {
id
status
updatedAt
}
}3.6 Resolver设计
Resolver是GraphQL API中的核心组件,它负责解析字段的值。每个字段都有一个对应的Resolver函数。
3.6.1 基本Resolver
基本Resolver用于解析简单字段的值。
示例(Node.js + Apollo Server):
const resolvers = {
Query: {
// 获取单个用户
user: (parent, args, context, info) => {
const { id } = args;
// 从数据库或其他数据源获取用户数据
return db.users.findOne({ id });
},
// 获取所有用户
users: (parent, args, context, info) => {
const { page = 1, size = 10, sort = 'createdAt_ASC' } = args;
// 计算偏移量
const offset = (page - 1) * size;
// 构建排序选项
const [sortField, sortOrder] = sort.split('_');
const sortOptions = { [sortField]: sortOrder === 'ASC' ? 1 : -1 };
// 从数据库获取用户列表
return db.users.find({}).sort(sortOptions).skip(offset).limit(size);
},
// 获取单个商品
product: (parent, args, context, info) => {
const { id } = args;
// 从数据库获取商品数据
return db.products.findOne({ id });
},
// 获取所有商品
products: (parent, args, context, info) => {
const { page = 1, size = 10, sort = 'createdAt_ASC', category, priceGte, priceLte } = args;
// 计算偏移量
const offset = (page - 1) * size;
// 构建排序选项
const [sortField, sortOrder] = sort.split('_');
const sortOptions = { [sortField]: sortOrder === 'ASC' ? 1 : -1 };
// 构建过滤条件
const filter = {};
if (category) filter.category = category;
if (priceGte !== undefined) filter.price = { ...filter.price, $gte: priceGte };
if (priceLte !== undefined) filter.price = { ...filter.price, $lte: priceLte };
// 从数据库获取商品列表
return db.products.find(filter).sort(sortOptions).skip(offset).limit(size);
},
// 获取单个订单
order: (parent, args, context, info) => {
const { id } = args;
// 从数据库获取订单数据
return db.orders.findOne({ id });
},
// 获取所有订单
orders: (parent, args, context, info) => {
const { page = 1, size = 10, sort = 'createdAt_ASC', status, userId } = args;
// 计算偏移量
const offset = (page - 1) * size;
// 构建排序选项
const [sortField, sortOrder] = sort.split('_');
const sortOptions = { [sortField]: sortOrder === 'ASC' ? 1 : -1 };
// 构建过滤条件
const filter = {};
if (status) filter.status = status;
if (userId) filter.userId = userId;
// 从数据库获取订单列表
return db.orders.find(filter).sort(sortOptions).skip(offset).limit(size);
},
},
Mutation: {
// 创建用户
createUser: (parent, args, context, info) => {
const { input } = args;
// 创建用户
const user = {
id: generateId(),
name: input.name,
email: input.email,
password: hashPassword(input.password),
age: input.age,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
// 保存到数据库
db.users.insert(user);
return user;
},
// 更新用户
updateUser: (parent, args, context, info) => {
const { input } = args;
const { id } = input;
// 从数据库获取用户
let user = db.users.findOne({ id });
if (!user) {
throw new Error(`User with id ${id} not found`);
}
// 更新用户信息
user = {
...user,
...input,
updatedAt: new Date().toISOString(),
};
// 保存到数据库
db.users.update({ id }, user);
return user;
},
// 删除用户
deleteUser: (parent, args, context, info) => {
const { id } = args;
// 从数据库删除用户
const result = db.users.remove({ id });
return result.deletedCount > 0;
},
// 创建订单
createOrder: async (parent, args, context, info) => {
const { input } = args;
const { userId, items } = input;
// 验证用户是否存在
const user = db.users.findOne({ id: userId });
if (!user) {
throw new Error(`User with id ${userId} not found`);
}
// 验证商品是否存在并计算总价
let totalAmount = 0;
const orderItems = [];
for (const item of items) {
const product = db.products.findOne({ id: item.productId });
if (!product) {
throw new Error(`Product with id ${item.productId} not found`);
}
const itemTotal = product.price * item.quantity;
totalAmount += itemTotal;
orderItems.push({
id: generateId(),
productId: item.productId,
quantity: item.quantity,
price: product.price,
});
}
// 创建订单
const order = {
id: generateId(),
userId,
items: orderItems,
totalAmount,
status: 'PENDING',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
// 保存到数据库
db.orders.insert(order);
// 发布订单创建事件
pubsub.publish('ORDER_CREATED', { orderCreated: order });
return order;
},
// 更新订单状态
updateOrderStatus: (parent, args, context, info) => {
const { id, status } = args;
// 从数据库获取订单
let order = db.orders.findOne({ id });
if (!order) {
throw new Error(`Order with id ${id} not found`);
}
// 更新订单状态
order.status = status;
order.updatedAt = new Date().toISOString();
// 保存到数据库
db.orders.update({ id }, order);
// 发布订单状态更新事件
pubsub.publish('ORDER_STATUS_UPDATED', { orderStatusUpdated: order });
return order;
},
// 删除订单
deleteOrder: (parent, args, context, info) => {
const { id } = args;
// 从数据库删除订单
const result = db.orders.remove({ id });
return result.deletedCount > 0;
},
},
Subscription: {
// 订阅订单创建事件
orderCreated: {
subscribe: () => pubsub.asyncIterator(['ORDER_CREATED']),
},
// 订阅订单状态更新事件
orderStatusUpdated: {
subscribe: () => pubsub.asyncIterator(['ORDER_STATUS_UPDATED']),
},
},
// 嵌套字段的Resolver
User: {
// 解析用户的订单
orders: (parent, args, context, info) => {
const { id } = parent;
return db.orders.find({ userId: id });
},
},
Order: {
// 解析订单的用户
user: (parent, args, context, info) => {
const { userId } = parent;
return db.users.findOne({ id: userId });
},
// 解析订单的商品项
items: (parent, args, context, info) => {
const { items } = parent;
return items;
},
},
OrderItem: {
// 解析订单项的商品
product: (parent, args, context, info) => {
const { productId } = parent;
return db.products.findOne({ id: productId });
},
},
};
},
// 获取所有商品
products: (parent, args, context, info) => {
const { page = 1, size = 10, sort = 'createdAt_ASC', category, priceGte, priceLte } = args;
// 计算偏移量
const offset = (page - 1) * size;
// 构建排序选项
const [sortField, sortOrder] = sort.split('_');
const sortOptions = { [sortField]: sortOrder === 'ASC' ? 1 : -1 };
// 构建过滤条件
const filter = {};
if (category) filter.category = category;
if (priceGte !== undefined) filter.price = { ...filter.price, $gte: priceGte };
if (priceLte !== undefined) filter.price = { ...filter.price, $lte: priceLte };
// 从数据库获取商品列表
return db.products.find(filter).sort(sortOptions).skip(offset).limit(size);
},
// 获取单个订单
order: (parent, args, context, info) => {
const { id } = args;
// 从数据库获取订单数据
return db.orders.findOne({ id });
},
// 获取所有订单
orders: (parent, args, context, info) => {
const { page = 1, size = 10, sort = 'createdAt_ASC', status, userId } = args;
// 计算偏移量
const offset = (page - 1) * size;
// 构建排序选项
const [sortField, sortOrder] = sort.split('_');
const sortOptions = { [sortField]: sortOrder === 'ASC' ? 1 : -1 };
// 构建过滤条件
const filter = {};
if (status) filter.status = status;
if (userId) filter.userId = userId;
// 从数据库获取订单列表
return db.orders.find(filter).sort(sortOptions).skip(offset).limit(size);
},
},
Mutation: {
// 创建用户
createUser: (parent, args, context, info) => {
const { input } = args;
// 创建用户
const user = {
id: generateId(),
name: input.name,
email: input.email,
password: hashPassword(input.password),
age: input.age,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
// 保存到数据库
db.users.insert(user);
return user;
},
// 更新用户
updateUser: (parent, args, context, info) => {
const { input } = args;
const { id } = input;
// 从数据库获取用户
let user = db.users.findOne({ id });
if (!user) {
throw new Error(`User with id ${id} not found`);
}
// 更新用户信息
user = {
...user,
...input,
updatedAt: new Date().toISOString(),
};
// 保存到数据库
db.users.update({ id }, user);
return user;
},
// 删除用户
deleteUser: (parent, args, context, info) => {
const { id } = args;
// 从数据库删除用户
const result = db.users.remove({ id });
return result.deletedCount > 0;
},
// 创建订单
createOrder: async (parent, args, context, info) => {
const { input } = args;
const { userId, items } = input;
// 验证用户是否存在
const user = db.users.findOne({ id: userId });
if (!user) {
throw new Error(`User with id ${userId} not found`);
}
// 验证商品是否存在并计算总价
let totalAmount = 0;
const orderItems = [];
for (const item of items) {
const product = db.products.findOne({ id: item.productId });
if (!product) {
throw new Error(`Product with id ${item.productId} not found`);
}
const itemTotal = product.price * item.quantity;
totalAmount += itemTotal;
orderItems.push({
id: generateId(),
productId: item.productId,
quantity: item.quantity,
price: product.price,
});
}
// 创建订单
const order = {
id: generateId(),
userId,
items: orderItems,
totalAmount,
status: 'PENDING',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
// 保存到数据库
db.orders.insert(order);
// 发布订单创建事件
pubsub.publish('ORDER_CREATED', { orderCreated: order });
return order;
},
// 更新订单状态
updateOrderStatus: (parent, args, context, info) => {
const { id, status } = args;
// 从数据库获取订单
let order = db.orders.findOne({ id });
if (!order) {
throw new Error(`Order with id ${id} not found`);
}
// 更新订单状态
order.status = status;
order.updatedAt = new Date().toISOString();
// 保存到数据库
db.orders.update({ id }, order);
// 发布订单状态更新事件
pubsub.publish('ORDER_STATUS_UPDATED', { orderStatusUpdated: order });
return order;
},
// 删除订单
deleteOrder: (parent, args, context, info) => {
const { id } = args;
// 从数据库删除订单
const result = db.orders.remove({ id });
return result.deletedCount > 0;
},
},
Subscription: {
// 订阅订单创建事件
orderCreated: {
subscribe: () => pubsub.asyncIterator(['ORDER_CREATED']),
},
// 订阅订单状态更新事件
orderStatusUpdated: {
subscribe: () => pubsub.asyncIterator(['ORDER_STATUS_UPDATED']),
},
},
// 嵌套字段的Resolver
User: {
// 解析用户的订单
orders: (parent, args, context, info) => {
const { id } = parent;
return db.orders.find({ userId: id });
},
},
Order: {
// 解析订单的用户
user: (parent, args, context, info) => {
const { userId } = parent;
return db.users.findOne({ id: userId });
},
// 解析订单的商品项
items: (parent, args, context, info) => {
const { items } = parent;
return items;
},
},
OrderItem: {
// 解析订单项的商品
product: (parent, args, context, info) => {
const { productId } = parent;
return db.products.findOne({ id: productId });
},
},
};3.6.2 嵌套Resolver
嵌套Resolver用于解析嵌套字段的值,特别是当需要从其他数据源获取关联数据时。
示例(Java + Spring for GraphQL):
@Controller
public class UserController {
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
// 查询单个用户
@QueryMapping
public User user(@Argument Long id) {
return userService.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
}
// 查询所有用户
@QueryMapping
public List<User> users(
@Argument Integer page,
@Argument Integer size,
@Argument String sort) {
Pageable pageable = PageRequest.of(
page != null ? page - 1 : 0,
size != null ? size : 10,
Sort.by(getSortDirection(sort)));
return userService.findAll(pageable).getContent();
}
// 解析用户的订单
@SchemaMapping(typeName = "User", field = "orders")
public List<Order> orders(User user) {
return orderService.findByUserId(user.getId());
}
private Sort.Direction getSortDirection(String sort) {
if (sort == null || sort.isEmpty()) {
return Sort.Direction.ASC;
}
if (sort.endsWith("_DESC")) {
return Sort.Direction.DESC;
}
return Sort.Direction.ASC;
}
}
@Controller
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private UserService userService;
@Autowired
private ProductService productService;
// 查询单个订单
@QueryMapping
public Order order(@Argument Long id) {
return orderService.findById(id)
.orElseThrow(() -> new OrderNotFoundException("Order not found with id: " + id));
}
// 查询所有订单
@QueryMapping
public List<Order> orders(
@Argument Integer page,
@Argument Integer size,
@Argument String sort,
@Argument OrderStatus status,
@Argument Long userId) {
Pageable pageable = PageRequest.of(
page != null ? page - 1 : 0,
size != null ? size : 10,
Sort.by(getSortDirection(sort)));
return orderService.findOrders(pageable, status, userId).getContent();
}
// 创建订单
@MutationMapping
public Order createOrder(@Argument CreateOrderInput input) {
return orderService.createOrder(input);
}
// 更新订单状态
@MutationMapping
public Order updateOrderStatus(@Argument Long id, @Argument OrderStatus status) {
return orderService.updateOrderStatus(id, status);
}
// 删除订单
@MutationMapping
public boolean deleteOrder(@Argument Long id) {
orderService.deleteById(id);
return true;
}
// 解析订单的用户
@SchemaMapping(typeName = "Order", field = "user")
public User user(Order order) {
return userService.findById(order.getUserId())
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + order.getUserId()));
}
// 解析订单的商品项
@SchemaMapping(typeName = "Order", field = "items")
public List<OrderItem> items(Order order) {
return order.getItems();
}
// 解析订单项的商品
@SchemaMapping(typeName = "OrderItem", field = "product")
public Product product(OrderItem orderItem) {
return productService.findById(orderItem.getProductId())
.orElseThrow(() -> new ProductNotFoundException("Product not found with id: " + orderItem.getProductId()));
}
private Sort.Direction getSortDirection(String sort) {
if (sort == null || sort.isEmpty()) {
return Sort.Direction.ASC;
}
if (sort.endsWith("_DESC")) {
return Sort.Direction.DESC;
}
return Sort.Direction.ASC;
}
}
@Controller
public class ProductController {
@Autowired
private ProductService productService;
// 查询单个商品
@QueryMapping
public Product product(@Argument Long id) {
return productService.findById(id)
.orElseThrow(() -> new ProductNotFoundException("Product not found with id: " + id));
}
// 查询所有商品
@QueryMapping
public List<Product> products(
@Argument Integer page,
@Argument Integer size,
@Argument String sort,
@Argument String category,
@Argument Double priceGte,
@Argument Double priceLte) {
Pageable pageable = PageRequest.of(
page != null ? page - 1 : 0,
size != null ? size : 10,
Sort.by(getSortDirection(sort)));
return productService.findProducts(pageable, category, priceGte, priceLte).getContent();
}
private Sort.Direction getSortDirection(String sort) {
if (sort == null || sort.isEmpty()) {
return Sort.Direction.ASC;
}
if (sort.endsWith("_DESC")) {
return Sort.Direction.DESC;
}
return Sort.Direction.ASC;
}
}
// 订阅相关配置
@Configuration
public class SubscriptionConfig {
@Bean
public ExecutorSubscriptionRegistry subscriptionRegistry() {
return new ExecutorSubscriptionRegistry();
}
@Bean
public Publisher<Order> orderCreatedPublisher() {
return new SimplePublisher<>();
}
@Bean
public Publisher<Order> orderStatusUpdatedPublisher() {
return new SimplePublisher<>();
}
}
@Controller
public class SubscriptionController {
@Autowired
private Publisher<Order> orderCreatedPublisher;
@Autowired
private Publisher<Order> orderStatusUpdatedPublisher;
// 订阅订单创建事件
@SubscriptionMapping("orderCreated")
public Publisher<Order> orderCreated() {
return orderCreatedPublisher;
}
// 订阅订单状态更新事件
@SubscriptionMapping("orderStatusUpdated")
public Publisher<Order> orderStatusUpdated() {
return orderStatusUpdatedPublisher;
}
}3.7 GraphQL API的安全设计
GraphQL API的安全设计与REST API类似,但也有一些特殊的考虑因素:
- 认证和授权:使用OAuth 2.0、JWT等机制进行认证和授权
- 查询复杂度控制:限制查询的复杂度,防止恶意查询耗尽服务器资源
- 查询深度控制:限制查询的嵌套深度,防止无限递归查询
- 字段级权限控制:对不同用户授予不同字段的访问权限
- 批量操作限制:限制批量操作的数量,防止DoS攻击
- 输入验证:验证所有输入参数,防止注入攻击
- HTTPS:使用HTTPS加密传输数据
示例(查询复杂度控制):
const { ApolloServer, gql, makeExecutableSchema } = require('apollo-server');
const { createComplexityLimitRule } = require('graphql-validation-complexity');
// 定义Schema
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
orders: [Order!]!
}
type Order {
id: ID!
totalAmount: Float!
items: [OrderItem!]!
}
type OrderItem {
id: ID!
quantity: Int!
product: Product!
}
type Product {
id: ID!
name: String!
price: Float!
}
type Query {
user(id: ID!): User
users: [User!]!
}
`;
// 定义Resolver
const resolvers = {
Query: {
user: (parent, args) => { /* 实现 */ },
users: (parent, args) => { /* 实现 */ },
},
User: { orders: (parent) => { /* 实现 */ } },
Order: { items: (parent) => { /* 实现 */ } },
OrderItem: { product: (parent) => { /* 实现 */ } },
};
// 创建Schema
const schema = makeExecutableSchema({ typeDefs, resolvers });
// 创建查询复杂度限制规则
const complexityLimitRule = createComplexityLimitRule(1000, {
onCost: cost => console.log('Query cost:', cost),
createError: (max, actual) =>
new Error(`Query is too complex: ${actual}. Maximum allowed complexity: ${max}`),
});
// 创建Apollo Server
const server = new ApolloServer({
schema,
validationRules: [complexityLimitRule],
context: ({ req }) => {
// 认证逻辑
const token = req.headers.authorization || '';
const user = authenticateUser(token);
return { user };
},
});
// 启动服务器
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});四、API网关设计
API网关是微服务架构中的重要组件,它作为系统的唯一入口,负责请求路由、负载均衡、认证授权、限流熔断等功能。
4.1 API网关的核心功能
- 请求路由:根据请求路径将请求转发到相应的服务
- 负载均衡:在多个服务实例之间进行负载均衡
- 认证授权:对请求进行认证和授权检查
- 限流熔断:对请求进行限流和熔断处理
- 监控日志:收集请求的监控和日志信息
- 协议转换:在不同协议之间进行转换
- 数据转换:对请求和响应数据进行转换
- 缓存:对频繁请求的响应进行缓存
- API组合:将多个API的响应组合成一个响应
- 安全防护:防止常见的Web攻击,如SQL注入、XSS等
4.2 API网关的架构设计
API网关的架构设计应考虑以下几个方面:
4.2.1 组件设计
API网关通常由以下几个核心组件组成:
- 请求处理器:接收和处理客户端请求
- 路由管理器:管理请求路由规则
- 过滤器链:包含多个过滤器,用于实现各种功能
- 服务发现客户端:与服务注册中心交互,获取服务实例信息
- 负载均衡器:在多个服务实例之间进行负载均衡
- 认证授权管理器:处理认证和授权逻辑
- 限流熔断管理器:处理限流和熔断逻辑
- 监控日志收集器:收集监控和日志信息
- 配置管理器:管理网关的配置信息
4.2.2 流程图
客户端请求 → API网关 → 请求解析 → 路由匹配 → 前置过滤器 → 服务调用 → 后置过滤器 → 响应处理 → 客户端
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
监控日志 配置管理 服务发现 认证授权 限流熔断 负载均衡 数据转换 缓存管理4.2.3 部署模式
API网关的部署模式包括:
- 单实例部署:简单但不适合生产环境
- 集群部署:多实例部署,提高可用性和性能
- 容器化部署:使用Docker容器化部署,便于管理和扩展
- 云原生部署:在Kubernetes等云原生平台上部署,充分利用云原生特性
4.3 API网关的实现方案
常见的API网关实现方案包括:
4.3.1 开源API网关
- Spring Cloud Gateway:Spring Cloud提供的API网关,基于Spring Boot和Spring WebFlux
- Netflix Zuul:Netflix开源的API网关,基于Java
- Kong:基于Nginx的开源API网关,支持插件扩展
- Traefik:开源的云原生反向代理和负载均衡工具
- APISIX:基于Nginx和Lua的开源API网关
4.3.2 商业API网关
- Apigee:Google提供的API管理平台
- MuleSoft Anypoint Platform:MuleSoft提供的集成平台
- Tyk:开源的API管理平台,也提供商业版本
- AWS API Gateway:AWS提供的API管理服务
- Azure API Management:Azure提供的API管理服务
4.4 API网关的最佳实践
- 合理设计路由规则:使用清晰的路由规则,便于管理和维护
- 实施细粒度的认证授权:根据API的重要性和敏感程度,实施细粒度的认证授权
- 配置合理的限流熔断策略:根据服务的性能和容量,配置合理的限流熔断策略
- 实施监控和告警:对API网关进行监控,及时发现和解决问题
- 优化性能:对API网关进行性能优化,提高响应速度和吞吐量
- 实施缓存策略:对频繁请求的响应进行缓存,减轻后端服务的压力
- 定期更新和维护:定期更新API网关的版本,修复安全漏洞和bug
- 考虑高可用性:设计高可用的API网关架构,避免单点故障
五、API文档和测试
5.1 API文档
API文档是API设计的重要组成部分,它可以帮助开发者理解和使用API。
5.1.1 API文档的重要性
- 提高开发效率:清晰的文档可以帮助开发者快速理解和使用API
- 减少沟通成本:文档可以减少开发团队之间的沟通成本
- 促进API复用:良好的文档可以促进API的复用
- 提升用户体验:对于外部API,良好的文档可以提升开发者的使用体验
- 便于维护:文档可以帮助维护团队理解API的设计意图和实现细节
5.1.2 API文档工具
常见的API文档工具包括:
- Swagger/OpenAPI:最流行的API文档工具,支持自动生成和交互式文档
- Postman:API测试工具,也支持API文档生成和分享
- GraphQL Playground:GraphQL API的交互式文档和测试工具
- RAML:RESTful API建模语言,用于描述API
- Apiary:API设计和文档平台
- ReadMe:现代化的API文档平台
- Slate:漂亮的API文档生成工具
- Docusaurus:Facebook开源的文档网站生成工具
5.1.3 OpenAPI文档示例
openapi: 3.0.3
info:
title: Product API
description: API for managing products
version: 1.0.0
servers:
- url: http://api.example.com/v1
paths:
/products:
get:
summary: Get all products
description: Retrieve a list of products
parameters:
- name: page
in: query
description: Page number
schema:
type: integer
default: 1
- name: size
in: query
description: Number of items per page
schema:
type: integer
default: 10
- name: sort
in: query
description: Sort field and direction, e.g. 'price,desc'
schema:
type: string
default: 'createdAt,asc'
- name: category
in: query
description: Filter by category
schema:
type: string
- name: price_lte
in: query
description: Maximum price
schema:
type: number
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 200
message:
type: string
example: Success
data:
type: array
items:
$ref: '#/components/schemas/Product'
post:
summary: Create a new product
description: Create a new product
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateProductRequest'
responses:
'201':
description: Product created successfully
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 201
message:
type: string
example: Product created successfully
data:
$ref: '#/components/schemas/Product'
'400':
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/products/{id}:
get:
summary: Get product by ID
description: Retrieve a product by its ID
parameters:
- name: id
in: path
required: true
description: Product ID
schema:
type: string
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 200
message:
type: string
example: Success
data:
$ref: '#/components/schemas/Product'
'404':
description: Product not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
put:
summary: Update product
description: Update an existing product
parameters:
- name: id
in: path
required: true
description: Product ID
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateProductRequest'
responses:
'200':
description: Product updated successfully
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 200
message:
type: string
example: Product updated successfully
data:
$ref: '#/components/schemas/Product'
'404':
description: Product not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
delete:
summary: Delete product
description: Delete a product by its ID
parameters:
- name: id
in: path
required: true
description: Product ID
schema:
type: string
responses:
'200':
description: Product deleted successfully
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 200
message:
type: string
example: Product deleted successfully
data:
type: boolean
example: true
'404':
description: Product not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
components:
schemas:
Product:
type: object
properties:
id:
type: string
example: '123'
name:
type: string
example: 'Smartphone'
description:
type: string
example: 'Latest model smartphone with advanced features'
price:
type: number
example: 599.99
category:
type: string
example: 'electronics'
createdAt:
type: string
format: date-time
example: '2023-01-01T12:00:00Z'
updatedAt:
type: string
format: date-time
example: '2023-01-01T12:00:00Z'
CreateProductRequest:
type: object
required:
- name
- price
- category
properties:
name:
type: string
example: 'Smartphone'
description:
type: string
example: 'Latest model smartphone with advanced features'
price:
type: number
example: 599.99
category:
type: string
example: 'electronics'
UpdateProductRequest:
type: object
properties:
name:
type: string
example: 'Smartphone'
description:
type: string
example: 'Latest model smartphone with advanced features'
price:
type: number
example: 599.99
category:
type: string
example: 'electronics'
ErrorResponse:
type: object
properties:
code:
type: integer
example: 400
message:
type: string
example: 'Bad request'
errors:
type: array
items:
type: object
properties:
field:
type: string
example: 'name'
message:
type: string
example: 'Name is required'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []5.2 API测试
API测试是确保API质量和稳定性的重要手段,常见的API测试类型包括:
5.2.1 单元测试
单元测试用于测试API的最小功能单元,如单个函数或方法。
示例(Java + JUnit):
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@InjectMocks
private ProductService productService;
@Test
public void shouldGetProductById() {
// 准备测试数据
Long productId = 1L;
Product product = new Product();
product.setId(productId);
product.setName("Smartphone");
product.setPrice(599.99);
// 配置Mock行为
when(productRepository.findById(productId)).thenReturn(Optional.of(product));
// 执行测试
Product result = productService.findById(productId);
// 验证结果
assertNotNull(result);
assertEquals(productId, result.getId());
assertEquals("Smartphone", result.getName());
assertEquals(599.99, result.getPrice(), 0.01);
// 验证Mock调用
verify(productRepository, times(1)).findById(productId);
}
@Test
public void shouldThrowExceptionWhenProductNotFound() {
// 准备测试数据
Long productId = 1L;
// 配置Mock行为
when(productRepository.findById(productId)).thenReturn(Optional.empty());
// 执行测试并验证异常
assertThrows(ProductNotFoundException.class, () -> {
productService.findById(productId);
});
// 验证Mock调用
verify(productRepository, times(1)).findById(productId);
}
@Test
public void shouldCreateProduct() {
// 准备测试数据
CreateProductRequest request = new CreateProductRequest();
request.setName("Smartphone");
request.setDescription("Latest model smartphone");
request.setPrice(599.99);
request.setCategory("electronics");
Product product = new Product();
product.setId(1L);
product.setName(request.getName());
product.setDescription(request.getDescription());
product.setPrice(request.getPrice());
product.setCategory(request.getCategory());
product.setCreatedAt(LocalDateTime.now());
product.setUpdatedAt(LocalDateTime.now());
// 配置Mock行为
when(productRepository.save(any(Product.class))).thenReturn(product);
// 执行测试
Product result = productService.createProduct(request);
// 验证结果
assertNotNull(result);
assertEquals(1L, result.getId().longValue());
assertEquals(request.getName(), result.getName());
assertEquals(request.getPrice(), result.getPrice(), 0.01);
// 验证Mock调用
verify(productRepository, times(1)).save(any(Product.class));
}
}5.2.2 集成测试
集成测试用于测试API的各个组件之间的交互。
示例(Spring Boot Test):
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDateTime;
import java.util.List;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class ProductControllerIT {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private ProductRepository productRepository;
@BeforeEach
public void setUp() {
// 清空数据库
productRepository.deleteAll();
// 初始化测试数据
Product product = new Product();
product.setName("Smartphone");
product.setDescription("Latest model smartphone");
product.setPrice(599.99);
product.setCategory("electronics");
product.setCreatedAt(LocalDateTime.now());
product.setUpdatedAt(LocalDateTime.now());
productRepository.save(product);
}
@Test
public void shouldGetAllProducts() throws Exception {
// 执行请求
mockMvc.perform(MockMvcRequestBuilders.get("/api/products")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(1))
.andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("Smartphone"))
.andExpect(MockMvcResultMatchers.jsonPath("$[0].price").value(599.99));
}
@Test
public void shouldGetProductById() throws Exception {
// 获取已存在的产品ID
Product existingProduct = productRepository.findAll().get(0);
Long productId = existingProduct.getId();
// 执行请求
mockMvc.perform(MockMvcRequestBuilders.get("/api/products/{id}", productId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(productId))
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Smartphone"));
}
@Test
public void shouldCreateProduct() throws Exception {
// 准备请求数据
CreateProductRequest request = new CreateProductRequest();
request.setName("Laptop");
request.setDescription("Gaming laptop");
request.setPrice(1299.99);
request.setCategory("electronics");
// 执行请求
mockMvc.perform(MockMvcRequestBuilders.post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").exists())
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Laptop"))
.andExpect(MockMvcResultMatchers.jsonPath("$.price").value(1299.99));
// 验证数据库中是否新增了产品
List<Product> products = productRepository.findAll();
assertEquals(2, products.size());
}
@Test
public void shouldUpdateProduct() throws Exception {
// 获取已存在的产品ID
Product existingProduct = productRepository.findAll().get(0);
Long productId = existingProduct.getId();
// 准备请求数据
UpdateProductRequest request = new UpdateProductRequest();
request.setName("Updated Smartphone");
request.setPrice(649.99);
// 执行请求
mockMvc.perform(MockMvcRequestBuilders.put("/api/products/{id}", productId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(productId))
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Updated Smartphone"))
.andExpect(MockMvcResultMatchers.jsonPath("$.price").value(649.99));
}
@Test
public void shouldDeleteProduct() throws Exception {
// 获取已存在的产品ID
Product existingProduct = productRepository.findAll().get(0);
Long productId = existingProduct.getId();
// 执行请求
mockMvc.perform(MockMvcRequestBuilders.delete("/api/products/{id}", productId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isNoContent());
// 验证数据库中是否删除了产品
List<Product> products = productRepository.findAll();
assertEquals(0, products.size());
// 验证删除后再查询会返回404
mockMvc.perform(MockMvcRequestBuilders.get("/api/products/{id}", productId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isNotFound());
}
@Test
public void shouldReturnNotFoundWhenProductDoesNotExist() throws Exception {
// 使用不存在的产品ID
Long nonExistentId = 999L;
// 执行请求
mockMvc.perform(MockMvcRequestBuilders.get("/api/products/{id}", nonExistentId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isNotFound());
}
}5.2.3 契约测试
契约测试用于确保API提供者和消费者之间的契约一致性。
示例(Spring Cloud Contract):
// 生产者契约定义
Contract.make {
description "should return product by id"
request {
method GET()
urlPath("/api/products/1") {
queryParameters {
}
}
headers {
contentType(applicationJson())
}
}
response {
status 200
headers {
contentType(applicationJson())
}
body("""
{
"id": 1,
"name": "Smartphone",
"description": "Latest model smartphone",
"price": 599.99,
"category": "electronics",
"createdAt": $(regex("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\.\\d{3}Z")),
"updatedAt": $(regex("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\.\\d{3}Z"))
}
""")
}
}5.2.4 API自动化测试工具
除了代码级别的测试外,还可以使用专业的API自动化测试工具进行测试:
- Postman:功能强大的API开发和测试平台,支持创建自动化测试集合
- SoapUI:专业的API测试工具,支持REST和SOAP API测试
- RestAssured:Java的REST API测试框架,提供流畅的API进行测试
- Karate:基于Cucumber的API测试框架,支持BDD风格的测试
- Insomnia:轻量级的API测试工具,界面简洁易用
六、总结
API设计是构建现代软件系统的关键环节,良好的API设计可以提高系统的可维护性、可扩展性和用户体验。本文介绍了API设计的核心原则、RESTful API设计、GraphQL API设计、API网关设计、API文档与测试等方面的内容,希望能够帮助开发者设计高质量的API。
API设计是一个持续演进的过程,随着业务需求的变化和技术的发展,API也需要不断优化和更新。在API设计和演进过程中,应始终遵循API设计的核心原则,保持API的一致性、可用性和安全性,同时注重API的文档化和测试,确保API的质量和稳定性。
通过合理的API设计,可以构建出更加灵活、可靠和高效的软件系统,为用户提供更好的服务和体验。
