构建标准且高效的REST风格API
REST (Representational State Transfer) 是一种软件架构风格,用于创建可扩展的Web服务。RESTful API是基于REST架构的应用程序接口,它使用HTTP协议的方法来执行操作。
特性 | REST | SOAP |
---|---|---|
协议 | 使用HTTP/HTTPS | 独立于协议,可以使用HTTP、SMTP等 |
数据格式 | 通常使用JSON或XML | 仅使用XML |
带宽占用 | 较低 | 较高 |
学习曲线 | 简单易学 | 相对复杂 |
缓存 | 可以利用HTTP缓存 | 需要自定义实现 |
安全性 | 使用HTTPS和认证机制 | 内置安全标准(WS-Security) |
一个良好的RESTful API设计始于资源的命名:
良好的资源命名示例:
/users
- 获取用户列表/users/123
- 获取ID为123的用户/users/123/orders
- 获取ID为123的用户的订单列表/users/123/orders/456
- 获取ID为123的用户的ID为456的订单不推荐的资源命名示例:
/getUsers
- 在URI中使用了动词/user/123/createOrder
- 在URI中使用了动词/api/v1/getUserOrders?userId=123
- 混合使用路径参数和查询参数表示资源RESTful API充分利用HTTP方法(也称为HTTP动词)来表达对资源的操作意图:
HTTP方法 | 操作 | 描述 | 是否幂等 |
---|---|---|---|
GET | 读取(Read) | 获取资源,不应该对资源状态有任何影响 | 是 |
POST | 创建(Create) | 创建新资源 | 否 |
PUT | 更新(Update) | 更新已存在的资源(全量更新) | 是 |
DELETE | 删除(Delete) | 删除资源 | 是 |
PATCH | 部分更新(Partial Update) | 对资源进行部分更新 | 否 |
幂等性(Idempotence)是指多次执行相同的操作,结果都是相同的。例如,多次执行相同的GET请求,结果应该是一致的;多次执行相同的PUT请求,资源的状态也应该是一致的。
合理使用HTTP状态码可以提供清晰的操作结果反馈:
状态码 | 描述 | 场景示例 |
---|---|---|
200 OK | 请求成功 | GET请求成功返回数据 |
201 Created | 资源创建成功 | POST请求成功创建资源 |
204 No Content | 请求成功但无返回内容 | DELETE请求成功 |
400 Bad Request | 客户端请求有错误 | 请求参数不符合要求 |
401 Unauthorized | 未认证 | 用户未登录 |
403 Forbidden | 没有权限 | 用户已认证但权限不足 |
404 Not Found | 资源不存在 | 请求的资源不存在 |
405 Method Not Allowed | 方法不允许 | 资源不支持该HTTP方法 |
409 Conflict | 资源冲突 | 更新资源时版本冲突 |
429 Too Many Requests | 请求过多 | 客户端超出了请求限制 |
500 Internal Server Error | 服务器内部错误 | 服务端代码异常 |
API版本控制是确保向后兼容性和平滑升级的关键。常见的版本控制策略包括:
/api/v1/users
/api/users?version=1
Accept: application/vnd.example.v1+json
X-API-Version: 1
最佳实践:URI路径版本控制是最直观和常用的方式,但HTTP头版本控制更符合RESTful原则(资源不应该随版本而变化)。
以下是使用Spring Boot构建RESTful API的基本环境配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
# application.properties 或 application.yml
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@JsonIgnore
private String password;
private LocalDateTime createdAt = LocalDateTime.now();
}
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByEmail(String email);
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public List<User> getAllUsers() {
return userRepository.findAll();
}
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
public User createUser(User user) {
if (userRepository.existsByEmail(user.getEmail())) {
throw new RuntimeException("邮箱已被使用");
}
return userRepository.save(user);
}
public Optional<User> updateUser(Long id, User user) {
return userRepository.findById(id)
.map(existingUser -> {
existingUser.setUsername(user.getUsername());
existingUser.setEmail(user.getEmail());
return userRepository.save(existingUser);
});
}
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User created = userService.createUser(user);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User user) {
return userService.updateUser(id, user)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.badRequest().body(errors);
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, String>> handleRuntimeException(RuntimeException ex) {
Map<String, String> error = Map.of("message", ex.getMessage());
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> handleGeneralExceptions(Exception ex) {
Map<String, String> error = Map.of("message", "发生了一个错误");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
使用Springdoc-OpenAPI集成Swagger来自动生成API文档:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.9</version>
</dependency>
@Configuration
public class OpenAPIConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("用户管理API")
.version("1.0")
.description("用户管理系统的RESTful API文档")
.contact(new Contact()
.name("开发团队")
.email("dev@example.com")
.url("https://example.com")));
}
}
通过访问 http://localhost:8080/swagger-ui.html
即可查看生成的API文档。
对于返回大量数据的API,应实现分页与排序功能:
@GetMapping
public ResponseEntity<Page<User>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
Page<User> users = userService.getUsers(pageable);
return ResponseEntity.ok(users);
}
HATEOAS(Hypermedia as the Engine of Application State)是REST应用程序架构的一个约束,它使API具有自描述性。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
@GetMapping("/{id}")
public EntityModel<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id)
.orElseThrow(() -> new ResourceNotFoundException("未找到ID为: " + id + "的用户"));
return EntityModel.of(user,
linkTo(methodOn(UserController.class).getUserById(id)).withSelfRel(),
linkTo(methodOn(UserController.class).getAllUsers()).withRel("users"));
}
为保护API免受过载,可实现限流与熔断机制:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Configuration
public class ResilienceConfig {
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(3)).build())
.circuitBreakerConfig(CircuitBreakerConfig.custom()
.slidingWindowSize(10)
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(10))
.permittedNumberOfCallsInHalfOpenState(5)
.build())
.build());
}
@Bean
public RateLimiterRegistry rateLimiterRegistry() {
RateLimiterConfig config = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(10)
.timeoutDuration(Duration.ofMillis(100))
.build();
return RateLimiterRegistry.of(config);
}
}
@GetMapping
@RateLimiter(name = "userApi")
@CircuitBreaker(name = "userApi", fallbackMethod = "fallbackGetAllUsers")
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
public ResponseEntity<List<User>> fallbackGetAllUsers(Exception ex) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body(Collections.emptyList());
}
RESTful API应支持多种内容格式,如JSON和XML:
# application.properties
spring.mvc.contentnegotiation.favor-parameter=true
spring.mvc.contentnegotiation.parameter-name=format
spring.mvc.contentnegotiation.media-types.json=application/json
spring.mvc.contentnegotiation.media-types.xml=application/xml
添加XML支持依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/v1/auth/**").permitAll()
.antMatchers(HttpMethod.GET, "/api/v1/**").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
@Component
public class JwtTokenProvider {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.jwt.expiration}")
private int jwtExpiration;
public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes()), SignatureAlgorithm.HS512)
.compact();
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes()))
.build()
.parseClaimsJws(token);
return true;
} catch (Exception ex) {
return false;
}
}
}
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
/api/getUsers
而非/api/users
RESTful API设计是现代Web应用开发的基础。通过遵循REST架构风格和本教程中介绍的最佳实践,可以构建出高效、可扩展、易于理解和维护的API。
关键要点回顾:
持续学习:API设计是一个不断发展的领域,建议关注最新的RESTful API设计趋势和最佳实践,如GraphQL、API网关等技术。