目录
- Swagger/OpenAPI集成
- API文档生成与管理
- API First开发实践
- 自动生成客户端代码
- API契约测试
- MockMvc集成测试
- API性能测试
- API监控与告警
- 文档版本管理
- 最佳实践
- 总结
Swagger/OpenAPI集成
Swagger配置
SpringDoc OpenAPI配置:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.15</version>
</dependency>
OpenAPI配置类:
@Configuration
@EnableOpenApi
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI(@Value("${spring.application.version:1.0.0}") String appVersion,
@Value("${spring.application.name:Spring MVC App}") String appName) {
return new OpenAPI()
.info(new Info()
.title(appName + " API")
.version(appVersion)
.description("基于Spring MVC的企业级应用API文档")
.contact(new Contact()
.name("开发团队")
.email("dev@company.com")
.url("https://company.com"))
.license(new License()
.name("MIT License")
.url("https://opensource.org/licenses/MIT"))
.termsOfService("https://company.com/terms"))
.components(new Components()
.addSecuritySchemes("bearer-jwt", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name("Authorization"))
.addSchemas("Error", new Schema<>()
.type("object")
.addProperty("timestamp", new Schema<>().type("string").format("date-time"))
.addProperty("status", new Schema<>().type("integer"))
.addProperty("error", new Schema<>().type("string"))
.addProperty("message", new Schema<>().type("string"))
.addProperty("path", new Schema<>().type("string")))
.addSchemas("ApiResponse", new Schema<>()
.type("object")
.addProperty("success", new Schema<>().type("boolean"))
.addProperty("message", new Schema<>().type("string"))
.addProperty("data", new Schema<>().type("object"))
.addProperty("timestamp", new Schema<>().type("string").format("date-time"))))
.addSecurityItem(new SecurityRequirement().addList("bearer-jwt"))
.externalDocs(new ExternalDocumentation()
.description("完整文档")
.url("https://docs.company.com"));
}
@Bean
public OpenApiCustomizer openApiCustomiser() {
return openApi -> {
// 添加全局响应
openApi.getPaths().values().forEach(pathItem -> {
pathItem.readOperations().forEach(operation -> {
operation.addTagsItem("API");
// 添加通用响应
operation.getResponses().addApiResponse(
"400",
new ApiResponse()
.description("请求参数错误")
.content(new MediaType()
.schema(new Schema<>().$ref("#/components/schemas/Error"))));
operation.getResponses().addApiResponse(
"401",
new ApiResponse()
.description("未授权访问")
.content(new MediaType()
.schema(new Schema<>().$ref("#/components/schemas/Error"))));
operation.getResponses().addApiResponse(
"500",
new ApiResponse()
.description("服务器内部错误")
.content(new MediaType()
.schema(new Schema<>().$ref("#/components/schemas/Error"))));
});
});
};
}
}
控制器文档注解
用户管理API文档:
@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "用户管理", description = "用户相关操作API")
@SecurityRequirement(name = "bearer-jwt")
public class UserController {
@Autowired
private UserService userService;
@Operation(
summary = "获取用户列表",
description = "分页获取系统中的所有用户,支持搜索和过滤",
tags = {"用户管理"}
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "成功获取用户列表",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = UserResponseDto.class),
examples = @Example(
summary = "用户列表示例",
description = "包含用户基本信息和分页信息的响应",
value = """
{
"content": [
{
"id": 1,
"username": "john.doe",
"email": "john.doe@example.com",
"firstName": "John",
"lastName": "Doe",
"status": "ACTIVE",
"role": "USER",
"createdDate": "2024-01-15T10:30:00Z"
}
],
"totalElements": 100,
"totalPages": 10,
"size": 10,
"number": 0,
"first": true,
"last": false
}
"""
)
)
),
@ApiResponse(responseCode = "401", description = "未授权访问"),
@ApiResponse(responseCode = "403", description = "权限不足")
})
@GetMapping
public ResponseEntity<PageResult<UserResponseDto>> getUsers(
@Parameter(
description = "页码,从0开始",
example = "0",
required = false
)
@RequestParam(defaultValue = "0")
@Min(0) int page,
@Parameter(
description = "每页数量",
example = "20",
required = false
)
@RequestParam(defaultValue = "20")
@Min(1) @Max(100) int size,
@Parameter(
description = "搜索关键词,支持用户名、邮箱搜索",
example = "john",
required = false
)
@RequestParam(required = false) String search,
@Parameter(
description = "用户状态过滤",
example = "ACTIVE",
required = false
)
@RequestParam(required = false) UserStatus status,
@Parameter(
description = "排序字段",
example = "createdDate",
required = false
)
@RequestParam(defaultValue = "createdDate") String sortBy,
@Parameter(
description = "排序方向",
example = "desc",
required = false
)
@RequestParam(defaultValue = "desc") Sort.Direction sortDir
) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortDir, sortBy));
PageResult<User> users = userService.searchUsers(search, status, pageable);
PageResult<UserResponseDto> response = userMapper.toResponsePages(users);
return ResponseEntity.ok(response);
}
@Operation(
summary = "根据ID获取用户详情",
description = "通过用户ID获取用户的详细信息",
tags = {"用户管理"}
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "成功获取用户详情",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = UserDetailDto.class)
)
),
@ApiResponse(responseCode = "404", description = "用户不存在"),
@ApiResponse(responseCode = "403", description = "无权限访问")
})
@GetMapping("/{id}")
public ResponseEntity<UserDetailDto> getUserById(
@Parameter(
description = "用户ID",
example = "1",
required = true
)
@PathVariable
@NotNull Long id
) {
User user = userService.findById(id);
UserDetailDto response = userMapper.toDetailDto(user);
return ResponseEntity.ok(response);
}
@Operation(
summary = "创建新用户",
description = "管理员创建新用户账号",
tags = {"用户管理"}
)
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<UserResponseDto> createUser(
@Parameter(description = "用户创建请求", required = true)
@Valid @RequestBody UserCreateRequestDto request
) {
User user = userMapper.toEntity(request);
User savedUser = userService.createUser(user);
UserResponseDto response = userMapper.toResponseDto(savedUser);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@Operation(
summary = "更新用户信息",
description = "更新用户的基本信息和设置",
tags = {"用户管理"}
)
@PutMapping("/{id}")
public ResponseEntity<UserResponseDto> updateUser(
@Parameter(description = "用户ID", required = true)
@PathVariable @NotNull Long id,
@Parameter(description = "用户更新请求", required = true)
@Valid @RequestBody UserUpdateRequestDto request
) {
User user = userService.findById(id);
userMapper.updateEntity(user, request);
User updatedUser = userService.updateUser(user);
UserResponseDto response = userMapper.toResponseDto(updatedUser);
return ResponseEntity.ok(response);
}
@Operation(
summary = "删除用户",
description = "软删除用户账号",
tags = {"用户管理"}
)
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(
@Parameter(description = "用户ID", required = true)
@PathVariable @NotNull Long id
) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
DTO文档注解
数据传输对象文档:
@Schema(description = "用户响应DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserResponseDto {
@Schema(description = "用户ID", example = "1", accessMode = Schema.AccessMode.READ_ONLY)
private Long id;
@Schema(description = "用户名", example = "john.doe", required = true)
private String username;
@Schema(description = "邮箱地址", example = "john.doe@example.com", required = true)
private String email;
@Schema(description = "名字", example = "John")
private String firstName;
@Schema(description = "姓氏", example = "Doe")
private String lastName;
@Schema(description = "用户状态", example = "ACTIVE", implementation = UserStatus.class)
private UserStatus status;
@Schema(description = "用户角色", example = "USER", implementation = UserRole.class)
private UserRole role;
@Schema(description = "头像URL", example = "https://example.com/avatar.jpg")
private String avatarUrl;
@Schema(description = "创建时间", example = "2024-01-15T10:30:00Z", accessMode = Schema.AccessMode.READ_ONLY)
private LocalDateTime createdDate;
@Schema(description = "最后登录时间", example = "2024-01-15T14:20:00Z", accessMode = Schema.AccessMode.READ_ONLY)
private LocalDateTime lastLogin;
}
@Schema(description = "用户创建请求DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserCreateRequestDto {
@Schema(description = "用户名", example = "john.doe", required = true, minLength = 3, maxLength = 50)
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在3-50字符之间")
@Pattern(regexp = "^[a-zA-Z0-9._-]+$", message = "用户名只能包含字母、数字、点、下划线和连字符")
private String username;
@Schema(description = "邮箱地址", example = "john.doe@example.com", required = true)
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@Schema(description = "密码", example = "SecurePassword123!", required = true, minLength = 8)
@NotBlank(message = "密码不能为空")
@Size(min = 8, message = "密码至少8个字符")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&].{8,}$",
message = "密码必须包含大小写字母、数字和特殊字符")
private String password;
@Schema(description = "名字", example = "John", maxLength = 50)
@Size(max = 50, message = "名字不能超过50个字符")
private String firstName;
@Schema(description = "姓氏", example = "Doe", maxLength = 50)
@Size(max = 50, message = "用户名不能超过50个字符")
private String lastName;
@Schema(description = "电话号码", example = "+86 138 0013 8000")
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "电话号码格式不正确")
private String phone;
@Schema(description = "用户角色", example = "USER")
@NotNull(message = "用户角色不能为空")
private UserRole role;
@Schema(description = "用户状态", example = "ACTIVE")
private UserStatus status = UserStatus.ACTIVE;
}
API文档生成与管理
自动文档生成
配置类文档管理:
@Component
public class DocumentGenerator {
@Autowired
private OpenAPIService openAPIService;
@EventListener
@Async
public void handleApplicationReady(ApplicationReadyEvent event) {
generateApiDocumentation();
generateClientSdks();
}
private void generateApiDocumentation() {
try {
// 生成OpenAPI规范文档
String openapiSpec = openAPIService.getOpenApiSpec();
// 保存到文件
Path docsPath = Paths.get("docs/openapi.json");
Files.createDirectories(docsPath.getParent());
Files.write(docsPath, openapiSpec.getBytes());
// 生成多种格式文档
generateMarkdownDocs(openapiSpec);
generateYamlDocs(openapiSpec);
generateHtmlDocs(openapiSpec);
logger.info("API文档生成完成: docs/");
} catch (Exception e) {
logger.error("API文档生成失败", e);
}
}
private void generateMarkdownDocs(String openapiSpec) throws IOException {
OpenAPI openAPI = new OpenAPIParser()
.readContents(openapiSpec, null, new ParseOptions())
.getOpenAPI();
MarkdownGenerator generator = new MarkdownGenerator();
String markdownDocs = generator.generate(openAPI);
Files.write(Paths.get("docs/api.md"), markdownDocs.getBytes());
}
private void generateYamlDocs(String openapiSpec) throws IOException {
OpenAPI openAPI = Json.mapper().readValue(openapiSpec, OpenAPI.class);
String yamlSpec = Yaml.mapper().writeValueAsString(openAPI);
Files.write(Paths.get("docs/openapi.yaml"), yamlSpec.getBytes());
}
}
文档验证
API规范验证器:
@Component
public class ApiSpecValidator {
private final Validator validator;
public ApiSpecValidator() {
this.validator = Validation.buildDefaultValidatorFactory().getValidator();
}
@EventListener
public void validateApiSpec(ApplicationReadyEvent event) {
try {
OpenAPI openAPI = openAPIService.getOpenAPI();
validateOpenAPISpec(openAPI);
log.info("API规范验证通过");
} catch (Exception e) {
log.error("API规范验证失败", e);
throw new IllegalStateException("API规范不符合要求", e);
}
}
private void validateOpenAPISpec(OpenAPI openAPI) {
List<String> errors = new ArrayList<>();
// 验证必需字段
if (openAPI.getInfo() == null) {
errors.add("缺少API信息");
}
if (openAPI.getPaths() == null || openAPI.getPaths().isEmpty()) {
errors.add("缺少API路径定义");
}
// 验证每个端点
openAPI.getPaths().values().forEach(pathItem -> {
validatePathItem(pathItem, errors);
});
// 验证组件
if (openAPI.getComponents() != null) {
validateComponents(openAPI.getComponents(), errors);
}
if (!errors.isEmpty()) {
throw new ValidationException("API规范验证失败: " + String.join(", ", errors));
}
}
private void validatePathItem(PathItem pathItem, List<String> errors) {
List<Operation> operations = Arrays.asList(
pathItem.getGet(), pathItem.getPost(), pathItem.getPut(),
pathItem.getDelete(), pathItem.getPatch(), pathItem.getHead(), pathItem.getOptions()
);
operations.stream()
.filter(Objects::nonNull)
.forEach(operation -> {
validateOperation(operation, errors);
});
}
private void validateOperation(Operation operation, List<String> errors) {
if (operation.getOperationId() == null || operation.getOperationId().isEmpty()) {
errors.add("操作缺少operationId");
}
if (operation.getResponses() == null || operation.getResponses().isEmpty()) {
errors.add("操作 " + operation.getOperationId() + " 缺少响应定义");
}
// 验证必须有2xx响应
boolean hasSuccessResponse = operation.getResponses().keySet().stream()
.anyMatch(code -> code.startsWith("2"));
if (!hasSuccessResponse) {
errors.add("操作 " + operation.getOperationId() + " 缺少成功响应");
}
}
}
API First开发实践
契约优先开发
API契约定义:
# contract/user-api.yaml
openapi: 3.0.3
info:
title: User Management API
version: 1.0.0
description: 用户管理相关API
servers:
- url: http://localhost:8080/api/v1
description: 开发环境
- url: https://api.company.com/api/v1
description: 生产环境
components:
schemas:
User:
type: object
required:
- id
- username
- email
- status
properties:
id:
type: integer
format: int64
description: 用户ID
username:
type: string
description: 用户名
example: "john.doe"
email:
type: string
format: email
description: 邮箱地址
example: "john.doe@example.com"
firstName:
type: string
description: 名字
example: "John"
lastName:
type: string
description: 姓氏
example: "Doe"
status:
type: string
enum: [ACTIVE, INACTIVE, SUSPENDED]
description: 用户状态
createdDate:
type: string
format: date-time
description: 创建时间
UserCreateRequest:
type: object
required:
- username
- email
- password
properties:
username:
type: string
pattern: '^[a-zA-Z0-9._-]+$'
minLength: 3
maxLength: 50
email:
type: string
format: email
password:
type: string
minLength: 8
firstName:
type: string
maxLength: 50
lastName:
type: string
maxLength: 50
phone:
type: string
pattern: '^\\+?[1-9]\\d{1,14}$'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
paths:
/users:
get:
summary: 获取用户列表
description: 分页获取用户列表,支持搜索和过滤
security:
- bearerAuth: []
parameters:
- name: page
in: query
schema:
type: integer
minimum: 0
default: 0
description: 页码
- name: size
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
description: 每页数量
- name: search
in: query
schema:
type: string
description: 搜索关键词
- name: status
in: query
schema:
type: string
enum: [ACTIVE, INACTIVE, SUSPENDED]
description: 用户状态过滤
responses:
'200':
description: 成功获取用户列表
content:
application/json:
schema:
type: object
properties:
content:
type: array
items:
$ref: '#/components/schemas/User'
totalElements:
type: integer
totalPages:
type: integer
size:
type: integer
number:
type: integer
post:
summary: 创建新用户
description: 创建新的用户账号
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserCreateRequest'
responses:
'201':
description: 用户创建成功
content:
application/json:
schema:
$ref: '#/components/schemas/User'
契约测试
API契约测试实现:
@TestComponent
public class ContractTestProvider {
@PactTestFor(pacticipant = "user-service")
public void getUserList(PactDslWithProvider builder) {
PactDslJsonArray body = new PactDslJsonArray()
.object()
.numberValue("id", 1)
.stringType("username", "john.doe")
.stringType("email", "john.doe@example.com")
.stringType("status", "ACTIVE")
.closeObject();
builder
.given("用户列表存在")
.uponReceiving("获取用户列表请求")
.path("/api/v1/users")
.method("GET")
.headers(Map.of("Authorization", "Bearer token"))
.willRespondWith()
.status(200)
.matchHeader("Content-Type", "application/json")
.body(body);
}
@PactTestFor(pacticipant = "user-service")
public void createUser(PactDslWithProvider builder) {
PactDslJsonBody requestBody = new PactDslJsonBody()
.stringType("username", "test.user")
.stringType("email", "test@example.com")
.stringType("password", "password123");
PactDslJsonBody responseBody = new PactDslJsonBody()
.numberType("id", 1)
.stringType("username", "test.user")
.stringType("email", "test@example.com")
.stringType("status", "ACTIVE");
builder
.given("系统可以创建新用户")
.uponReceiving("创建用户请求")
.path("/api/v1/users")
.method("POST")
.headers(Map.of(
"Content-Type", "application/json",
"Authorization", "Bearer token"
))
.body(requestBody)
.willRespondWith()
.status(201)
.matchHeader("Content-Type", "application/json")
.body(responseBody);
}
}
@RunWith(PactRunner.class)
@Provider("user-service")
@PactFolder("pacts")
public class UserClientPactTest {
@Rule
public final PactProviderRuleMk2 providerRule = new PactProviderRuleMk2("user-service", null, 8080, this);
@InjectMocks
private UserApiClient userApiClient;
@Before
public void setUp() {
RestTemplate restTemplate = new RestTemplate();
userApiClient = new UserApiClientImpl("http://localhost:8080", restTemplate);
}
@Test
@PactVerification(value = "user-service", fragment = "getUserList")
public void testGetUserList() {
List<User> users = userApiClient.getUsers();
assertThat(users).isNotEmpty();
assertThat(users.get(0).getUsername()).isEqualTo("john.doe");
assertThat(users.get(0).getEmail()).isEqualTo("john.doe@example.com");
}
@Test
@PactVerification(value = "user-service", fragment = "createUser")
public void testCreateUser() {
UserCreateRequest request = UserCreateRequest.builder()
.username("test.user")
.email("test@example.com")
.password("password123")
.build();
User user = userApiClient.createUser(request);
assertThat(user.getUsername()).isEqualTo("test.user");
assertThat(user.getEmail()).isEqualTo("test@example.com");
assertThat(user.getStatus()).isEqualTo(UserStatus.ACTIVE);
}
}
MockMvc集成测试
Controller测试
REST控制器测试:
@WebMvcTest(UserController.class)
@Import({SecurityConfig.class, TestConfig.class})
@TestPropertySource(properties = {
"spring.jpa.hibernate.ddl-auto=none",
"logging.level.org.springframework.web=debug"
})
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@MockBean
private UserMapper userMapper;
@Autowired
private ObjectMapper objectMapper;
@Test
@WithMockUser(roles = "ADMIN")
@DisplayName("获取用户列表 - 成功")
void testGetUsers_Success() throws Exception {
// 准备测试数据
User user1 = User.builder()
.id(1L)
.username("john.doe")
.email("john@example.com")
.firstName("John")
.lastName("Doe")
.status(UserStatus.ACTIVE)
.role(UserRole.USER)
.createdDate(LocalDateTime.now())
.build();
PageResult<User> mockPageResult = new PageResult<>(
Lists.newArrayList(user1),
0, 20, 1, 1L, true, false
);
UserResponseDto responseDto = UserResponseDto.builder()
.id(1L)
.username("john.doe")
.email("john@example.com")
.firstName("John")
.lastName("Doe")
.status(UserStatus.ACTIVE)
.role(UserRole.USER)
.build();
PageResult<UserResponseDto> expectedResponse = new PageResult<>(
Lists.newArrayList(responseDto),
0, 20, 1, 1L, true, false
);
// Mock服务层
when(userService.searchUsers(anyString(), any(), any(Pageable.class)))
.thenReturn(mockPageResult);
when(userMapper.toResponsePages(any()))
.thenReturn(expectedResponse);
// 执行测试
mockMvc.perform(get("/api/v1/users")
.param("page", "0")
.param("size", "20")
.param("search", "john")
.param("status", "ACTIVE")
.param("sortBy", "createdDate")
.param("sortDir", "desc")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.content.length()").value(1))
.andExpect(jsonPath("$.content[0].id").value(1))
.andExpect(jsonPath("$.content[0].username").value("john.doe"))
.andExpect(jsonPath("$.content[0].email").value("john@example.com"))
.andExpect(jsonPath("$.totalElements").value(1))
.andExpect(jsonPath("$.totalPages").value(1))
.andDo(document("get-users",
requestParameters(
parameterWithName("page").description("页码"),
parameterWithName("size").description("每页数量"),
parameterWithName("search").description("搜索关键词"),
parameterWithName("status").description("用户状态"),
parameterWithName("sortBy").description("排序字段"),
parameterWithName("sortDir").description("排序方向")
),
responseFields(
fieldWithPath("content").description("用户列表"),
fieldWithPath("content[].id").description("用户ID"),
fieldWithPath("content[].username").description("用户名"),
fieldWithPath("content[].email").description("邮箱"),
fieldWithPath("totalElements").description("总记录数"),
fieldWithPath("totalPages").description("总页数"),
fieldWithPath("number").description("当前页"),
fieldWithPath("size").description("页面大小")
)
));
// 验证服务调用
verify(userService).searchUsers("john", UserStatus.ACTIVE, any(Pageable.class));
verify(userMapper).toResponsePages(mockPageResult);
}
@Test
@WithMockUser(roles = "ADMIN")
@DisplayName("创建用户 - 成功")
void testCreateUser_Success() throws Exception {
// 准备测试数据
UserCreateRequestDto requestDto = UserCreateRequestDto.builder()
.username("test.user")
.email("test@example.com")
.password("password123")
.firstName("Test")
.lastName("User")
.role(UserRole.USER)
.status(UserStatus.ACTIVE)
.build();
User newUser = User.builder()
.id(1L)
.username("test.user")
.email("test@example.com")
.firstName("Test")
.lastName("User")
.role(UserRole.USER)
.status(UserStatus.ACTIVE)
.build();
UserResponseDto responseDto = UserResponseDto.builder()
.id(1L)
.username("test.user")
.email("test@example.com")
.firstName("Test")
.lastName("User")
.status(UserStatus.ACTIVE)
.role(UserRole.USER)
.build();
// Mock服务层
when(userMapper.toEntity(any(UserCreateRequestDto.class)))
.thenReturn(newUser);
when(userService.createUser(any(User.class)))
.thenReturn(newUser);
when(userMapper.toResponseDto(any(User.class)))
.thenReturn(responseDto);
// 执行测试
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestDto)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.username").value("test.user"))
.andExpect(jsonPath("$.email").value("test@example.com"))
.andDo(document("create-user",
requestFields(
fieldWithPath("username").description("用户名"),
fieldWithPath("email").description("邮箱"),
fieldWithPath("password").description("密码"),
fieldWithPath("firstName").description("名字"),
fieldWithPath("lastName").description("姓氏"),
fieldWithPath("role").description("用户角色"),
fieldWithPath("status").description("用户状态")
),
responseFields(
fieldWithPath("id").description("用户ID"),
fieldWithPath("username").description("用户名"),
fieldWithPath("email").description("邮箱"),
fieldWithPath("firstName").description("名字"),
fieldWithPath("lastName").description("姓氏"),
fieldWithPath("status").description("用户状态"),
fieldWithPath("role").description("用户角色")
)
));
// 验证服务调用
verify(userService).createUser(any(User.class));
verify(userMapper).toEntity(requestDto);
verify(userMapper).toResponseDto(newUser);
}
@Test
@WithMockUser(roles = "USER")
@DisplayName("创建用户 - 权限不足")
void testCreateUser_Unauthorized() throws Exception {
UserCreateRequestDto requestDto = UserCreateRequestDto.builder()
.username("test.user")
.email("test@example.com")
.password("password123")
.build();
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestDto)))
.andExpect(status().isForbidden());
}
@Test
@WithAnonymousUser
@DisplayName("访问受保护端点 - 未认证")
void testAccessProtectedEndpoint_Unauthenticated() throws Exception {
mockMvc.perform(get("/api/v1/users"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "ADMIN")
@DisplayName("获取用户详情 - 用户不存在")
void testGetUserById_NotFound() throws Exception {
when(userService.findById(999L))
.thenThrow(new UserNotFoundException("用户不存在"));
mockMvc.perform(get("/api/v1/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$message").value("用户不存在"));
}
}
API性能测试
JMeter测试
JMeter测试计划配置:
<jmeterTestPlan version="1.2">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="User API Performance Test" enabled="true">
<elementProp name="TestPlan.arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments">
<elementProp name="baseUrl" elementType="Argument">
<stringProp name="Argument.name">baseUrl</stringProp>
<stringProp name="Argument.value">http://localhost:8080/api/v1</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</TestPlan>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Load Test - Get Users" enabled="true">
<stringProp name="ThreadGroup.num_threads">100</stringProp>
<stringProp name="ThreadGroup.ramp_time">30</stringProp>
<stringProp name="ThreadGroup.duration">300</stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Users List" enabled="true">
<stringProp name="HTTPSampler.domain">${baseUrl}</stringProp>
<stringProp name="HTTPSampler.port">8080</stringProp>
<stringProp name="HTTPSampler.protocol">http</stringProp>
<stringProp name="HTTPSampler.path">/users</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<stringProp name="HTTPSampler.DO_MULTIPART_POST">false</stringProp>
</HTTPSamplerProxy>
</hashTree>
</ThreadGroup>
</hashTree>
</jmeterTestPlan>
Gatling性能测试
Gatling测试场景:
import io.gatling.core.Predef._
import io.gatling.http.Predef._
class UserApiLoadTest extends Simulation {
val httpConf = http
.baseURL("http://localhost:8080/api/v1")
.acceptHeader("application/json")
.contentTypeHeader("application/json")
.authorizationHeader("Bearer ${token}")
val scn = scenario("User API Load Test")
.exec(
http("Get Users List")
.get("/users")
.queryParam("page", "0")
.queryParam("size", "20")
.check(status.is(200))
.check(jsonPath("$.content").exists)
.check(jsonPath("$.totalElements").isNumber)
.check(jsonPath("$.totalPages").isNumber)
)
.pause(1, 3)
.exec(
http("Get User by ID")
.get("/users/1")
.check(status.is(200))
.check(jsonPath("$.id").is(1))
.check(jsonPath("$.username").exists)
.check(jsonPath("$.email").exists)
)
.pause(2, 5)
.exec(
http("Create User")
.post("/users")
.body(StringBody(
"""
{
"username": "loadtest_${random}",
"email": "loadtest_${random}@example.com",
"password": "LoadTest123!",
"firstName": "LoadTest",
"lastName": "User",
"role": "USER"
}
""".stripMargin
))
.check(status.is(201))
.check(jsonPath("$.id").saveAs("newUserId"))
.check(jsonPath("$.username").saveAs("newUsername"))
)
.pause(1, 2)
.exec(
http("Update User")
.put("/users/${newUserId}")
.body(StringBody(
"""
{
"firstName": "UpdatedLoadTest",
"lastName": "UpdatedUser",
"phone": "+86 138 0013 8000"
}
""".stripMargin
))
.check(status.is(200))
.check(jsonPath("$.firstName").is("UpdatedLoadTest"))
)
setUp(
scn.inject(
nothingFor(5.seconds),
rampUsers(50) during(30.seconds),
constantUsersPerSec(20) during(2.minutes),
nothingFor(10.seconds),
rampUsers(100) during(30.seconds),
constantUsersPerSec(50) during(5.minutes)
)
).protocols(httpConf)
.assertions(
global.responseTime.percentile3.lessThan(2000),
global.successfulRequests.percent.greaterThan(95),
forAll.failedRequests.percent.lessThan(5)
)
}
总结
Spring MVC API文档与自动化测试教程提供了完整的API开发质量保证方案:
核心技术要点
- API文档生成 - Swagger/OpenAPI自动文档工具
- 契约优先开发 - API First开发方法论
- 自动测试覆盖 - MockMvc单元测试和集成测试
- 性能测试 - JMeter和Gatling负载测试
- 客户端生成 - 自动生成多语言SDK
- 文档管理 - 版本控制和文档发布
企业级应用价值
- 开发效率双倍提升:API优先开发模式大大提高开发效率
- 团队协作无缝对接:标准化的API文档确保前后端协作顺畅
- 测试覆盖率高达90%+:自动化测试确保API质量稳定可靠
- 性能指标可视化监控:实时掌握API性能和用户体验指标
最佳实践建议
- 契约优先:先定义API契约,再进行实现
- 自动化覆盖:代码覆盖率要保持在90%以上
- 文档实时更新:API文档要与代码实现同步更新
- 性能基准设定:建立明确的性能基准和告警机制
- 版本管理规范:API变更要遵循语义化版本规范
掌握API文档与测试技术,将使您的Spring MVC应用在企业级开发中更具竞争力和可靠性!