Comprehensive Claude Code guidance system with: - 5 agents: tdd-guardian, code-reviewer, security-scanner, refactor-scan, dependency-audit - 18 skills covering languages (Python, TypeScript, Rust, Go, Java, C#), infrastructure (AWS, Azure, GCP, Terraform, Ansible, Docker/K8s, Database, CI/CD), testing (TDD, UI, Browser), and patterns (Monorepo, API Design, Observability) - 3 hooks: secret detection, auto-formatting, TDD git pre-commit - Strict TDD enforcement with 80%+ coverage requirements - Multi-model strategy: Opus for planning, Sonnet for execution (opusplan) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
676 lines
19 KiB
Markdown
676 lines
19 KiB
Markdown
---
|
|
name: java-development
|
|
description: Java development patterns with Spring Boot, JUnit 5, and modern Java practices. Use when writing Java code.
|
|
---
|
|
|
|
# Java Development Skill
|
|
|
|
## Project Structure (Spring Boot)
|
|
|
|
```
|
|
project/
|
|
├── src/
|
|
│ ├── main/
|
|
│ │ ├── java/
|
|
│ │ │ └── com/mycompany/myapp/
|
|
│ │ │ ├── MyApplication.java
|
|
│ │ │ ├── domain/
|
|
│ │ │ │ ├── User.java
|
|
│ │ │ │ └── Order.java
|
|
│ │ │ ├── repository/
|
|
│ │ │ │ └── UserRepository.java
|
|
│ │ │ ├── service/
|
|
│ │ │ │ └── UserService.java
|
|
│ │ │ ├── controller/
|
|
│ │ │ │ └── UserController.java
|
|
│ │ │ └── config/
|
|
│ │ │ └── SecurityConfig.java
|
|
│ │ └── resources/
|
|
│ │ ├── application.yml
|
|
│ │ └── application-test.yml
|
|
│ └── test/
|
|
│ └── java/
|
|
│ └── com/mycompany/myapp/
|
|
│ ├── service/
|
|
│ │ └── UserServiceTest.java
|
|
│ └── controller/
|
|
│ └── UserControllerTest.java
|
|
├── pom.xml
|
|
└── README.md
|
|
```
|
|
|
|
## Testing with JUnit 5
|
|
|
|
### Unit Tests with Mockito
|
|
```java
|
|
package com.mycompany.myapp.service;
|
|
|
|
import com.mycompany.myapp.domain.User;
|
|
import com.mycompany.myapp.repository.UserRepository;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.Nested;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
import org.mockito.InjectMocks;
|
|
import org.mockito.Mock;
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
|
import java.util.Optional;
|
|
|
|
import static org.assertj.core.api.Assertions.*;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.Mockito.*;
|
|
|
|
@ExtendWith(MockitoExtension.class)
|
|
class UserServiceTest {
|
|
|
|
@Mock
|
|
private UserRepository userRepository;
|
|
|
|
@InjectMocks
|
|
private UserService userService;
|
|
|
|
@Nested
|
|
@DisplayName("createUser")
|
|
class CreateUser {
|
|
|
|
@Test
|
|
@DisplayName("should create user with valid data")
|
|
void shouldCreateUserWithValidData() {
|
|
// Given
|
|
var request = UserFixtures.createUserRequest();
|
|
var savedUser = UserFixtures.user();
|
|
when(userRepository.save(any(User.class))).thenReturn(savedUser);
|
|
|
|
// When
|
|
var result = userService.createUser(request);
|
|
|
|
// Then
|
|
assertThat(result.getId()).isNotNull();
|
|
assertThat(result.getEmail()).isEqualTo(request.getEmail());
|
|
verify(userRepository).save(any(User.class));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("should throw exception for duplicate email")
|
|
void shouldThrowExceptionForDuplicateEmail() {
|
|
// Given
|
|
var request = UserFixtures.createUserRequest();
|
|
when(userRepository.existsByEmail(request.getEmail())).thenReturn(true);
|
|
|
|
// When/Then
|
|
assertThatThrownBy(() -> userService.createUser(request))
|
|
.isInstanceOf(DuplicateEmailException.class)
|
|
.hasMessageContaining(request.getEmail());
|
|
}
|
|
}
|
|
|
|
@Nested
|
|
@DisplayName("getUserById")
|
|
class GetUserById {
|
|
|
|
@Test
|
|
@DisplayName("should return user when found")
|
|
void shouldReturnUserWhenFound() {
|
|
// Given
|
|
var user = UserFixtures.user();
|
|
when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
|
|
|
|
// When
|
|
var result = userService.getUserById(user.getId());
|
|
|
|
// Then
|
|
assertThat(result).isPresent();
|
|
assertThat(result.get().getId()).isEqualTo(user.getId());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("should return empty when user not found")
|
|
void shouldReturnEmptyWhenNotFound() {
|
|
// Given
|
|
when(userRepository.findById(any())).thenReturn(Optional.empty());
|
|
|
|
// When
|
|
var result = userService.getUserById("unknown-id");
|
|
|
|
// Then
|
|
assertThat(result).isEmpty();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Test Fixtures
|
|
```java
|
|
package com.mycompany.myapp.fixtures;
|
|
|
|
import com.mycompany.myapp.domain.User;
|
|
import com.mycompany.myapp.dto.CreateUserRequest;
|
|
|
|
import java.time.Instant;
|
|
import java.util.UUID;
|
|
|
|
public final class UserFixtures {
|
|
|
|
private UserFixtures() {}
|
|
|
|
public static User user() {
|
|
return user(builder -> {});
|
|
}
|
|
|
|
public static User user(UserCustomizer customizer) {
|
|
var builder = User.builder()
|
|
.id(UUID.randomUUID().toString())
|
|
.email("test@example.com")
|
|
.name("Test User")
|
|
.role(Role.USER)
|
|
.createdAt(Instant.now())
|
|
.updatedAt(Instant.now());
|
|
|
|
customizer.customize(builder);
|
|
return builder.build();
|
|
}
|
|
|
|
public static CreateUserRequest createUserRequest() {
|
|
return createUserRequest(builder -> {});
|
|
}
|
|
|
|
public static CreateUserRequest createUserRequest(CreateUserRequestCustomizer customizer) {
|
|
var builder = CreateUserRequest.builder()
|
|
.email("newuser@example.com")
|
|
.name("New User")
|
|
.password("SecurePass123!");
|
|
|
|
customizer.customize(builder);
|
|
return builder.build();
|
|
}
|
|
|
|
@FunctionalInterface
|
|
public interface UserCustomizer {
|
|
void customize(User.UserBuilder builder);
|
|
}
|
|
|
|
@FunctionalInterface
|
|
public interface CreateUserRequestCustomizer {
|
|
void customize(CreateUserRequest.CreateUserRequestBuilder builder);
|
|
}
|
|
}
|
|
|
|
// Usage in tests
|
|
var adminUser = UserFixtures.user(builder -> builder.role(Role.ADMIN));
|
|
var customRequest = UserFixtures.createUserRequest(builder ->
|
|
builder.email("custom@example.com"));
|
|
```
|
|
|
|
### Parameterized Tests
|
|
```java
|
|
package com.mycompany.myapp.validation;
|
|
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.params.ParameterizedTest;
|
|
import org.junit.jupiter.params.provider.CsvSource;
|
|
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
|
import org.junit.jupiter.params.provider.ValueSource;
|
|
|
|
import static org.assertj.core.api.Assertions.*;
|
|
|
|
class EmailValidatorTest {
|
|
|
|
private final EmailValidator validator = new EmailValidator();
|
|
|
|
@ParameterizedTest
|
|
@DisplayName("should accept valid emails")
|
|
@ValueSource(strings = {
|
|
"user@example.com",
|
|
"user.name@example.com",
|
|
"user+tag@example.co.uk"
|
|
})
|
|
void shouldAcceptValidEmails(String email) {
|
|
assertThat(validator.isValid(email)).isTrue();
|
|
}
|
|
|
|
@ParameterizedTest
|
|
@DisplayName("should reject invalid emails")
|
|
@ValueSource(strings = {
|
|
"invalid",
|
|
"missing@domain",
|
|
"@nodomain.com",
|
|
"spaces in@email.com"
|
|
})
|
|
void shouldRejectInvalidEmails(String email) {
|
|
assertThat(validator.isValid(email)).isFalse();
|
|
}
|
|
|
|
@ParameterizedTest
|
|
@DisplayName("should reject null and empty emails")
|
|
@NullAndEmptySource
|
|
void shouldRejectNullAndEmpty(String email) {
|
|
assertThat(validator.isValid(email)).isFalse();
|
|
}
|
|
|
|
@ParameterizedTest
|
|
@DisplayName("should validate password strength")
|
|
@CsvSource({
|
|
"password,false",
|
|
"Password1,false",
|
|
"Password1!,true",
|
|
"P@ssw0rd,true"
|
|
})
|
|
void shouldValidatePasswordStrength(String password, boolean expected) {
|
|
assertThat(validator.isStrongPassword(password)).isEqualTo(expected);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Integration Tests with Spring
|
|
```java
|
|
package com.mycompany.myapp.controller;
|
|
|
|
import com.mycompany.myapp.fixtures.UserFixtures;
|
|
import com.mycompany.myapp.repository.UserRepository;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
|
import org.springframework.boot.test.context.SpringBootTest;
|
|
import org.springframework.http.MediaType;
|
|
import org.springframework.test.context.ActiveProfiles;
|
|
import org.springframework.test.web.servlet.MockMvc;
|
|
|
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
|
|
|
@SpringBootTest
|
|
@AutoConfigureMockMvc
|
|
@ActiveProfiles("test")
|
|
class UserControllerIntegrationTest {
|
|
|
|
@Autowired
|
|
private MockMvc mockMvc;
|
|
|
|
@Autowired
|
|
private UserRepository userRepository;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
userRepository.deleteAll();
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("POST /api/users creates user and returns 201")
|
|
void createUserReturnsCreated() throws Exception {
|
|
var request = """
|
|
{
|
|
"email": "new@example.com",
|
|
"name": "New User",
|
|
"password": "SecurePass123!"
|
|
}
|
|
""";
|
|
|
|
mockMvc.perform(post("/api/users")
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content(request))
|
|
.andExpect(status().isCreated())
|
|
.andExpect(jsonPath("$.id").isNotEmpty())
|
|
.andExpect(jsonPath("$.email").value("new@example.com"))
|
|
.andExpect(jsonPath("$.name").value("New User"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("GET /api/users/{id} returns user when found")
|
|
void getUserReturnsUserWhenFound() throws Exception {
|
|
var user = userRepository.save(UserFixtures.user());
|
|
|
|
mockMvc.perform(get("/api/users/{id}", user.getId()))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.id").value(user.getId()))
|
|
.andExpect(jsonPath("$.email").value(user.getEmail()));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("GET /api/users/{id} returns 404 when not found")
|
|
void getUserReturns404WhenNotFound() throws Exception {
|
|
mockMvc.perform(get("/api/users/{id}", "unknown-id"))
|
|
.andExpect(status().isNotFound());
|
|
}
|
|
}
|
|
```
|
|
|
|
## Domain Models with Records
|
|
|
|
```java
|
|
package com.mycompany.myapp.domain;
|
|
|
|
import jakarta.validation.constraints.*;
|
|
import java.time.Instant;
|
|
|
|
// Immutable domain model using records (Java 17+)
|
|
public record User(
|
|
String id,
|
|
@NotBlank @Email String email,
|
|
@NotBlank @Size(max = 100) String name,
|
|
Role role,
|
|
Instant createdAt,
|
|
Instant updatedAt
|
|
) {
|
|
// Compact constructor for validation
|
|
public User {
|
|
if (email != null) {
|
|
email = email.toLowerCase().trim();
|
|
}
|
|
}
|
|
|
|
// Static factory methods
|
|
public static User create(String email, String name) {
|
|
var now = Instant.now();
|
|
return new User(
|
|
UUID.randomUUID().toString(),
|
|
email,
|
|
name,
|
|
Role.USER,
|
|
now,
|
|
now
|
|
);
|
|
}
|
|
|
|
// Wither methods for immutable updates
|
|
public User withName(String newName) {
|
|
return new User(id, email, newName, role, createdAt, Instant.now());
|
|
}
|
|
|
|
public User withRole(Role newRole) {
|
|
return new User(id, email, name, newRole, createdAt, Instant.now());
|
|
}
|
|
}
|
|
|
|
// For mutable entities (JPA)
|
|
@Entity
|
|
@Table(name = "users")
|
|
@Getter
|
|
@Builder
|
|
@NoArgsConstructor
|
|
@AllArgsConstructor
|
|
public class UserEntity {
|
|
|
|
@Id
|
|
@GeneratedValue(strategy = GenerationType.UUID)
|
|
private String id;
|
|
|
|
@Column(unique = true, nullable = false)
|
|
private String email;
|
|
|
|
@Column(nullable = false)
|
|
private String name;
|
|
|
|
@Enumerated(EnumType.STRING)
|
|
private Role role;
|
|
|
|
@Column(name = "created_at")
|
|
private Instant createdAt;
|
|
|
|
@Column(name = "updated_at")
|
|
private Instant updatedAt;
|
|
|
|
@PrePersist
|
|
void onCreate() {
|
|
createdAt = Instant.now();
|
|
updatedAt = createdAt;
|
|
}
|
|
|
|
@PreUpdate
|
|
void onUpdate() {
|
|
updatedAt = Instant.now();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Service Layer
|
|
|
|
```java
|
|
package com.mycompany.myapp.service;
|
|
|
|
import com.mycompany.myapp.domain.User;
|
|
import com.mycompany.myapp.dto.CreateUserRequest;
|
|
import com.mycompany.myapp.exception.DuplicateEmailException;
|
|
import com.mycompany.myapp.exception.UserNotFoundException;
|
|
import com.mycompany.myapp.repository.UserRepository;
|
|
import lombok.RequiredArgsConstructor;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import java.util.Optional;
|
|
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
@Transactional(readOnly = true)
|
|
public class UserService {
|
|
|
|
private final UserRepository userRepository;
|
|
private final PasswordEncoder passwordEncoder;
|
|
|
|
@Transactional
|
|
public User createUser(CreateUserRequest request) {
|
|
if (userRepository.existsByEmail(request.getEmail())) {
|
|
throw new DuplicateEmailException(request.getEmail());
|
|
}
|
|
|
|
var user = User.create(
|
|
request.getEmail(),
|
|
request.getName()
|
|
);
|
|
|
|
return userRepository.save(user);
|
|
}
|
|
|
|
public Optional<User> getUserById(String id) {
|
|
return userRepository.findById(id);
|
|
}
|
|
|
|
public User getUserByIdOrThrow(String id) {
|
|
return userRepository.findById(id)
|
|
.orElseThrow(() -> new UserNotFoundException(id));
|
|
}
|
|
|
|
@Transactional
|
|
public User updateUser(String id, UpdateUserRequest request) {
|
|
var user = getUserByIdOrThrow(id);
|
|
|
|
var updatedUser = user
|
|
.withName(request.getName())
|
|
.withRole(request.getRole());
|
|
|
|
return userRepository.save(updatedUser);
|
|
}
|
|
|
|
@Transactional
|
|
public void deleteUser(String id) {
|
|
if (!userRepository.existsById(id)) {
|
|
throw new UserNotFoundException(id);
|
|
}
|
|
userRepository.deleteById(id);
|
|
}
|
|
}
|
|
```
|
|
|
|
## REST Controllers
|
|
|
|
```java
|
|
package com.mycompany.myapp.controller;
|
|
|
|
import com.mycompany.myapp.dto.*;
|
|
import com.mycompany.myapp.service.UserService;
|
|
import jakarta.validation.Valid;
|
|
import lombok.RequiredArgsConstructor;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
import java.util.List;
|
|
|
|
@RestController
|
|
@RequestMapping("/api/users")
|
|
@RequiredArgsConstructor
|
|
public class UserController {
|
|
|
|
private final UserService userService;
|
|
private final UserMapper userMapper;
|
|
|
|
@GetMapping
|
|
public List<UserResponse> listUsers() {
|
|
return userService.getAllUsers().stream()
|
|
.map(userMapper::toResponse)
|
|
.toList();
|
|
}
|
|
|
|
@GetMapping("/{id}")
|
|
public ResponseEntity<UserResponse> getUser(@PathVariable String id) {
|
|
return userService.getUserById(id)
|
|
.map(userMapper::toResponse)
|
|
.map(ResponseEntity::ok)
|
|
.orElse(ResponseEntity.notFound().build());
|
|
}
|
|
|
|
@PostMapping
|
|
@ResponseStatus(HttpStatus.CREATED)
|
|
public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) {
|
|
var user = userService.createUser(request);
|
|
return userMapper.toResponse(user);
|
|
}
|
|
|
|
@PutMapping("/{id}")
|
|
public UserResponse updateUser(
|
|
@PathVariable String id,
|
|
@Valid @RequestBody UpdateUserRequest request) {
|
|
var user = userService.updateUser(id, request);
|
|
return userMapper.toResponse(user);
|
|
}
|
|
|
|
@DeleteMapping("/{id}")
|
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
|
public void deleteUser(@PathVariable String id) {
|
|
userService.deleteUser(id);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Exception Handling
|
|
|
|
```java
|
|
package com.mycompany.myapp.exception;
|
|
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.ProblemDetail;
|
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|
|
|
import java.net.URI;
|
|
|
|
@RestControllerAdvice
|
|
public class GlobalExceptionHandler {
|
|
|
|
@ExceptionHandler(UserNotFoundException.class)
|
|
public ProblemDetail handleUserNotFound(UserNotFoundException ex) {
|
|
var problem = ProblemDetail.forStatusAndDetail(
|
|
HttpStatus.NOT_FOUND,
|
|
ex.getMessage()
|
|
);
|
|
problem.setTitle("User Not Found");
|
|
problem.setType(URI.create("https://api.myapp.com/errors/user-not-found"));
|
|
return problem;
|
|
}
|
|
|
|
@ExceptionHandler(DuplicateEmailException.class)
|
|
public ProblemDetail handleDuplicateEmail(DuplicateEmailException ex) {
|
|
var problem = ProblemDetail.forStatusAndDetail(
|
|
HttpStatus.CONFLICT,
|
|
ex.getMessage()
|
|
);
|
|
problem.setTitle("Duplicate Email");
|
|
problem.setType(URI.create("https://api.myapp.com/errors/duplicate-email"));
|
|
return problem;
|
|
}
|
|
|
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
|
|
var problem = ProblemDetail.forStatusAndDetail(
|
|
HttpStatus.BAD_REQUEST,
|
|
"Validation failed"
|
|
);
|
|
problem.setTitle("Validation Error");
|
|
|
|
var errors = ex.getBindingResult().getFieldErrors().stream()
|
|
.map(e -> new ValidationError(e.getField(), e.getDefaultMessage()))
|
|
.toList();
|
|
|
|
problem.setProperty("errors", errors);
|
|
return problem;
|
|
}
|
|
|
|
record ValidationError(String field, String message) {}
|
|
}
|
|
```
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
# Maven
|
|
mvn clean install # Build and test
|
|
mvn test # Run tests only
|
|
mvn verify # Run integration tests
|
|
mvn spring-boot:run # Run application
|
|
mvn jacoco:report # Generate coverage report
|
|
|
|
# Gradle
|
|
./gradlew build # Build and test
|
|
./gradlew test # Run tests only
|
|
./gradlew integrationTest # Run integration tests
|
|
./gradlew bootRun # Run application
|
|
./gradlew jacocoTestReport # Generate coverage report
|
|
|
|
# Common flags
|
|
-DskipTests # Skip tests
|
|
-Dspring.profiles.active=test # Set profile
|
|
```
|
|
|
|
## Dependencies (pom.xml)
|
|
|
|
```xml
|
|
<dependencies>
|
|
<!-- Spring Boot -->
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-web</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-data-jpa</artifactId>
|
|
</dependency>
|
|
|
|
<!-- Lombok -->
|
|
<dependency>
|
|
<groupId>org.projectlombok</groupId>
|
|
<artifactId>lombok</artifactId>
|
|
<scope>provided</scope>
|
|
</dependency>
|
|
|
|
<!-- Testing -->
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-test</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.assertj</groupId>
|
|
<artifactId>assertj-core</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
</dependencies>
|
|
```
|