目录
- 1. 引言
- 2. RESTful API基础概念
- 3. Spring Boot环境搭建
- 4. 项目结构设计
- 5. 核心组件开发
- 6. 数据库集成
- 7. 安全认证
- 8. 异常处理
- 9. API文档生成
- 10. 测试策略
- 11. 部署与监控
- 12. 最佳实践
1. 引言
在现代软件开发中,RESTful API已成为构建分布式系统和微服务架构的标准方式。Spring Boot作为Java生态系统中最受欢迎的框架之一,为开发高质量的RESTful API提供了强大的支持。
本指南将带您从零开始,使用Spring Boot构建一个完整的企业级RESTful API项目,涵盖从基础概念到生产部署的全过程。
为什么选择Spring Boot?
- 快速开发:约定优于配置,减少样板代码
- 生态丰富:完善的Spring生态系统支持
- 生产就绪:内置监控、健康检查等企业级特性
- 社区活跃:丰富的文档和社区支持
2. RESTful API基础概念
2.1 REST架构原则
REST(Representational State Transfer)是一种软件架构风格,遵循以下核心原则:
- 无状态性:每个请求都包含处理该请求所需的所有信息
- 统一接口:使用标准的HTTP方法和状态码
- 资源导向:将数据和功能视为资源,通过URI标识
- 分层系统:支持分层架构,提高可扩展性
2.2 HTTP方法映射
HTTP方法 | 操作类型 | 示例 | 描述 |
---|---|---|---|
GET | 查询 | GET /users | 获取用户列表 |
POST | 创建 | POST /users | 创建新用户 |
PUT | 更新 | PUT /users/1 | 完整更新用户 |
PATCH | 部分更新 | PATCH /users/1 | 部分更新用户 |
DELETE | 删除 | DELETE /users/1 | 删除用户 |
2.3 HTTP状态码
- 2xx 成功:200 OK, 201 Created, 204 No Content
- 4xx 客户端错误:400 Bad Request, 401 Unauthorized, 404 Not Found
- 5xx 服务器错误:500 Internal Server Error, 503 Service Unavailable
3. Spring Boot环境搭建
3.1 开发环境要求
- JDK 11或更高版本
- Maven 3.6+或Gradle 6.8+
- IDE(推荐IntelliJ IDEA或Eclipse)
- 数据库(MySQL、PostgreSQL等)
3.2 创建Spring Boot项目
使用Spring Initializr(https://start.spring.io/)创建项目:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>restful-api-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3.3 应用配置
# application.yml
server:
port: 8080
servlet:
context-path: /api/v1
spring:
application:
name: restful-api-demo
datasource:
url: jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC
username: ${DB_USERNAME:root}
password: ${DB_PASSWORD:password}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
jackson:
default-property-inclusion: non_null
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
logging:
level:
com.example: DEBUG
org.springframework.security: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/application.log
4. 项目结构设计
4.1 推荐的包结构
src/main/java/com/example/demo/
├── DemoApplication.java # 启动类
├── config/ # 配置类
│ ├── SecurityConfig.java
│ ├── WebConfig.java
│ └── SwaggerConfig.java
├── controller/ # 控制器层
│ ├── UserController.java
│ └── ProductController.java
├── service/ # 服务层
│ ├── UserService.java
│ ├── UserServiceImpl.java
│ └── ProductService.java
├── repository/ # 数据访问层
│ ├── UserRepository.java
│ └── ProductRepository.java
├── entity/ # 实体类
│ ├── User.java
│ └── Product.java
├── dto/ # 数据传输对象
│ ├── request/
│ │ ├── CreateUserRequest.java
│ │ └── UpdateUserRequest.java
│ └── response/
│ ├── UserResponse.java
│ └── ApiResponse.java
├── exception/ # 异常处理
│ ├── GlobalExceptionHandler.java
│ ├── BusinessException.java
│ └── ResourceNotFoundException.java
└── util/ # 工具类
├── DateUtil.java
└── ValidationUtil.java
4.2 分层架构说明
- Controller层:处理HTTP请求,参数验证,调用Service层
- Service层:业务逻辑处理,事务管理
- Repository层:数据访问,与数据库交互
- Entity层:数据库实体映射
- DTO层:数据传输对象,API输入输出
5. 核心组件开发
5.1 实体类设计
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(name = "full_name")
private String fullName;
@Enumerated(EnumType.STRING)
private UserStatus status;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
public enum UserStatus {
ACTIVE, INACTIVE, SUSPENDED
}
5.2 数据传输对象
// 创建用户请求
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码长度不能少于6位")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
private String fullName;
}
// 用户响应
@Data
@Builder
public class UserResponse {
private Long id;
private String username;
private String email;
private String fullName;
private UserStatus status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
// 统一API响应
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private String timestamp;
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.success(true)
.message("操作成功")
.data(data)
.timestamp(LocalDateTime.now().toString())
.build();
}
public static <T> ApiResponse<T> error(String message) {
return ApiResponse.<T>builder()
.success(false)
.message(message)
.timestamp(LocalDateTime.now().toString())
.build();
}
}
5.3 Repository层
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
List<User> findByStatus(UserStatus status);
@Query("SELECT u FROM User u WHERE u.fullName LIKE %:name%")
List<User> findByFullNameContaining(@Param("name") String name);
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateUserStatus(@Param("id") Long id, @Param("status") UserStatus status);
}
5.4 Service层
public interface UserService {
UserResponse createUser(CreateUserRequest request);
UserResponse getUserById(Long id);
UserResponse getUserByUsername(String username);
List<UserResponse> getAllUsers();
UserResponse updateUser(Long id, UpdateUserRequest request);
void deleteUser(Long id);
List<UserResponse> searchUsersByName(String name);
}
@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserMapper userMapper;
public UserServiceImpl(UserRepository userRepository,
PasswordEncoder passwordEncoder,
UserMapper userMapper) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.userMapper = userMapper;
}
@Override
public UserResponse createUser(CreateUserRequest request) {
log.info("Creating user with username: {}", request.getUsername());
// 检查用户名是否已存在
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
throw new BusinessException("用户名已存在");
}
// 检查邮箱是否已存在
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
throw new BusinessException("邮箱已存在");
}
User user = User.builder()
.username(request.getUsername())
.password(passwordEncoder.encode(request.getPassword()))
.email(request.getEmail())
.fullName(request.getFullName())
.status(UserStatus.ACTIVE)
.build();
User savedUser = userRepository.save(user);
log.info("User created successfully with id: {}", savedUser.getId());
return userMapper.toResponse(savedUser);
}
@Override
@Transactional(readOnly = true)
public UserResponse getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("用户不存在,ID: " + id));
return userMapper.toResponse(user);
}
@Override
@Transactional(readOnly = true)
public UserResponse getUserByUsername(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("用户不存在,用户名: " + username));
return userMapper.toResponse(user);
}
@Override
@Transactional(readOnly = true)
public List<UserResponse> getAllUsers() {
List<User> users = userRepository.findAll();
return users.stream()
.map(userMapper::toResponse)
.collect(Collectors.toList());
}
@Override
public UserResponse updateUser(Long id, UpdateUserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("用户不存在,ID: " + id));
if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) {
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
throw new BusinessException("邮箱已存在");
}
user.setEmail(request.getEmail());
}
if (request.getFullName() != null) {
user.setFullName(request.getFullName());
}
User updatedUser = userRepository.save(user);
return userMapper.toResponse(updatedUser);
}
@Override
public void deleteUser(Long id) {
if (!userRepository.existsById(id)) {
throw new ResourceNotFoundException("用户不存在,ID: " + id);
}
userRepository.deleteById(id);
log.info("User deleted successfully with id: {}", id);
}
@Override
@Transactional(readOnly = true)
public List<UserResponse> searchUsersByName(String name) {
List<User> users = userRepository.findByFullNameContaining(name);
return users.stream()
.map(userMapper::toResponse)
.collect(Collectors.toList());
}
}
5.5 Controller层
@RestController
@RequestMapping("/users")
@Validated
@Slf4j
@CrossOrigin(origins = "*")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "创建用户", description = "创建新的用户账户")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "用户创建成功"),
@ApiResponse(responseCode = "400", description = "请求参数无效"),
@ApiResponse(responseCode = "409", description = "用户名或邮箱已存在")
})
public ApiResponse<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
log.info("Received request to create user: {}", request.getUsername());
UserResponse user = userService.createUser(request);
return ApiResponse.success(user);
}
@GetMapping("/{id}")
@Operation(summary = "根据ID获取用户", description = "通过用户ID获取用户详细信息")
public ApiResponse<UserResponse> getUserById(@PathVariable @Min(1) Long id) {
UserResponse user = userService.getUserById(id);
return ApiResponse.success(user);
}
@GetMapping
@Operation(summary = "获取用户列表", description = "获取所有用户的列表")
public ApiResponse<List<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "10") @Min(1) @Max(100) int size) {
List<UserResponse> users = userService.getAllUsers();
return ApiResponse.success(users);
}
@GetMapping("/search")
@Operation(summary = "搜索用户", description = "根据姓名搜索用户")
public ApiResponse<List<UserResponse>> searchUsers(
@RequestParam @NotBlank String name) {
List<UserResponse> users = userService.searchUsersByName(name);
return ApiResponse.success(users);
}
@PutMapping("/{id}")
@Operation(summary = "更新用户", description = "更新用户信息")
public ApiResponse<UserResponse> updateUser(
@PathVariable @Min(1) Long id,
@Valid @RequestBody UpdateUserRequest request) {
UserResponse user = userService.updateUser(id, request);
return ApiResponse.success(user);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "删除用户", description = "根据ID删除用户")
public ApiResponse<Void> deleteUser(@PathVariable @Min(1) Long id) {
userService.deleteUser(id);
return ApiResponse.success(null);
}
}
6. 数据库集成
6.1 JPA配置
@Configuration
@EnableJpaRepositories(basePackages = "com.example.demo.repository")
@EnableJpaAuditing
public class JpaConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.of("system");
}
return Optional.of(authentication.getName());
};
}
}
6.2 数据库迁移
使用Flyway进行数据库版本管理:
-- V1__Create_users_table.sql
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
full_name VARCHAR(100),
status ENUM('ACTIVE', 'INACTIVE', 'SUSPENDED') DEFAULT 'ACTIVE',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_status (status)
);
6.3 连接池配置
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
max-lifetime: 1200000
connection-timeout: 20000
validation-timeout: 3000
leak-detection-threshold: 60000
7. 安全认证
7.1 Spring Security配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtRequestFilter jwtRequestFilter;
public SecurityConfig(JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
JwtRequestFilter jwtRequestFilter) {
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtRequestFilter = jwtRequestFilter;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers(HttpMethod.GET, "/users/**").hasAnyRole("USER", "ADMIN")
.requestMatchers(HttpMethod.POST, "/users").hasRole("ADMIN")
.requestMatchers(HttpMethod.PUT, "/users/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/users/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex.authenticationEntryPoint(jwtAuthenticationEntryPoint))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
7.2 JWT工具类
@Component
public class JwtUtil {
private String secret = "mySecretKey";
private int jwtExpiration = 86400; // 24小时
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
}
8. 异常处理
8.1 自定义异常类
public class BusinessException extends RuntimeException {
private final String code;
public BusinessException(String message) {
super(message);
this.code = "BUSINESS_ERROR";
}
public BusinessException(String code, String message) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
}
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
public class ValidationException extends RuntimeException {
private final Map<String, String> errors;
public ValidationException(Map<String, String> errors) {
super("Validation failed");
this.errors = errors;
}
public Map<String, String> getErrors() {
return errors;
}
}
8.2 全局异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiResponse<Void> handleResourceNotFoundException(ResourceNotFoundException ex) {
log.error("Resource not found: {}", ex.getMessage());
return ApiResponse.error(ex.getMessage());
}
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Void> handleBusinessException(BusinessException ex) {
log.error("Business error: {}", ex.getMessage());
return ApiResponse.error(ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Map<String, String>> handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
log.error("Validation error: {}", errors);
return ApiResponse.<Map<String, String>>builder()
.success(false)
.message("参数验证失败")
.data(errors)
.timestamp(LocalDateTime.now().toString())
.build();
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Map<String, String>> handleConstraintViolationException(
ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(violation -> {
String propertyPath = violation.getPropertyPath().toString();
String message = violation.getMessage();
errors.put(propertyPath, message);
});
return ApiResponse.<Map<String, String>>builder()
.success(false)
.message("参数验证失败")
.data(errors)
.timestamp(LocalDateTime.now().toString())
.build();
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiResponse<Void> handleGenericException(Exception ex) {
log.error("Unexpected error occurred", ex);
return ApiResponse.error("系统内部错误,请稍后重试");
}
}
9. API文档生成
9.1 Swagger配置
@Configuration
@OpenAPIDefinition(
info = @Info(
title = "RESTful API Demo",
version = "1.0.0",
description = "Spring Boot RESTful API示例项目",
contact = @Contact(
name = "开发团队",
email = "dev@example.com"
)
),
servers = {
@Server(url = "http://localhost:8080/api/v1", description = "开发环境"),
@Server(url = "https://api.example.com/v1", description = "生产环境")
}
)
@SecurityScheme(
name = "bearerAuth",
type = SecuritySchemeType.HTTP,
bearerFormat = "JWT",
scheme = "bearer"
)
public class SwaggerConfig {
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public")
.pathsToMatch("/users/**", "/auth/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/admin/**")
.build();
}
}
9.2 API文档注解示例
@Tag(name = "用户管理", description = "用户相关的API接口")
@RestController
@RequestMapping("/users")
public class UserController {
@Operation(
summary = "创建用户",
description = "创建新的用户账户,需要管理员权限",
security = @SecurityRequirement(name = "bearerAuth")
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "201",
description = "用户创建成功",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = UserResponse.class)
)
),
@ApiResponse(
responseCode = "400",
description = "请求参数无效",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResponse.class)
)
)
})
@PostMapping
public ApiResponse<UserResponse> createUser(
@Parameter(description = "用户创建请求", required = true)
@Valid @RequestBody CreateUserRequest request) {
// 实现代码
}
}
10. 测试策略
10.1 单元测试
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private UserMapper userMapper;
@InjectMocks
private UserServiceImpl userService;
@Test
@DisplayName("创建用户 - 成功")
void createUser_Success() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setUsername("testuser");
request.setPassword("password123");
request.setEmail("test@example.com");
User savedUser = User.builder()
.id(1L)
.username("testuser")
.email("test@example.com")
.status(UserStatus.ACTIVE)
.build();
UserResponse expectedResponse = UserResponse.builder()
.id(1L)
.username("testuser")
.email("test@example.com")
.status(UserStatus.ACTIVE)
.build();
when(userRepository.findByUsername("testuser")).thenReturn(Optional.empty());
when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.empty());
when(passwordEncoder.encode("password123")).thenReturn("encodedPassword");
when(userRepository.save(any(User.class))).thenReturn(savedUser);
when(userMapper.toResponse(savedUser)).thenReturn(expectedResponse);
// When
UserResponse result = userService.createUser(request);
// Then
assertThat(result).isNotNull();
assertThat(result.getUsername()).isEqualTo("testuser");
assertThat(result.getEmail()).isEqualTo("test@example.com");
verify(userRepository).findByUsername("testuser");
verify(userRepository).findByEmail("test@example.com");
verify(userRepository).save(any(User.class));
}
@Test
@DisplayName("创建用户 - 用户名已存在")
void createUser_UsernameExists_ThrowsException() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setUsername("existinguser");
request.setEmail("test@example.com");
when(userRepository.findByUsername("existinguser"))
.thenReturn(Optional.of(new User()));
// When & Then
assertThatThrownBy(() -> userService.createUser(request))
.isInstanceOf(BusinessException.class)
.hasMessage("用户名已存在");
verify(userRepository).findByUsername("existinguser");
verify(userRepository, never()).save(any(User.class));
}
}
10.2 集成测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-test.properties")
@Transactional
class UserControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("创建用户 - 集成测试")
void createUser_IntegrationTest() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setUsername("integrationtest");
request.setPassword("password123");
request.setEmail("integration@example.com");
request.setFullName("Integration Test");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<CreateUserRequest> entity = new HttpEntity<>(request, headers);
// When
ResponseEntity<ApiResponse> response = restTemplate.postForEntity(
"/users", entity, ApiResponse.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().isSuccess()).isTrue();
// 验证数据库中的数据
Optional<User> savedUser = userRepository.findByUsername("integrationtest");
assertThat(savedUser).isPresent();
assertThat(savedUser.get().getEmail()).isEqualTo("integration@example.com");
}
}
10.3 API测试
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestMethodOrder(OrderAnnotation.class)
class UserApiTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
@Order(1)
@DisplayName("API测试 - 创建用户")
void testCreateUser() throws Exception {
CreateUserRequest request = new CreateUserRequest();
request.setUsername("apitest");
request.setPassword("password123");
request.setEmail("api@example.com");
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.username").value("apitest"))
.andExpect(jsonPath("$.data.email").value("api@example.com"));
}
@Test
@Order(2)
@DisplayName("API测试 - 获取用户列表")
void testGetAllUsers() throws Exception {
mockMvc.perform(get("/users")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data").isArray());
}
}
11. 部署与监控
11.1 Docker化部署
# Dockerfile
FROM openjdk:17-jdk-slim
LABEL maintainer="dev@example.com"
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
- DB_HOST=mysql
- DB_USERNAME=root
- DB_PASSWORD=password
depends_on:
- mysql
networks:
- app-network
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: demo_db
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
networks:
- app-network
volumes:
mysql_data:
networks:
app-network:
driver: bridge
11.2 健康检查
@Component
public class CustomHealthIndicator implements HealthIndicator {
private final UserRepository userRepository;
public CustomHealthIndicator(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public Health health() {
try {
long userCount = userRepository.count();
return Health.up()
.withDetail("userCount", userCount)
.withDetail("status", "Database connection is healthy")
.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}
11.3 监控配置
# application.yml - 监控配置
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
export:
prometheus:
enabled: true
info:
env:
enabled: true
info:
app:
name: RESTful API Demo
version: 1.0.0
description: Spring Boot RESTful API示例项目
11.4 日志配置
<!-- logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProfile name="!prod">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<springProfile name="prod">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</springProfile>
</configuration>
12. 最佳实践
12.1 API设计原则
-
RESTful设计
- 使用名词而非动词作为资源名称
- 使用HTTP方法表示操作类型
- 使用HTTP状态码表示操作结果
-
版本控制
- 在URL中包含版本号:
/api/v1/users
- 使用语义化版本控制
- 保持向后兼容性
- 在URL中包含版本号:
-
错误处理
- 统一的错误响应格式
- 有意义的错误消息
- 适当的HTTP状态码
-
安全性
- 使用HTTPS传输
- 实施认证和授权
- 输入验证和输出编码
- 防止SQL注入和XSS攻击
12.2 性能优化
-
数据库优化
- 合理使用索引
- 避免N+1查询问题
- 使用连接池
- 实施缓存策略
-
缓存策略
@Service public class UserService { @Cacheable(value = "users", key = "#id") public UserResponse getUserById(Long id) { // 实现代码 } @CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { // 实现代码 } }
-
分页处理
@GetMapping public ApiResponse<Page<UserResponse>> getAllUsers( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "id") String sortBy, @RequestParam(defaultValue = "asc") String sortDir) { Sort sort = sortDir.equalsIgnoreCase("desc") ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending(); Pageable pageable = PageRequest.of(page, size, sort); Page<User> users = userRepository.findAll(pageable); Page<UserResponse> userResponses = users.map(userMapper::toResponse); return ApiResponse.success(userResponses); }
12.3 代码质量
-
代码规范
- 使用一致的命名约定
- 编写清晰的注释
- 保持方法简洁
- 遵循SOLID原则
-
测试覆盖率
- 单元测试覆盖率 > 80%
- 集成测试覆盖关键业务流程
- 使用测试驱动开发(TDD)
-
文档维护
- 保持API文档更新
- 编写详细的README
- 提供使用示例
12.4 部署策略
-
环境管理
- 开发、测试、生产环境分离
- 使用配置文件管理不同环境
- 实施CI/CD流水线
-
监控告警
- 应用性能监控(APM)
- 日志聚合和分析
- 业务指标监控
- 告警机制设置
总结
本指南详细介绍了使用Spring Boot构建企业级RESTful API的完整流程,从基础概念到生产部署,涵盖了开发过程中的各个重要环节。
关键要点回顾
- 架构设计:采用分层架构,职责分离明确
- 安全性:实施JWT认证,角色权限控制
- 数据处理:使用JPA进行数据持久化,合理设计实体关系
- 异常处理:统一异常处理机制,友好的错误提示
- API文档:使用Swagger生成交互式文档
- 测试策略:完善的单元测试和集成测试
- 部署运维:Docker化部署,完善的监控体系
后续学习建议
- 微服务架构:学习Spring Cloud,构建分布式系统
- 消息队列:集成RabbitMQ或Kafka处理异步任务
- 缓存优化:深入学习Redis缓存策略
- 性能调优:JVM调优,数据库性能优化
- DevOps实践:CI/CD流水线,自动化部署
通过本指南的学习和实践,您应该能够独立构建高质量的企业级RESTful API项目。