Files
ai-development-scaffold/.claude/skills/languages/java/SKILL.md
James Bland befb8fbaeb feat: initial Claude Code configuration scaffold
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>
2026-01-20 15:47:34 -05:00

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>
```