本文目录导读:

- 案例背景与用户故事 (User Story)
- 技术栈选型 (Tech Stack)
- 敏捷开发流程实践
- 代码实战 (TDD导向)
- 持续集成与代码质量
- 每日站会示例 (Daily Standup)
- Sprint评审演示
- 敏捷回顾要点
这是一个关于 Java敏捷开发 的完整案例介绍,涵盖 Spring Boot + JPA + TDD 的典型实践。
我们将模拟一个最经典的业务场景:在线书店 的 用户注册与图书搜索 功能开发。
案例背景与用户故事 (User Story)
以 Scrum 框架为例,产品经理 (PO) 定义了第一个Sprint的任务:
US-001:用户注册
作为访客,我希望通过邮箱和密码注册,以便成为会员并购买图书。 验收标准:
- 邮箱不能重复。
- 密码长度必须 >= 6位。
- 注册成功后返回用户ID。
US-002:图书搜索
作为会员,我希望按书名模糊搜索,以便快速找到想买的书。 验收标准:
- 支持关键词模糊搜索。
- 无结果时返回空列表,非null。
技术栈选型 (Tech Stack)
- 框架:Spring Boot 3.x
- ORM:Spring Data JPA (Hibernate)
- 数据库:H2 (开发/测试用) 或 MySQL
- 测试:JUnit 5 + Mockito + AssertJ
- 构建:Maven / Gradle
- 开发模式:TDD (测试驱动开发) + 结对编程
敏捷开发流程实践
我们按 3天Sprint 演示:
- Day 1 (上午):计划会议 + 任务拆分
- Day 1 (下午):TDD开发用户注册
- Day 2:TDD开发图书搜索
- Day 3:集成测试 + 代码审查 + 演示
代码实战 (TDD导向)
1 第1次迭代:编写测试(红)
test层:UserServiceTest.java
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@MockBean
private UserRepository userRepository;
@Test
void shouldRegisterUserSuccessfully() {
// Given
RegisterRequest request = new RegisterRequest("test@example.com", "password123");
when(userRepository.existsByEmail("test@example.com")).thenReturn(false);
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId(1L);
return user;
});
// When
UserResponse response = userService.register(request);
// Then
assertThat(response).isNotNull();
assertThat(response.getUserId()).isEqualTo(1L);
assertThat(response.getEmail()).isEqualTo("test@example.com");
}
@Test
void shouldThrowExceptionWhenEmailExists() {
// Given
RegisterRequest request = new RegisterRequest("existing@example.com", "password123");
when(userRepository.existsByEmail("existing@example.com")).thenReturn(true);
// When & Then
assertThrows(DuplicateEmailException.class, () -> {
userService.register(request);
});
}
@Test
void shouldThrowExceptionWhenPasswordTooShort() {
// Given
RegisterRequest request = new RegisterRequest("test@example.com", "123");
// When & Then
assertThrows(InvalidPasswordException.class, () -> {
userService.register(request);
});
}
}
2 第1次迭代:实现代码(绿)
实体层:User.java
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@CreationTimestamp
private LocalDateTime createdAt;
}
服务层:UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserResponse register(RegisterRequest request) {
// 验证密码长度
if (request.getPassword().length() < 6) {
throw new InvalidPasswordException("Password must be at least 6 characters");
}
// 检查邮箱唯一性
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateEmailException("Email already registered");
}
// 创建并保存用户
User user = new User();
user.setEmail(request.getEmail());
user.setPassword(passwordEncoder.encode(request.getPassword()));
User savedUser = userRepository.save(user);
// 返回响应
return new UserResponse(savedUser.getId(), savedUser.getEmail());
}
}
3 第2次迭代:图书搜索功能
测试:BookServiceTest.java
@Test
void shouldReturnBooksWhenKeywordMatchesTitle() {
// Given
String keyword = "Java";
Book book1 = new Book(1L, "Java 入门", "作者A");
Book book2 = new Book(2L, "Java 高级", "作者B");
when(bookRepository.findByTitleContaining(keyword)).thenReturn(List.of(book1, book2));
// When
List<BookResponse> result = bookService.searchByTitle(keyword);
// Then
assertThat(result).hasSize(2);
assertThat(result.get(0).getTitle()).contains("Java");
}
@Test
void shouldReturnEmptyListWhenKeywordNotFound() {
// Given
when(bookRepository.findByTitleContaining("PHP")).thenReturn(Collections.emptyList());
// When
List<BookResponse> result = bookService.searchByTitle("PHP");
// Then
assertThat(result).isEmpty(); // 不是 null
}
实现:BookService.java
public List<BookResponse> searchByTitle(String keyword) {
List<Book> books = bookRepository.findByTitleContaining(keyword);
return books.stream()
.map(book -> new BookResponse(book.getId(), book.getTitle(), book.getAuthor()))
.collect(Collectors.toList());
}
仓库层:BookRepository.java
public interface BookRepository extends JpaRepository<Book, Long> {
List<Book> findByTitleContaining(String keyword);
}
持续集成与代码质量
在敏捷开发中,不仅写代码,还要保证质量:
1 CI/CD 配置
# .github/workflows/ci.yml (GitHub Actions)
name: Java CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
- name: Build with Maven
run: mvn clean verify # 包括测试
- name: Generate Coverage Report
run: mvn jacoco:report
- name: Upload Coverage
uses: codecov/codecov-action@v3
2 代码审查 Checklist
| 检查项 | 通过? |
|---|---|
| 测试覆盖率高(>80%) | |
| 无硬编码配置 | |
| 遵循单一职责原则 | |
| 方法 < 30行 | |
| 无 System.out |
每日站会示例 (Daily Standup)
场景:Sprint 第2天
开发者A:
- ✅ 昨天完成了 US-001 的注册功能(已完成测试)
- 🔄 今天开始 US-002 的搜索功能
- ❌ 遇到问题:JPA模糊查询性能,计划使用全文索引
开发者B:
- ✅ 昨天搭建了 H2 测试数据库配置
- 🔄 今天写集成测试
- ❌ 没有障碍
Sprint评审演示
最终展示时,可使用 Swagger 或 Postman 演示:
POST /api/users/register
Content-Type: application/json
{
"email": "alice@example.com",
"password": "secure123"
}
成功响应:
{
"userId": 1001,
"email": "alice@example.com"
}
GET /api/books/search?keyword=Java
响应:
[
{"id": 1, "title": "Java 入门", "author": "张三"},
{"id": 2, "title": "Java 高级编程", "author": "李四"}
]
敏捷回顾要点
在项目交付后,团队应进行 Sprint Retrospective:
| 做得好的 | 需要改进的 |
|---|---|
| TDD 显著减少bug | 前期估算不够准确 |
| 每日站会高效 | 需要更多自动化测试 |
| 代码审查及时 | 文档更新滞后 |
这个案例展示了 Java敏捷开发 的完整闭环:
- 需求 → 用户故事 + 验收标准
- 开发 → TDD (红-绿-重构)
- 协作 → 每日站会 + 结对编程
- 质量 → CI/CD + 代码审查
- 交付 → Sprint评审 + 回顾
对于团队来说,关键不在于工具多先进,而在于 反馈循环是否快、测试是否充分、团队沟通是否顺畅。
如果你有具体的业务场景想用敏捷方式实现(比如支付、购物车、订单系统),我可以为你定制一个更详细的案例。