目錄
- 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: 8080servlet:context-path: /api/v1spring:application:name: restful-api-demodatasource:url: jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTCusername: ${DB_USERNAME:root}password: ${DB_PASSWORD:password}driver-class-name: com.mysql.cj.jdbc.Driverjpa:hibernate:ddl-auto: updateshow-sql: falseproperties:hibernate:dialect: org.hibernate.dialect.MySQL8Dialectformat_sql: truejackson:default-property-inclusion: non_nulldate-format: yyyy-MM-dd HH:mm:sstime-zone: GMT+8logging:level:com.example: DEBUGorg.springframework.security: DEBUGpattern: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;}@Overridepublic 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());}@Overridepublic 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);}@Overridepublic 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 {@Beanpublic 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: 20minimum-idle: 5idle-timeout: 300000max-lifetime: 1200000connection-timeout: 20000validation-timeout: 3000leak-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;}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {return authConfig.getAuthenticationManager();}@Beanpublic 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 {@Beanpublic GroupedOpenApi publicApi() {return GroupedOpenApi.builder().group("public").pathsToMatch("/users/**", "/auth/**").build();}@Beanpublic 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)))})@PostMappingpublic ApiResponse<UserResponse> createUser(@Parameter(description = "用戶創建請求", required = true)@Valid @RequestBody CreateUserRequest request) {// 實現代碼}
}
10. 測試策略
10.1 單元測試
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {@Mockprivate UserRepository userRepository;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserMapper userMapper;@InjectMocksprivate UserServiceImpl userService;@Test@DisplayName("創建用戶 - 成功")void createUser_Success() {// GivenCreateUserRequest 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);// WhenUserResponse result = userService.createUser(request);// ThenassertThat(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() {// GivenCreateUserRequest request = new CreateUserRequest();request.setUsername("existinguser");request.setEmail("test@example.com");when(userRepository.findByUsername("existinguser")).thenReturn(Optional.of(new User()));// When & ThenassertThatThrownBy(() -> 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 {@Autowiredprivate TestRestTemplate restTemplate;@Autowiredprivate UserRepository userRepository;@Test@DisplayName("創建用戶 - 集成測試")void createUser_IntegrationTest() {// GivenCreateUserRequest 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);// WhenResponseEntity<ApiResponse> response = restTemplate.postForEntity("/users", entity, ApiResponse.class);// ThenassertThat(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 {@Autowiredprivate MockMvc mockMvc;@Autowiredprivate 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-slimLABEL maintainer="dev@example.com"VOLUME /tmpARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jarEXPOSE 8080ENTRYPOINT ["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=passworddepends_on:- mysqlnetworks:- app-networkmysql:image: mysql:8.0environment:MYSQL_ROOT_PASSWORD: passwordMYSQL_DATABASE: demo_dbports:- "3306:3306"volumes:- mysql_data:/var/lib/mysqlnetworks:- app-networkvolumes: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;}@Overridepublic 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,prometheusendpoint:health:show-details: alwaysmetrics:export:prometheus:enabled: trueinfo:env:enabled: trueinfo:app:name: RESTful API Demoversion: 1.0.0description: 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項目。