springboot-verification

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Spring Boot Verification Loop

Spring Boot 验证流程

Run before PRs, after major changes, and pre-deploy.
在提交PR、重大变更后以及部署前运行。

When to Activate

激活时机

  • Before opening a pull request for a Spring Boot service
  • After major refactoring or dependency upgrades
  • Pre-deployment verification for staging or production
  • Running full build → lint → test → security scan pipeline
  • Validating test coverage meets thresholds
  • 为Spring Boot服务提交拉取请求(PR)前
  • 重大重构或依赖升级后
  • 预发布环境或生产环境部署前的验证
  • 运行完整的构建 → 代码检查 → 测试 → 安全扫描流水线
  • 验证测试覆盖率是否达到阈值

Phase 1: Build

阶段1:构建

bash
mvn -T 4 clean verify -DskipTests
bash
mvn -T 4 clean verify -DskipTests

or

or

./gradlew clean assemble -x test

If build fails, stop and fix.
./gradlew clean assemble -x test

如果构建失败,停止并修复问题。

Phase 2: Static Analysis

阶段2:静态分析

Maven (common plugins):
bash
mvn -T 4 spotbugs:check pmd:check checkstyle:check
Gradle (if configured):
bash
./gradlew checkstyleMain pmdMain spotbugsMain
Maven(常用插件):
bash
mvn -T 4 spotbugs:check pmd:check checkstyle:check
Gradle(若已配置):
bash
./gradlew checkstyleMain pmdMain spotbugsMain

Phase 3: Tests + Coverage

阶段3:测试 + 覆盖率

bash
mvn -T 4 test
mvn jacoco:report   # verify 80%+ coverage
bash
mvn -T 4 test
mvn jacoco:report   # 验证覆盖率≥80%

or

or

./gradlew test jacocoTestReport

Report:
- Total tests, passed/failed
- Coverage % (lines/branches)
./gradlew test jacocoTestReport

报告内容:
- 测试总数、通过/失败数量
- 覆盖率百分比(行/分支)

Unit Tests

单元测试

Test service logic in isolation with mocked dependencies:
java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

  @Mock private UserRepository userRepository;
  @InjectMocks private UserService userService;

  @Test
  void createUser_validInput_returnsUser() {
    var dto = new CreateUserDto("Alice", "alice@example.com");
    var expected = new User(1L, "Alice", "alice@example.com");
    when(userRepository.save(any(User.class))).thenReturn(expected);

    var result = userService.create(dto);

    assertThat(result.name()).isEqualTo("Alice");
    verify(userRepository).save(any(User.class));
  }

  @Test
  void createUser_duplicateEmail_throwsException() {
    var dto = new CreateUserDto("Alice", "existing@example.com");
    when(userRepository.existsByEmail(dto.email())).thenReturn(true);

    assertThatThrownBy(() -> userService.create(dto))
        .isInstanceOf(DuplicateEmailException.class);
  }
}
通过模拟依赖项,独立测试服务逻辑:
java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

  @Mock private UserRepository userRepository;
  @InjectMocks private UserService userService;

  @Test
  void createUser_validInput_returnsUser() {
    var dto = new CreateUserDto("Alice", "alice@example.com");
    var expected = new User(1L, "Alice", "alice@example.com");
    when(userRepository.save(any(User.class))).thenReturn(expected);

    var result = userService.create(dto);

    assertThat(result.name()).isEqualTo("Alice");
    verify(userRepository).save(any(User.class));
  }

  @Test
  void createUser_duplicateEmail_throwsException() {
    var dto = new CreateUserDto("Alice", "existing@example.com");
    when(userRepository.existsByEmail(dto.email())).thenReturn(true);

    assertThatThrownBy(() -> userService.create(dto))
        .isInstanceOf(DuplicateEmailException.class);
  }
}

Integration Tests with Testcontainers

基于Testcontainers的集成测试

Test against a real database instead of H2:
java
@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {

  @Container
  static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
      .withDatabaseName("testdb");

  @DynamicPropertySource
  static void configureProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgres::getJdbcUrl);
    registry.add("spring.datasource.username", postgres::getUsername);
    registry.add("spring.datasource.password", postgres::getPassword);
  }

  @Autowired private UserRepository userRepository;

  @Test
  void findByEmail_existingUser_returnsUser() {
    userRepository.save(new User("Alice", "alice@example.com"));

    var found = userRepository.findByEmail("alice@example.com");

    assertThat(found).isPresent();
    assertThat(found.get().getName()).isEqualTo("Alice");
  }
}
针对真实数据库而非H2进行测试:
java
@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {

  @Container
  static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
      .withDatabaseName("testdb");

  @DynamicPropertySource
  static void configureProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgres::getJdbcUrl);
    registry.add("spring.datasource.username", postgres::getUsername);
    registry.add("spring.datasource.password", postgres::getPassword);
  }

  @Autowired private UserRepository userRepository;

  @Test
  void findByEmail_existingUser_returnsUser() {
    userRepository.save(new User("Alice", "alice@example.com"));

    var found = userRepository.findByEmail("alice@example.com");

    assertThat(found).isPresent();
    assertThat(found.get().getName()).isEqualTo("Alice");
  }
}

API Tests with MockMvc

基于MockMvc的API测试

Test controller layer with full Spring context:
java
@WebMvcTest(UserController.class)
class UserControllerTest {

  @Autowired private MockMvc mockMvc;
  @MockBean private UserService userService;

  @Test
  void createUser_validInput_returns201() throws Exception {
    var user = new UserDto(1L, "Alice", "alice@example.com");
    when(userService.create(any())).thenReturn(user);

    mockMvc.perform(post("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content("""
                {"name": "Alice", "email": "alice@example.com"}
                """))
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$.name").value("Alice"));
  }

  @Test
  void createUser_invalidEmail_returns400() throws Exception {
    mockMvc.perform(post("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content("""
                {"name": "Alice", "email": "not-an-email"}
                """))
        .andExpect(status().isBadRequest());
  }
}
在完整Spring上下文环境下测试控制器层:
java
@WebMvcTest(UserController.class)
class UserControllerTest {

  @Autowired private MockMvc mockMvc;
  @MockBean private UserService userService;

  @Test
  void createUser_validInput_returns201() throws Exception {
    var user = new UserDto(1L, "Alice", "alice@example.com");
    when(userService.create(any())).thenReturn(user);

    mockMvc.perform(post("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content("""
                {"name": "Alice", "email": "alice@example.com"}
                """))
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$.name").value("Alice"));
  }

  @Test
  void createUser_invalidEmail_returns400() throws Exception {
    mockMvc.perform(post("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content("""
                {"name": "Alice", "email": "not-an-email"}
                """))
        .andExpect(status().isBadRequest());
  }
}

Phase 4: Security Scan

阶段4:安全扫描

bash
undefined
bash
undefined

Dependency CVEs

依赖项CVE检测

mvn org.owasp:dependency-check-maven:check
mvn org.owasp:dependency-check-maven:check

or

or

./gradlew dependencyCheckAnalyze
./gradlew dependencyCheckAnalyze

Secrets in source

源代码中的密钥检测

grep -rn "password\s*=\s*"" src/ --include=".java" --include=".yml" --include=".properties" grep -rn "sk-|api_key|secret" src/ --include=".java" --include="*.yml"
grep -rn "password\s*=\s*"" src/ --include=".java" --include=".yml" --include=".properties" grep -rn "sk-|api_key|secret" src/ --include=".java" --include="*.yml"

Secrets (git history)

Git历史中的密钥检测

git secrets --scan # if configured
undefined
git secrets --scan # 若已配置
undefined

Common Security Findings

常见安全问题检查

undefined
undefined

Check for System.out.println (use logger instead)

检查是否存在System.out.println(应使用日志框架替代)

grep -rn "System.out.print" src/main/ --include="*.java"
grep -rn "System.out.print" src/main/ --include="*.java"

Check for raw exception messages in responses

检查响应中是否包含原始异常信息

grep -rn "e.getMessage()" src/main/ --include="*.java"
grep -rn "e.getMessage()" src/main/ --include="*.java"

Check for wildcard CORS

检查是否存在通配符CORS配置

grep -rn "allowedOrigins.*" src/main/ --include=".java"
undefined
grep -rn "allowedOrigins.*" src/main/ --include=".java"
undefined

Phase 5: Lint/Format (optional gate)

阶段5:代码检查/格式化(可选关卡)

bash
mvn spotless:apply   # if using Spotless plugin
./gradlew spotlessApply
bash
mvn spotless:apply   # 若使用Spotless插件
./gradlew spotlessApply

Phase 6: Diff Review

阶段6:差异审查

bash
git diff --stat
git diff
Checklist:
  • No debugging logs left (
    System.out
    ,
    log.debug
    without guards)
  • Meaningful errors and HTTP statuses
  • Transactions and validation present where needed
  • Config changes documented
bash
git diff --stat
git diff
检查清单:
  • 未遗留调试日志(如无防护的
    System.out
    log.debug
  • 错误信息和HTTP状态码具备明确含义
  • 必要位置已添加事务和验证逻辑
  • 配置变更已记录

Output Template

输出模板

VERIFICATION REPORT
===================
Build:     [PASS/FAIL]
Static:    [PASS/FAIL] (spotbugs/pmd/checkstyle)
Tests:     [PASS/FAIL] (X/Y passed, Z% coverage)
Security:  [PASS/FAIL] (CVE findings: N)
Diff:      [X files changed]

Overall:   [READY / NOT READY]

Issues to Fix:
1. ...
2. ...
验证报告
=========
构建:     [通过/失败]
静态分析: [通过/失败](spotbugs/pmd/checkstyle)
测试:     [通过/失败](X/Y通过,Z%覆盖率)
安全扫描: [通过/失败](CVE漏洞数量:N)
代码差异: [X个文件变更]

整体状态: [就绪 / 未就绪]

待修复问题:
1. ...
2. ...

Continuous Mode

持续运行模式

  • Re-run phases on significant changes or every 30–60 minutes in long sessions
  • Keep a short loop:
    mvn -T 4 test
    + spotbugs for quick feedback
Remember: Fast feedback beats late surprises. Keep the gate strict—treat warnings as defects in production systems.
  • 在重大变更后或长会话中每30–60分钟重新运行流程
  • 保留轻量流程:
    mvn -T 4 test
    + spotbugs以快速获取反馈
谨记:快速反馈优于事后补救。严格把关——在生产环境中,将警告视为缺陷处理。