Skip to content

Commit 76cfb4e

Browse files
Copilotafranken
andcommitted
Extract Spring guidance into docs/SPRING.md and reference it from AGENTS.md
Co-authored-by: afranken <763000+afranken@users.noreply.github.com>
1 parent b937959 commit 76cfb4e

File tree

2 files changed

+144
-1
lines changed

2 files changed

+144
-1
lines changed

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ See **[docs/KOTLIN.md](docs/KOTLIN.md)** for Kotlin idioms, naming conventions,
6868

6969
See **[docs/JAVA.md](docs/JAVA.md)** for Java idioms, naming conventions, common anti-patterns, and Javadoc guidelines.
7070

71-
**Spring**: `@RestController`, `@Service`, `@Component`, constructor injection over field injection
71+
See **[docs/SPRING.md](docs/SPRING.md)** for Spring Boot patterns, bean registration, dependency injection, controller guidelines, configuration properties, exception handling, and testing.
7272

7373
## XML Serialization
7474

docs/SPRING.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Spring Guidelines — S3Mock
2+
3+
Canonical reference for Spring Boot idioms, patterns, and code quality standards used across this project.
4+
5+
## Bean Registration
6+
7+
Register beans explicitly in `@Configuration` classes using `@Bean` factory methods — not via component scanning or class-level `@Service`/`@Component` annotations.
8+
9+
```kotlin
10+
@Configuration
11+
class ServiceConfiguration {
12+
@Bean
13+
fun bucketService(bucketStore: BucketStore, objectStore: ObjectStore): BucketService =
14+
BucketService(bucketStore, objectStore)
15+
}
16+
```
17+
18+
Three configuration layers mirror the architecture:
19+
- `StoreConfiguration` — store beans
20+
- `ServiceConfiguration` — service beans (imported in `@SpringBootTest`)
21+
- `ControllerConfiguration` — controller, filter, and exception-handler beans
22+
23+
## Dependency Injection
24+
25+
Always use **constructor injection**. Never use `@Autowired` on fields or setters in production code.
26+
27+
```kotlin
28+
// DO — constructor injection
29+
class BucketService(
30+
private val bucketStore: BucketStore,
31+
private val objectStore: ObjectStore,
32+
)
33+
34+
// DON'T — field injection
35+
class BucketService {
36+
@Autowired private lateinit var bucketStore: BucketStore
37+
}
38+
```
39+
40+
## Controllers
41+
42+
- `@RestController` classes map HTTP only — never contain business logic
43+
- All logic is delegated to a `@Service`; controllers call the service and return the result
44+
- Return typed DTOs, never raw strings
45+
- Controllers never catch exceptions — exception handlers in `ControllerConfiguration` do that
46+
47+
```kotlin
48+
// DO
49+
@GetMapping("/{bucketName}")
50+
fun getBucket(@PathVariable bucketName: String): ResponseEntity<ListBucketResult> =
51+
ResponseEntity.ok(bucketService.listObjects(bucketName))
52+
53+
// DON'T
54+
@GetMapping("/{bucketName}")
55+
fun getBucket(@PathVariable bucketName: String): String {
56+
// business logic here ...
57+
return "<ListBucketResult>...</ListBucketResult>"
58+
}
59+
```
60+
61+
## Configuration Properties
62+
63+
Bind configuration via `@ConfigurationProperties` data classes — never inject individual values with `@Value` in production code.
64+
65+
```kotlin
66+
@JvmRecord
67+
@ConfigurationProperties("com.adobe.testing.s3mock.store")
68+
data class StoreProperties(
69+
@param:DefaultValue("false") val retainFilesOnExit: Boolean,
70+
@param:DefaultValue("us-east-1") val region: Region,
71+
)
72+
```
73+
74+
Enable each properties class with `@EnableConfigurationProperties` in the matching `@Configuration`:
75+
76+
```kotlin
77+
@Configuration
78+
@EnableConfigurationProperties(StoreProperties::class)
79+
class StoreConfiguration { ... }
80+
```
81+
82+
## Exception Handling
83+
84+
- Services throw `S3Exception` constants (e.g., `S3Exception.NO_SUCH_BUCKET`) — never create new exception classes
85+
- Exception handlers are `@ControllerAdvice` classes registered as `@Bean`s in `ControllerConfiguration`
86+
- `S3MockExceptionHandler` converts `S3Exception` → XML `ErrorResponse` with the correct HTTP status
87+
- `IllegalStateExceptionHandler` converts unexpected errors → `500 InternalError`
88+
89+
```kotlin
90+
@ControllerAdvice
91+
class S3MockExceptionHandler : ResponseEntityExceptionHandler() {
92+
@ExceptionHandler(S3Exception::class)
93+
fun handleS3Exception(s3Exception: S3Exception): ResponseEntity<ErrorResponse> { ... }
94+
}
95+
```
96+
97+
## Testing
98+
99+
### Service and Store Tests — `@SpringBootTest`
100+
101+
Use `@SpringBootTest` scoped to the relevant `@Configuration` class with `@MockitoBean` for dependencies.
102+
103+
```kotlin
104+
@SpringBootTest(classes = [ServiceConfiguration::class], webEnvironment = SpringBootTest.WebEnvironment.NONE)
105+
@MockitoBean(types = [MultipartService::class, MultipartStore::class])
106+
internal class BucketServiceTest : ServiceTestBase() {
107+
@Autowired private lateinit var iut: BucketService
108+
109+
@Test
110+
fun `should return no such bucket`() { ... }
111+
}
112+
```
113+
114+
Always extend the appropriate base class:
115+
- `ServiceTestBase` — service layer tests
116+
- `StoreTestBase` — store layer tests
117+
118+
### Controller Tests — `@WebMvcTest`
119+
120+
Use `@WebMvcTest` scoped to the controller under test with `@MockitoBean` for services and `BaseControllerTest` for shared fixtures.
121+
122+
```kotlin
123+
@WebMvcTest(controllers = [BucketController::class], ...
124+
@MockitoBean(types = [BucketService::class])
125+
internal class BucketControllerTest : BaseControllerTest() {
126+
@Autowired private lateinit var mockMvc: MockMvc
127+
@Autowired private lateinit var bucketService: BucketService
128+
129+
@Test
130+
fun `should list buckets`() { ... }
131+
}
132+
```
133+
134+
### Common Anti-Patterns
135+
136+
| Anti-Pattern | Refactor To |
137+
|---|---|
138+
| `@ExtendWith(MockitoExtension::class)` + `@Mock` + `@InjectMocks` | `@SpringBootTest` + `@MockitoBean` + `@Autowired` |
139+
| `@Autowired` field injection in production code | Constructor injection |
140+
| Business logic in controller method | Delegate to `@Service` |
141+
| Returning a raw `String` from a controller | Return a typed DTO wrapped in `ResponseEntity` |
142+
| `@Value("${property}")` scattered throughout beans | `@ConfigurationProperties` data class |
143+
| New exception class for S3 errors | `S3Exception` constant |

0 commit comments

Comments
 (0)