@booklib/skills 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -2
- package/README.md +1 -0
- package/package.json +1 -1
- package/skills/spring-boot-in-action/SKILL.md +312 -0
- package/skills/spring-boot-in-action/evals/evals.json +39 -0
- package/skills/spring-boot-in-action/examples/after.md +185 -0
- package/skills/spring-boot-in-action/examples/before.md +84 -0
- package/skills/spring-boot-in-action/references/practices-catalog.md +403 -0
- package/skills/spring-boot-in-action/scripts/review.py +184 -0
package/CLAUDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# booklib-ai/skills
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
22 AI agent skills grounded in canonical programming books. Each skill packages expert practices from a specific book into reusable instructions that Claude and other AI agents can apply to code generation, code review, and design decisions.
|
|
4
4
|
|
|
5
5
|
## Quick Install
|
|
6
6
|
|
|
@@ -25,6 +25,7 @@ npx skills add booklib-ai/skills --all -g
|
|
|
25
25
|
| `kotlin-in-action` | Kotlin in Action |
|
|
26
26
|
| `programming-with-rust` | Programming with Rust — Donis Marshall |
|
|
27
27
|
| `rust-in-action` | Rust in Action — Tim McNamara |
|
|
28
|
+
| `spring-boot-in-action` | Spring Boot in Action — Craig Walls |
|
|
28
29
|
| `lean-startup` | The Lean Startup — Eric Ries |
|
|
29
30
|
| `microservices-patterns` | Microservices Patterns — Chris Richardson |
|
|
30
31
|
| `refactoring-ui` | Refactoring UI — Adam Wathan & Steve Schoger |
|
|
@@ -54,4 +55,4 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to add a new skill. Each skill
|
|
|
54
55
|
npx @booklib/skills check <skill-name>
|
|
55
56
|
```
|
|
56
57
|
|
|
57
|
-
All
|
|
58
|
+
All 22 existing skills are at Platinum (13/13 checks).
|
package/README.md
CHANGED
|
@@ -134,6 +134,7 @@ This means skills compose: `skill-router` acts as an orchestrator that picks the
|
|
|
134
134
|
| 🔷 [effective-typescript](./skills/effective-typescript/) | TypeScript best practices from Dan Vanderkam's *Effective TypeScript* — type system, type design, avoiding any, type declarations, and migration |
|
|
135
135
|
| 🦀 [programming-with-rust](./skills/programming-with-rust/) | Rust practices from Donis Marshall's *Programming with Rust* — ownership, borrowing, lifetimes, error handling, traits, and fearless concurrency |
|
|
136
136
|
| ⚙️ [rust-in-action](./skills/rust-in-action/) | Systems programming from Tim McNamara's *Rust in Action* — smart pointers, endianness, memory, file formats, TCP networking, concurrency, and OS fundamentals |
|
|
137
|
+
| 🌱 [spring-boot-in-action](./skills/spring-boot-in-action/) | Spring Boot best practices from Craig Walls' *Spring Boot in Action* — auto-configuration, starters, externalized config, profiles, testing with MockMvc, Actuator, and deployment |
|
|
137
138
|
| ⚡ [kotlin-in-action](./skills/kotlin-in-action/) | Practices from *Kotlin in Action* (2nd Ed) — functions, classes, lambdas, nullability, and coroutines |
|
|
138
139
|
| 🚀 [lean-startup](./skills/lean-startup/) | Practices from Eric Ries' *The Lean Startup* — MVP testing, validated learning, Build-Measure-Learn loop, and pivots |
|
|
139
140
|
| 🔧 [microservices-patterns](./skills/microservices-patterns/) | Expert guidance on microservices patterns from Chris Richardson's *Microservices Patterns* — decomposition, sagas, API gateways, event sourcing, CQRS, and service mesh |
|
package/package.json
CHANGED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spring-boot-in-action
|
|
3
|
+
description: >
|
|
4
|
+
Write and review Spring Boot applications using practices from "Spring Boot in Action"
|
|
5
|
+
by Craig Walls. Covers auto-configuration, starter dependencies, externalizing
|
|
6
|
+
configuration with properties and profiles, Spring Security, testing with MockMvc
|
|
7
|
+
and @SpringBootTest, Spring Actuator for production observability, and deployment
|
|
8
|
+
strategies (JAR, WAR, Cloud Foundry). Use when building Spring Boot apps, configuring
|
|
9
|
+
beans, writing integration tests, setting up health checks, or deploying to production.
|
|
10
|
+
Trigger on: "Spring Boot", "Spring", "@SpringBootApplication", "auto-configuration",
|
|
11
|
+
"application.properties", "application.yml", "@RestController", "@Service",
|
|
12
|
+
"@Repository", "SpringBootTest", "Actuator", "starter", ".java files", "Maven", "Gradle".
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Spring Boot in Action Skill
|
|
16
|
+
|
|
17
|
+
Apply the practices from Craig Walls' "Spring Boot in Action" to review existing code and write new Spring Boot applications. This skill operates in two modes: **Review Mode** (analyze code for violations of Spring Boot idioms) and **Write Mode** (produce clean, idiomatic Spring Boot from scratch).
|
|
18
|
+
|
|
19
|
+
The core philosophy: Spring Boot removes boilerplate through **auto-configuration**, **starter dependencies**, and **sensible defaults**. Fight the framework only when necessary — and when you do, prefer `application.properties` over code.
|
|
20
|
+
|
|
21
|
+
## Reference Files
|
|
22
|
+
|
|
23
|
+
- `practices-catalog.md` — Before/after examples for auto-configuration, starters, properties, profiles, security, testing, Actuator, and deployment
|
|
24
|
+
|
|
25
|
+
## How to Use This Skill
|
|
26
|
+
|
|
27
|
+
**Before responding**, read `practices-catalog.md` for the topic at hand. For configuration issues read the properties/profiles section. For test code read the testing section. For a full review, read all sections.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Mode 1: Code Review
|
|
32
|
+
|
|
33
|
+
When the user asks you to **review** Spring Boot code, follow this process:
|
|
34
|
+
|
|
35
|
+
### Step 1: Identify the Layer
|
|
36
|
+
Determine whether the code is a controller, service, repository, configuration class, or test. Review focus shifts by layer.
|
|
37
|
+
|
|
38
|
+
### Step 2: Analyze the Code
|
|
39
|
+
|
|
40
|
+
Check these areas in order of severity:
|
|
41
|
+
|
|
42
|
+
1. **Auto-Configuration** (Ch 2, 3): Is auto-configuration being fought manually? Look for `@Bean` definitions that replicate what Spring Boot already provides (DataSource, Jackson, Security, etc.). Remove manual config where auto-config suffices.
|
|
43
|
+
|
|
44
|
+
2. **Starter Dependencies** (Ch 2): Are dependencies declared individually instead of using starters? `spring-boot-starter-web`, `spring-boot-starter-data-jpa`, `spring-boot-starter-security` etc. bundle correct transitive dependencies and version-manage them.
|
|
45
|
+
|
|
46
|
+
3. **Externalized Configuration** (Ch 3): Are values hardcoded that belong in `application.properties`? Ports, URLs, credentials, timeouts should all be externalized. Use `@ConfigurationProperties` for type-safe config objects; use `@Value` only for single values.
|
|
47
|
+
|
|
48
|
+
4. **Profiles** (Ch 3): Is environment-specific config (dev DB vs prod DB) handled with `if` statements or system properties? Use `@Profile` and `application-{profile}.properties` instead.
|
|
49
|
+
|
|
50
|
+
5. **Security** (Ch 3): Is `WebSecurityConfigurerAdapter` extended when simple property-based config would suffice? Is HTTP Basic enabled in production? Are actuator endpoints exposed without auth?
|
|
51
|
+
|
|
52
|
+
6. **Testing** (Ch 4):
|
|
53
|
+
- Use `@SpringBootTest` for full integration tests, not raw `new MyService()`
|
|
54
|
+
- Use `@WebMvcTest` for controller-only tests (no full context)
|
|
55
|
+
- Use `@DataJpaTest` for repository tests (in-memory DB, no web layer)
|
|
56
|
+
- Use `MockMvc` for controller assertions without starting a server
|
|
57
|
+
- Use `@MockBean` to replace real beans with mocks in slice tests
|
|
58
|
+
- Avoid `@SpringBootTest(webEnvironment = RANDOM_PORT)` unless testing the full HTTP stack
|
|
59
|
+
|
|
60
|
+
7. **Actuator** (Ch 7): Is the application missing health/metrics endpoints? Is `/actuator` fully exposed without security? Are custom health indicators implemented for critical dependencies?
|
|
61
|
+
|
|
62
|
+
8. **Deployment** (Ch 8): Is `spring.profiles.active` set for production? Is database migration (Flyway/Liquibase) configured? Is the app packaged as a self-contained JAR (preferred) or WAR?
|
|
63
|
+
|
|
64
|
+
9. **General Idioms**:
|
|
65
|
+
- Constructor injection over field injection (`@Autowired` on fields)
|
|
66
|
+
- `@RestController` = `@Controller` + `@ResponseBody` — use it for REST APIs
|
|
67
|
+
- Return `ResponseEntity<T>` from controllers when status codes matter
|
|
68
|
+
- `Optional<T>` from repository methods, never `null`
|
|
69
|
+
|
|
70
|
+
### Step 3: Report Findings
|
|
71
|
+
For each issue, report:
|
|
72
|
+
- **Chapter reference** (e.g., "Ch 3: Externalized Configuration")
|
|
73
|
+
- **Location** in the code
|
|
74
|
+
- **What's wrong** (the anti-pattern)
|
|
75
|
+
- **How to fix it** (the Spring Boot idiomatic way)
|
|
76
|
+
- **Priority**: Critical (security/bugs), Important (maintainability), Suggestion (polish)
|
|
77
|
+
|
|
78
|
+
### Step 4: Provide Fixed Code
|
|
79
|
+
Offer a corrected version with comments explaining each change.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Mode 2: Writing New Code
|
|
84
|
+
|
|
85
|
+
When the user asks you to **write** new Spring Boot code, apply these core principles:
|
|
86
|
+
|
|
87
|
+
### Project Bootstrap (Ch 1, 2)
|
|
88
|
+
|
|
89
|
+
1. **Start with Spring Initializr** (Ch 1). Use `start.spring.io` or `spring init` CLI. Select starters upfront — don't add raw dependencies manually.
|
|
90
|
+
|
|
91
|
+
2. **Use starters, not individual dependencies** (Ch 2). `spring-boot-starter-web` includes Tomcat, Spring MVC, Jackson, and logging at compatible versions. Never declare `spring-webmvc` + `jackson-databind` + `tomcat-embed-core` separately.
|
|
92
|
+
|
|
93
|
+
3. **The main class is the only required boilerplate** (Ch 2):
|
|
94
|
+
```java
|
|
95
|
+
@SpringBootApplication
|
|
96
|
+
public class MyApp {
|
|
97
|
+
public static void main(String[] args) {
|
|
98
|
+
SpringApplication.run(MyApp.class, args);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
`@SpringBootApplication` = `@Configuration` + `@EnableAutoConfiguration` + `@ComponentScan`.
|
|
103
|
+
|
|
104
|
+
### Configuration (Ch 3)
|
|
105
|
+
|
|
106
|
+
4. **Externalize all environment-specific values** (Ch 3). Nothing deployment-specific belongs in code. Use `application.properties` / `application.yml` for defaults.
|
|
107
|
+
|
|
108
|
+
5. **Use `@ConfigurationProperties` for grouped config** (Ch 3). Bind a prefix to a POJO — type-safe, IDE-friendly, testable:
|
|
109
|
+
```java
|
|
110
|
+
@ConfigurationProperties(prefix = "app.mail")
|
|
111
|
+
@Component
|
|
112
|
+
public class MailProperties {
|
|
113
|
+
private String host;
|
|
114
|
+
private int port = 25;
|
|
115
|
+
// getters + setters
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
6. **Use profiles for environment differences** (Ch 3). `application-dev.properties` overrides `application.properties` when `spring.profiles.active=dev`. Never use `if (env.equals("production"))` in code.
|
|
120
|
+
|
|
121
|
+
7. **Override auto-configuration surgically** (Ch 3). Use `spring.*` properties first. Only define a `@Bean` when properties are insufficient. Annotate with `@ConditionalOnMissingBean` if providing a fallback.
|
|
122
|
+
|
|
123
|
+
8. **Customize error pages declaratively** (Ch 3). Place `error/404.html`, `error/500.html` in `src/main/resources/templates/error/`. No custom `ErrorController` needed for basic cases.
|
|
124
|
+
|
|
125
|
+
### Security (Ch 3)
|
|
126
|
+
|
|
127
|
+
9. **Extend `WebSecurityConfigurerAdapter` only for custom rules** (Ch 3). For simple HTTP Basic with custom users, `spring.security.user.name` / `spring.security.user.password` properties suffice.
|
|
128
|
+
|
|
129
|
+
10. **Always secure Actuator endpoints in production** (Ch 7). Expose only `health` and `info` publicly; require authentication for `env`, `beans`, `mappings`, `shutdown`.
|
|
130
|
+
|
|
131
|
+
### REST Controllers (Ch 2)
|
|
132
|
+
|
|
133
|
+
11. **Use `@RestController` for API endpoints** (Ch 2). Eliminates `@ResponseBody` on every method.
|
|
134
|
+
|
|
135
|
+
12. **Return `ResponseEntity<T>` when HTTP status matters** (Ch 2). `ResponseEntity.ok(body)`, `ResponseEntity.notFound().build()`, `ResponseEntity.status(201).body(created)`.
|
|
136
|
+
|
|
137
|
+
13. **Use constructor injection, not field injection** (Ch 2). Constructor injection makes dependencies explicit and enables testing without Spring context:
|
|
138
|
+
```java
|
|
139
|
+
// Prefer this:
|
|
140
|
+
@RestController
|
|
141
|
+
public class BookController {
|
|
142
|
+
private final BookRepository repo;
|
|
143
|
+
public BookController(BookRepository repo) { this.repo = repo; }
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
14. **Use `Optional` from repository queries** (Ch 2). `repo.findById(id).orElseThrow(() -> new ResponseStatusException(NOT_FOUND))`.
|
|
148
|
+
|
|
149
|
+
### Testing (Ch 4)
|
|
150
|
+
|
|
151
|
+
15. **Match test slice to the layer being tested** (Ch 4):
|
|
152
|
+
- Web layer only → `@WebMvcTest(MyController.class)` + `MockMvc`
|
|
153
|
+
- Repository only → `@DataJpaTest`
|
|
154
|
+
- Full app → `@SpringBootTest`
|
|
155
|
+
- External service → `@MockBean` to replace
|
|
156
|
+
|
|
157
|
+
16. **Use `MockMvc` for controller assertions without starting a server** (Ch 4):
|
|
158
|
+
```java
|
|
159
|
+
mockMvc.perform(get("/books/1"))
|
|
160
|
+
.andExpect(status().isOk())
|
|
161
|
+
.andExpect(jsonPath("$.title").value("Spring Boot in Action"));
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
17. **Use `@MockBean` to isolate the unit under test** (Ch 4). Replaces the real bean in the Spring context with a Mockito mock — cleaner than manual wiring.
|
|
165
|
+
|
|
166
|
+
18. **Test security explicitly** (Ch 4). Use `.with(user("admin").roles("ADMIN"))` or `@WithMockUser` to assert secured endpoints reject unauthenticated requests.
|
|
167
|
+
|
|
168
|
+
### Actuator (Ch 7)
|
|
169
|
+
|
|
170
|
+
19. **Enable Actuator in every production app** (Ch 7). Add `spring-boot-starter-actuator`. At minimum expose `health` and `info`.
|
|
171
|
+
|
|
172
|
+
20. **Write custom `HealthIndicator` for critical dependencies** (Ch 7):
|
|
173
|
+
```java
|
|
174
|
+
@Component
|
|
175
|
+
public class DatabaseHealthIndicator implements HealthIndicator {
|
|
176
|
+
@Override
|
|
177
|
+
public Health health() {
|
|
178
|
+
return canConnect() ? Health.up().build()
|
|
179
|
+
: Health.down().withDetail("reason", "timeout").build();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
21. **Add custom metrics via `MeterRegistry`** (Ch 7). Counter, gauge, timer — gives Prometheus/Grafana visibility into business events.
|
|
185
|
+
|
|
186
|
+
22. **Restrict Actuator exposure in production** (Ch 7):
|
|
187
|
+
```properties
|
|
188
|
+
management.endpoints.web.exposure.include=health,info
|
|
189
|
+
management.endpoint.health.show-details=when-authorized
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Deployment (Ch 8)
|
|
193
|
+
|
|
194
|
+
23. **Package as an executable JAR by default** (Ch 8). `mvn package` produces a fat JAR with embedded Tomcat. Run with `java -jar app.jar`. No application server needed.
|
|
195
|
+
|
|
196
|
+
24. **Create a production profile** (Ch 8). `application-production.properties` sets `spring.datasource.url`, disables dev tools, sets log levels to WARN.
|
|
197
|
+
|
|
198
|
+
25. **Use Flyway or Liquibase for database migrations** (Ch 8). Add `spring-boot-starter-flyway`; place scripts in `classpath:db/migration/V1__init.sql`. Never use `spring.jpa.hibernate.ddl-auto=create` in production.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Starter Cheat Sheet (Ch 2, Appendix B)
|
|
203
|
+
|
|
204
|
+
| Need | Starter |
|
|
205
|
+
|------|---------|
|
|
206
|
+
| REST API | `spring-boot-starter-web` |
|
|
207
|
+
| JPA / Hibernate | `spring-boot-starter-data-jpa` |
|
|
208
|
+
| Security | `spring-boot-starter-security` |
|
|
209
|
+
| Observability | `spring-boot-starter-actuator` |
|
|
210
|
+
| Testing | `spring-boot-starter-test` |
|
|
211
|
+
| Thymeleaf views | `spring-boot-starter-thymeleaf` |
|
|
212
|
+
| Redis cache | `spring-boot-starter-data-redis` |
|
|
213
|
+
| Messaging | `spring-boot-starter-amqp` |
|
|
214
|
+
| DB migration | `flyway-core` |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Code Structure Template
|
|
219
|
+
|
|
220
|
+
```java
|
|
221
|
+
// Main class (Ch 2)
|
|
222
|
+
@SpringBootApplication
|
|
223
|
+
public class LibraryApp {
|
|
224
|
+
public static void main(String[] args) {
|
|
225
|
+
SpringApplication.run(LibraryApp.class, args);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Entity (Ch 2)
|
|
230
|
+
@Entity
|
|
231
|
+
public class Book {
|
|
232
|
+
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
233
|
+
private Long id;
|
|
234
|
+
private String title;
|
|
235
|
+
private String isbn;
|
|
236
|
+
// constructors, getters, setters
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Repository (Ch 2)
|
|
240
|
+
public interface BookRepository extends JpaRepository<Book, Long> {
|
|
241
|
+
List<Book> findByTitleContainingIgnoreCase(String title);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Service (Ch 2) — constructor injection
|
|
245
|
+
@Service
|
|
246
|
+
public class BookService {
|
|
247
|
+
private final BookRepository repo;
|
|
248
|
+
public BookService(BookRepository repo) { this.repo = repo; }
|
|
249
|
+
|
|
250
|
+
public Book findById(Long id) {
|
|
251
|
+
return repo.findById(id)
|
|
252
|
+
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Controller (Ch 2)
|
|
257
|
+
@RestController
|
|
258
|
+
@RequestMapping("/api/books")
|
|
259
|
+
public class BookController {
|
|
260
|
+
private final BookService service;
|
|
261
|
+
public BookController(BookService service) { this.service = service; }
|
|
262
|
+
|
|
263
|
+
@GetMapping("/{id}")
|
|
264
|
+
public ResponseEntity<Book> getBook(@PathVariable Long id) {
|
|
265
|
+
return ResponseEntity.ok(service.findById(id));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@PostMapping
|
|
269
|
+
public ResponseEntity<Book> createBook(@RequestBody Book book) {
|
|
270
|
+
Book saved = service.save(book);
|
|
271
|
+
URI location = URI.create("/api/books/" + saved.getId());
|
|
272
|
+
return ResponseEntity.created(location).body(saved);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// application.properties (Ch 3)
|
|
277
|
+
// spring.datasource.url=jdbc:postgresql://localhost/library
|
|
278
|
+
// spring.datasource.username=${DB_USER}
|
|
279
|
+
// spring.datasource.password=${DB_PASS}
|
|
280
|
+
// spring.jpa.hibernate.ddl-auto=validate
|
|
281
|
+
// management.endpoints.web.exposure.include=health,info
|
|
282
|
+
|
|
283
|
+
// application-dev.properties (Ch 3)
|
|
284
|
+
// spring.datasource.url=jdbc:h2:mem:library
|
|
285
|
+
// spring.jpa.hibernate.ddl-auto=create-drop
|
|
286
|
+
// logging.level.org.springframework=DEBUG
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Priority of Practices by Impact
|
|
292
|
+
|
|
293
|
+
### Critical (Security & Correctness)
|
|
294
|
+
- Ch 3: Never hardcode credentials — use `${ENV_VAR}` in properties
|
|
295
|
+
- Ch 3: Secure Actuator endpoints — `env`, `beans`, `shutdown` must require auth
|
|
296
|
+
- Ch 4: Test secured endpoints explicitly — assert 401/403 on unauthenticated requests
|
|
297
|
+
- Ch 8: Never use `ddl-auto=create` in production — use Flyway/Liquibase
|
|
298
|
+
|
|
299
|
+
### Important (Idiom & Maintainability)
|
|
300
|
+
- Ch 2: Constructor injection over `@Autowired` field injection
|
|
301
|
+
- Ch 2: `@RestController` over `@Controller` + `@ResponseBody` for APIs
|
|
302
|
+
- Ch 2: `Optional` from repository, never `null`
|
|
303
|
+
- Ch 3: `@ConfigurationProperties` over scattered `@Value` for grouped config
|
|
304
|
+
- Ch 3: Profiles for environment differences — not `if` statements
|
|
305
|
+
- Ch 4: `@WebMvcTest` for controller tests — not full `@SpringBootTest`
|
|
306
|
+
- Ch 7: Custom `HealthIndicator` for each critical dependency
|
|
307
|
+
|
|
308
|
+
### Suggestions (Polish)
|
|
309
|
+
- Ch 3: Custom error pages in `templates/error/` — no code needed
|
|
310
|
+
- Ch 7: Custom metrics via `MeterRegistry` for business events
|
|
311
|
+
- Ch 8: Production profile disables dev tools, sets WARN log level
|
|
312
|
+
- Ch 2: Use `spring-boot-devtools` in dev for live reload
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"evals": [
|
|
3
|
+
{
|
|
4
|
+
"id": "eval-01-autoconfig-injection-hardcoding",
|
|
5
|
+
"prompt": "Review this Spring Boot code:\n\n```java\n@Configuration\npublic class AppConfig {\n @Bean\n public DataSource dataSource() {\n DriverManagerDataSource ds = new DriverManagerDataSource();\n ds.setUrl(\"jdbc:postgresql://prod-db.internal/orders\");\n ds.setUsername(\"orders_user\");\n ds.setPassword(\"S3cr3tP@ss\");\n return ds;\n }\n\n @Bean\n public ObjectMapper objectMapper() {\n return new ObjectMapper();\n }\n}\n\n@RestController\npublic class OrderController {\n @Autowired\n private OrderRepository orderRepository;\n\n @Autowired\n private OrderService orderService;\n\n @GetMapping(\"/orders/{id}\")\n public Order getOrder(@PathVariable Long id) {\n return orderRepository.findById(id).orElse(null);\n }\n\n @PostMapping(\"/orders\")\n public Order createOrder(@RequestBody Order order) {\n return orderService.place(order);\n }\n}\n```",
|
|
6
|
+
"expectations": [
|
|
7
|
+
"Flag Ch 2/3: Manual DataSource @Bean fights Spring Boot auto-configuration — delete AppConfig.dataSource() and move connection details to application.properties using spring.datasource.url/username/password",
|
|
8
|
+
"Flag Ch 3: Credentials hardcoded in source code — must be externalized to environment variables: spring.datasource.password=${DB_PASS}",
|
|
9
|
+
"Flag Ch 2: ObjectMapper @Bean is unnecessary — Spring Boot auto-configures Jackson; only define a custom ObjectMapper when you need to change behavior",
|
|
10
|
+
"Flag Ch 2: @Autowired field injection on OrderRepository and OrderService — replace with constructor injection for testability",
|
|
11
|
+
"Flag Ch 2: getOrder returns null (200 with null body) when not found — use Optional.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build()) or throw ResponseStatusException(NOT_FOUND)",
|
|
12
|
+
"Flag Ch 2: createOrder returns 200 — POST that creates a resource should return 201 Created with a Location header; use ResponseEntity.created(uri).body(saved)"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "eval-02-testing-antipatterns",
|
|
17
|
+
"prompt": "Review this Spring Boot test code:\n\n```java\n@SpringBootTest\npublic class ProductControllerTest {\n @Autowired\n private ProductController controller;\n\n @Autowired\n private ProductRepository repo;\n\n @Test\n public void testGetProduct() {\n Product p = new Product(null, \"Widget\", 9.99);\n repo.save(p);\n Product result = controller.getProduct(p.getId());\n assertNotNull(result);\n assertEquals(\"Widget\", result.getName());\n }\n\n @Test\n public void testCreateProduct() {\n Product p = new Product(null, \"Gadget\", 19.99);\n Product result = controller.createProduct(p);\n assertNotNull(result.getId());\n }\n\n @Test\n public void testAdminEndpoint() {\n // No auth setup — just calls controller directly\n String result = controller.adminDashboard();\n assertNotNull(result);\n }\n}\n```",
|
|
18
|
+
"expectations": [
|
|
19
|
+
"Flag Ch 4: @SpringBootTest loads the full application context for what are simple controller tests — use @WebMvcTest(ProductController.class) with MockMvc for fast, isolated controller tests",
|
|
20
|
+
"Flag Ch 4: Directly calling controller.getProduct() bypasses HTTP layer — no status code, content-type, or header assertions are possible; use MockMvc.perform(get(...)).andExpect(status().isOk())",
|
|
21
|
+
"Flag Ch 4: testAdminEndpoint calls controller directly with no authentication context — use @WithMockUser(roles='ADMIN') and MockMvc to assert 403 for unauthorized and 200 for authorized access",
|
|
22
|
+
"Flag Ch 4: Tests use a real ProductRepository writing to the database — in a @WebMvcTest test, use @MockBean ProductService to isolate the controller from persistence",
|
|
23
|
+
"Flag Ch 4: No negative test cases — missing test for product not found (expect 404), and no test verifying createProduct returns 201 with a Location header",
|
|
24
|
+
"Provide corrected test class using @WebMvcTest, MockMvc, @MockBean, @WithMockUser, and assertions on HTTP status codes and response JSON"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "eval-03-idiomatic-spring-boot",
|
|
29
|
+
"prompt": "Review this Spring Boot code:\n\n```java\n@SpringBootApplication\npublic class LibraryApp {\n public static void main(String[] args) {\n SpringApplication.run(LibraryApp.class, args);\n }\n}\n\n@RestController\n@RequestMapping(\"/api/books\")\npublic class BookController {\n private final BookService service;\n\n public BookController(BookService service) {\n this.service = service;\n }\n\n @GetMapping(\"/{id}\")\n public ResponseEntity<Book> getBook(@PathVariable Long id) {\n return ResponseEntity.ok(service.findById(id));\n }\n\n @PostMapping\n public ResponseEntity<Book> createBook(@RequestBody Book book) {\n Book saved = service.save(book);\n URI location = URI.create(\"/api/books/\" + saved.getId());\n return ResponseEntity.created(location).body(saved);\n }\n}\n\n@Service\npublic class BookService {\n private static final Logger log = LoggerFactory.getLogger(BookService.class);\n private final BookRepository repo;\n\n public BookService(BookRepository repo) { this.repo = repo; }\n\n public Book findById(Long id) {\n return repo.findById(id)\n .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));\n }\n\n public Book save(Book book) { return repo.save(book); }\n}\n```\n\n```properties\nspring.datasource.url=${DB_URL:jdbc:h2:mem:library}\nspring.datasource.username=${DB_USER:sa}\nspring.datasource.password=${DB_PASS:}\nspring.jpa.hibernate.ddl-auto=validate\nmanagement.endpoints.web.exposure.include=health,info\n```",
|
|
30
|
+
"expectations": [
|
|
31
|
+
"Recognize this code is idiomatic Spring Boot — do NOT manufacture issues",
|
|
32
|
+
"Acknowledge correct patterns: @SpringBootApplication (Ch 1), constructor injection in both controller and service (Ch 2), ResponseEntity with correct 200/201 status codes and Location header (Ch 2), Optional.orElseThrow with ResponseStatusException for clean 404 (Ch 2), SLF4J logger (Ch 3), externalized config with env-var defaults (Ch 3), Actuator locked to health+info (Ch 7)",
|
|
33
|
+
"At most note: ResponseStatusException could include a descriptive message like 'Book ' + id + ' not found' for better client error messages",
|
|
34
|
+
"At most suggest: adding spring-boot-starter-actuator dependency if not already present to enable the management.endpoints config",
|
|
35
|
+
"Do NOT flag the absence of @Autowired — constructor injection is the preferred style and Spring auto-wires single constructors without any annotation"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# After: Spring Boot in Action
|
|
2
|
+
|
|
3
|
+
The same library API rewritten with idiomatic Spring Boot — auto-configuration, constructor injection, externalized config, profiles, proper testing, and Actuator.
|
|
4
|
+
|
|
5
|
+
```java
|
|
6
|
+
// @SpringBootApplication enables auto-config, component scan, config (Ch 1, 2)
|
|
7
|
+
@SpringBootApplication
|
|
8
|
+
public class LibraryApp {
|
|
9
|
+
public static void main(String[] args) {
|
|
10
|
+
SpringApplication.run(LibraryApp.class, args);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// No DatabaseConfig class needed — auto-configuration handles DataSource (Ch 2, 3)
|
|
15
|
+
// Credentials externalized to application.properties via environment variables
|
|
16
|
+
|
|
17
|
+
// Constructor injection — testable without Spring context (Ch 2)
|
|
18
|
+
@RestController
|
|
19
|
+
@RequestMapping("/api/books")
|
|
20
|
+
public class BookController {
|
|
21
|
+
private final BookService service;
|
|
22
|
+
|
|
23
|
+
public BookController(BookService service) {
|
|
24
|
+
this.service = service;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Returns 404 when not found, not null (Ch 2)
|
|
28
|
+
@GetMapping("/{id}")
|
|
29
|
+
public ResponseEntity<Book> getBook(@PathVariable Long id) {
|
|
30
|
+
return ResponseEntity.ok(service.findById(id));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Returns 201 Created with Location header (Ch 2)
|
|
34
|
+
@PostMapping
|
|
35
|
+
public ResponseEntity<Book> createBook(@RequestBody Book book) {
|
|
36
|
+
Book saved = service.save(book);
|
|
37
|
+
URI location = URI.create("/api/books/" + saved.getId());
|
|
38
|
+
return ResponseEntity.created(location).body(saved);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@GetMapping
|
|
42
|
+
public List<Book> search(@RequestParam(required = false, defaultValue = "") String q) {
|
|
43
|
+
return service.search(q);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Service with constructor injection and proper logging (Ch 2)
|
|
48
|
+
@Service
|
|
49
|
+
public class BookService {
|
|
50
|
+
private static final Logger log = LoggerFactory.getLogger(BookService.class);
|
|
51
|
+
private final BookRepository repo;
|
|
52
|
+
|
|
53
|
+
public BookService(BookRepository repo) {
|
|
54
|
+
this.repo = repo;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public Book findById(Long id) {
|
|
58
|
+
// Optional — 404 automatically surfaced (Ch 2)
|
|
59
|
+
return repo.findById(id)
|
|
60
|
+
.orElseThrow(() -> new ResponseStatusException(
|
|
61
|
+
HttpStatus.NOT_FOUND, "Book " + id + " not found"));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public Book save(Book book) {
|
|
65
|
+
return repo.save(book);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public List<Book> search(String query) {
|
|
69
|
+
log.debug("Searching for: {}", query); // proper logger, not println (Ch 3)
|
|
70
|
+
return query.isBlank()
|
|
71
|
+
? repo.findAll()
|
|
72
|
+
: repo.findByTitleContainingIgnoreCase(query);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Repository — Spring Data does the rest (Ch 2)
|
|
77
|
+
public interface BookRepository extends JpaRepository<Book, Long> {
|
|
78
|
+
List<Book> findByTitleContainingIgnoreCase(String title);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Type-safe configuration object (Ch 3)
|
|
82
|
+
@ConfigurationProperties(prefix = "app.library")
|
|
83
|
+
@Component
|
|
84
|
+
public class LibraryProperties {
|
|
85
|
+
private int maxSearchResults = 50;
|
|
86
|
+
private String defaultSortField = "title";
|
|
87
|
+
// getters + setters
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Custom health indicator for critical dependency (Ch 7)
|
|
91
|
+
@Component
|
|
92
|
+
public class StorageHealthIndicator implements HealthIndicator {
|
|
93
|
+
private final BookRepository repo;
|
|
94
|
+
public StorageHealthIndicator(BookRepository repo) { this.repo = repo; }
|
|
95
|
+
|
|
96
|
+
@Override
|
|
97
|
+
public Health health() {
|
|
98
|
+
try {
|
|
99
|
+
long count = repo.count();
|
|
100
|
+
return Health.up().withDetail("books", count).build();
|
|
101
|
+
} catch (Exception e) {
|
|
102
|
+
return Health.down().withDetail("error", e.getMessage()).build();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Controller slice test — no full context, fast (Ch 4)
|
|
108
|
+
@WebMvcTest(BookController.class)
|
|
109
|
+
public class BookControllerTest {
|
|
110
|
+
@Autowired
|
|
111
|
+
private MockMvc mockMvc;
|
|
112
|
+
|
|
113
|
+
@MockBean
|
|
114
|
+
private BookService service; // real service replaced with mock (Ch 4)
|
|
115
|
+
|
|
116
|
+
@Test
|
|
117
|
+
void getBook_returnsOk() throws Exception {
|
|
118
|
+
Book book = new Book(1L, "Spring Boot in Action", "9781617292545");
|
|
119
|
+
given(service.findById(1L)).willReturn(book);
|
|
120
|
+
|
|
121
|
+
mockMvc.perform(get("/api/books/1"))
|
|
122
|
+
.andExpect(status().isOk())
|
|
123
|
+
.andExpect(jsonPath("$.title").value("Spring Boot in Action"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@Test
|
|
127
|
+
void getBook_returns404WhenNotFound() throws Exception {
|
|
128
|
+
given(service.findById(99L))
|
|
129
|
+
.willThrow(new ResponseStatusException(HttpStatus.NOT_FOUND));
|
|
130
|
+
|
|
131
|
+
mockMvc.perform(get("/api/books/99"))
|
|
132
|
+
.andExpect(status().isNotFound());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@Test
|
|
136
|
+
@WithMockUser(roles = "USER")
|
|
137
|
+
void createBook_returns201() throws Exception {
|
|
138
|
+
Book book = new Book(null, "New Book", "1234567890");
|
|
139
|
+
Book saved = new Book(1L, "New Book", "1234567890");
|
|
140
|
+
given(service.save(any())).willReturn(saved);
|
|
141
|
+
|
|
142
|
+
mockMvc.perform(post("/api/books")
|
|
143
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
144
|
+
.content("{\"title\":\"New Book\",\"isbn\":\"1234567890\"}"))
|
|
145
|
+
.andExpect(status().isCreated())
|
|
146
|
+
.andExpect(header().string("Location", "/api/books/1"));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```properties
|
|
152
|
+
# application.properties — base config, all env-specific values externalized (Ch 3)
|
|
153
|
+
spring.datasource.url=${DB_URL:jdbc:h2:mem:library}
|
|
154
|
+
spring.datasource.username=${DB_USER:sa}
|
|
155
|
+
spring.datasource.password=${DB_PASS:}
|
|
156
|
+
spring.jpa.hibernate.ddl-auto=validate
|
|
157
|
+
|
|
158
|
+
# Actuator — health and info only exposed publicly (Ch 7)
|
|
159
|
+
management.endpoints.web.exposure.include=health,info
|
|
160
|
+
management.endpoint.health.show-details=when-authorized
|
|
161
|
+
|
|
162
|
+
# application-dev.properties — dev overrides (Ch 3)
|
|
163
|
+
# spring.datasource.url=jdbc:h2:mem:library
|
|
164
|
+
# spring.jpa.hibernate.ddl-auto=create-drop
|
|
165
|
+
# logging.level.com.example=DEBUG
|
|
166
|
+
# management.endpoints.web.exposure.include=*
|
|
167
|
+
|
|
168
|
+
# application-production.properties — production hardening (Ch 8)
|
|
169
|
+
# spring.jpa.hibernate.ddl-auto=validate
|
|
170
|
+
# logging.level.root=WARN
|
|
171
|
+
# management.endpoints.web.exposure.include=health,info
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Key improvements:**
|
|
175
|
+
- `@SpringBootApplication` enables auto-configuration — no manual `DataSource` bean (Ch 2)
|
|
176
|
+
- Credentials externalized to env vars via `${DB_URL}` — never hardcoded (Ch 3)
|
|
177
|
+
- Constructor injection throughout — testable without Spring context (Ch 2)
|
|
178
|
+
- `ResponseEntity` with correct status codes: 200, 201, 404 (Ch 2)
|
|
179
|
+
- `Optional` → `orElseThrow` → `ResponseStatusException` — clean 404 (Ch 2)
|
|
180
|
+
- `@ConfigurationProperties` for grouped app config (Ch 3)
|
|
181
|
+
- `@WebMvcTest` + `@MockBean` — fast, isolated controller tests (Ch 4)
|
|
182
|
+
- `@WithMockUser` — security tested explicitly (Ch 4)
|
|
183
|
+
- Custom `HealthIndicator` — DB health visible in Actuator (Ch 7)
|
|
184
|
+
- Actuator locked down — only `health` and `info` public (Ch 7)
|
|
185
|
+
- Profile-based properties — no env checks in code (Ch 3, 8)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Before: Spring Boot in Action
|
|
2
|
+
|
|
3
|
+
A book library REST API with common Spring Boot anti-patterns — manual configuration fighting auto-config, field injection, hardcoded values, missing tests, and no Actuator.
|
|
4
|
+
|
|
5
|
+
```java
|
|
6
|
+
// Main class missing @SpringBootApplication — won't auto-configure anything
|
|
7
|
+
@Configuration
|
|
8
|
+
@ComponentScan
|
|
9
|
+
public class LibraryApp {
|
|
10
|
+
public static void main(String[] args) {
|
|
11
|
+
SpringApplication.run(LibraryApp.class, args);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Manual DataSource bean — fights auto-configuration (Ch 2, 3)
|
|
16
|
+
@Configuration
|
|
17
|
+
public class DatabaseConfig {
|
|
18
|
+
@Bean
|
|
19
|
+
public DataSource dataSource() {
|
|
20
|
+
DriverManagerDataSource ds = new DriverManagerDataSource();
|
|
21
|
+
ds.setDriverClassName("org.postgresql.Driver");
|
|
22
|
+
ds.setUrl("jdbc:postgresql://localhost/library"); // hardcoded (Ch 3)
|
|
23
|
+
ds.setUsername("admin"); // hardcoded credential!
|
|
24
|
+
ds.setPassword("secret123"); // hardcoded credential!
|
|
25
|
+
return ds;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Field injection — untestable without Spring context (Ch 2)
|
|
30
|
+
@RestController
|
|
31
|
+
public class BookController {
|
|
32
|
+
@Autowired
|
|
33
|
+
private BookRepository bookRepository;
|
|
34
|
+
|
|
35
|
+
@Autowired
|
|
36
|
+
private BookService bookService;
|
|
37
|
+
|
|
38
|
+
// Returns null instead of 404 when book not found (Ch 2)
|
|
39
|
+
@GetMapping("/books/{id}")
|
|
40
|
+
public Book getBook(@PathVariable Long id) {
|
|
41
|
+
return bookRepository.findById(id).orElse(null); // null slips to client
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// No status code — always returns 200 even on create (Ch 2)
|
|
45
|
+
@PostMapping("/books")
|
|
46
|
+
@ResponseBody
|
|
47
|
+
public Book createBook(@RequestBody Book book) {
|
|
48
|
+
return bookRepository.save(book);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Service with field injection and no error handling
|
|
53
|
+
@Service
|
|
54
|
+
public class BookService {
|
|
55
|
+
@Autowired
|
|
56
|
+
private BookRepository bookRepository;
|
|
57
|
+
|
|
58
|
+
public List<Book> search(String query) {
|
|
59
|
+
// Environment check in code instead of using profiles (Ch 3)
|
|
60
|
+
if (System.getProperty("env").equals("dev")) {
|
|
61
|
+
System.out.println("Searching for: " + query); // println not logger
|
|
62
|
+
}
|
|
63
|
+
return bookRepository.findAll(); // returns everything, ignores query
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Test that boots full context just to test one controller method (Ch 4)
|
|
68
|
+
@SpringBootTest
|
|
69
|
+
public class BookControllerTest {
|
|
70
|
+
@Autowired
|
|
71
|
+
private BookController controller;
|
|
72
|
+
|
|
73
|
+
@Test
|
|
74
|
+
public void testGetBook() {
|
|
75
|
+
// Direct controller call — no HTTP semantics, no status code testing
|
|
76
|
+
Book result = controller.getBook(1L);
|
|
77
|
+
assertNotNull(result);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// application.properties — missing externalized config
|
|
82
|
+
// (no datasource url, credentials baked into Java code above)
|
|
83
|
+
// spring.jpa.hibernate.ddl-auto=create // destroys data on restart!
|
|
84
|
+
```
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# Spring Boot in Action — Practices Catalog
|
|
2
|
+
|
|
3
|
+
Before/after examples from each chapter group.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Auto-Configuration: Let Boot Do Its Job (Ch 2, 3)
|
|
8
|
+
|
|
9
|
+
**Before — manual DataSource fighting auto-config:**
|
|
10
|
+
```java
|
|
11
|
+
@Configuration
|
|
12
|
+
public class DatabaseConfig {
|
|
13
|
+
@Bean
|
|
14
|
+
public DataSource dataSource() {
|
|
15
|
+
DriverManagerDataSource ds = new DriverManagerDataSource();
|
|
16
|
+
ds.setUrl("jdbc:postgresql://localhost/mydb");
|
|
17
|
+
ds.setUsername("user");
|
|
18
|
+
ds.setPassword("pass");
|
|
19
|
+
return ds;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
**After — delete the class; use properties:**
|
|
24
|
+
```properties
|
|
25
|
+
spring.datasource.url=jdbc:postgresql://localhost/mydb
|
|
26
|
+
spring.datasource.username=${DB_USER}
|
|
27
|
+
spring.datasource.password=${DB_PASS}
|
|
28
|
+
```
|
|
29
|
+
Spring Boot auto-configures a connection pool (HikariCP) from these properties automatically.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Overriding Auto-Configuration Surgically (Ch 3)
|
|
34
|
+
|
|
35
|
+
**When auto-config is not enough — extend, don't replace:**
|
|
36
|
+
```java
|
|
37
|
+
// Wrong: replaces all security auto-config
|
|
38
|
+
@Configuration
|
|
39
|
+
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
40
|
+
@Override
|
|
41
|
+
protected void configure(HttpSecurity http) throws Exception {
|
|
42
|
+
http.authorizeRequests().anyRequest().permitAll(); // disables everything
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Right: override only what differs (Ch 3)
|
|
47
|
+
@Configuration
|
|
48
|
+
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
49
|
+
@Override
|
|
50
|
+
protected void configure(HttpSecurity http) throws Exception {
|
|
51
|
+
http
|
|
52
|
+
.authorizeRequests()
|
|
53
|
+
.antMatchers("/api/public/**").permitAll()
|
|
54
|
+
.anyRequest().authenticated()
|
|
55
|
+
.and()
|
|
56
|
+
.httpBasic();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Externalized Configuration (Ch 3)
|
|
64
|
+
|
|
65
|
+
**Before — hardcoded values:**
|
|
66
|
+
```java
|
|
67
|
+
@Service
|
|
68
|
+
public class MailService {
|
|
69
|
+
private String host = "smtp.gmail.com"; // hardcoded
|
|
70
|
+
private int port = 587; // hardcoded
|
|
71
|
+
private int timeout = 5000; // hardcoded
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
**After — `@ConfigurationProperties` (type-safe, testable):**
|
|
75
|
+
```java
|
|
76
|
+
@ConfigurationProperties(prefix = "app.mail")
|
|
77
|
+
@Component
|
|
78
|
+
public class MailProperties {
|
|
79
|
+
private String host;
|
|
80
|
+
private int port = 587;
|
|
81
|
+
private int timeoutMs = 5000;
|
|
82
|
+
// getters + setters
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
```properties
|
|
86
|
+
# application.properties
|
|
87
|
+
app.mail.host=smtp.gmail.com
|
|
88
|
+
app.mail.port=587
|
|
89
|
+
app.mail.timeout-ms=5000
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Profiles for Environment Differences (Ch 3)
|
|
95
|
+
|
|
96
|
+
**Before — environment check in code:**
|
|
97
|
+
```java
|
|
98
|
+
@Bean
|
|
99
|
+
public DataSource dataSource() {
|
|
100
|
+
if (System.getProperty("env").equals("dev")) {
|
|
101
|
+
return new EmbeddedDatabaseBuilder().setType(H2).build();
|
|
102
|
+
}
|
|
103
|
+
return productionDataSource(); // messy
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
**After — profile-specific properties files:**
|
|
107
|
+
```properties
|
|
108
|
+
# application.properties (defaults)
|
|
109
|
+
spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}/myapp
|
|
110
|
+
|
|
111
|
+
# application-dev.properties (activated by: spring.profiles.active=dev)
|
|
112
|
+
spring.datasource.url=jdbc:h2:mem:myapp
|
|
113
|
+
spring.jpa.hibernate.ddl-auto=create-drop
|
|
114
|
+
logging.level.org.springframework=DEBUG
|
|
115
|
+
|
|
116
|
+
# application-production.properties
|
|
117
|
+
spring.jpa.hibernate.ddl-auto=validate
|
|
118
|
+
logging.level.root=WARN
|
|
119
|
+
```
|
|
120
|
+
```java
|
|
121
|
+
// Profile-specific beans (Ch 3)
|
|
122
|
+
@Bean
|
|
123
|
+
@Profile("dev")
|
|
124
|
+
public DataSource devDataSource() { ... }
|
|
125
|
+
|
|
126
|
+
@Bean
|
|
127
|
+
@Profile("production")
|
|
128
|
+
public DataSource prodDataSource() { ... }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Constructor Injection (Ch 2)
|
|
134
|
+
|
|
135
|
+
**Before — field injection, untestable:**
|
|
136
|
+
```java
|
|
137
|
+
@Service
|
|
138
|
+
public class OrderService {
|
|
139
|
+
@Autowired
|
|
140
|
+
private OrderRepository repo; // can't test without Spring context
|
|
141
|
+
|
|
142
|
+
@Autowired
|
|
143
|
+
private PaymentGateway gateway;
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
**After — constructor injection:**
|
|
147
|
+
```java
|
|
148
|
+
@Service
|
|
149
|
+
public class OrderService {
|
|
150
|
+
private final OrderRepository repo;
|
|
151
|
+
private final PaymentGateway gateway;
|
|
152
|
+
|
|
153
|
+
public OrderService(OrderRepository repo, PaymentGateway gateway) {
|
|
154
|
+
this.repo = repo;
|
|
155
|
+
this.gateway = gateway;
|
|
156
|
+
}
|
|
157
|
+
// Now testable: new OrderService(mockRepo, mockGateway)
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## REST Controllers: Status Codes & ResponseEntity (Ch 2)
|
|
164
|
+
|
|
165
|
+
**Before — always 200, null on missing:**
|
|
166
|
+
```java
|
|
167
|
+
@GetMapping("/users/{id}")
|
|
168
|
+
public User getUser(@PathVariable Long id) {
|
|
169
|
+
return repo.findById(id).orElse(null); // 200 with null body
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@PostMapping("/users")
|
|
173
|
+
public User createUser(@RequestBody User user) {
|
|
174
|
+
return repo.save(user); // 200, should be 201
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
**After — correct HTTP semantics:**
|
|
178
|
+
```java
|
|
179
|
+
@GetMapping("/users/{id}")
|
|
180
|
+
public ResponseEntity<User> getUser(@PathVariable Long id) {
|
|
181
|
+
return repo.findById(id)
|
|
182
|
+
.map(ResponseEntity::ok)
|
|
183
|
+
.orElse(ResponseEntity.notFound().build()); // 404
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@PostMapping("/users")
|
|
187
|
+
public ResponseEntity<User> createUser(@RequestBody User user) {
|
|
188
|
+
User saved = repo.save(user);
|
|
189
|
+
URI location = URI.create("/users/" + saved.getId());
|
|
190
|
+
return ResponseEntity.created(location).body(saved); // 201 + Location
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Testing: Match Slice to Layer (Ch 4)
|
|
197
|
+
|
|
198
|
+
**Before — full context for every test (slow):**
|
|
199
|
+
```java
|
|
200
|
+
@SpringBootTest // loads ALL beans — overkill for a controller test
|
|
201
|
+
public class ProductControllerTest {
|
|
202
|
+
@Autowired
|
|
203
|
+
private ProductController controller;
|
|
204
|
+
|
|
205
|
+
@Test
|
|
206
|
+
public void testGet() {
|
|
207
|
+
// No HTTP semantics, no status code, no content-type testing
|
|
208
|
+
assertNotNull(controller.getProduct(1L));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
**After — slice tests by layer:**
|
|
213
|
+
```java
|
|
214
|
+
// Controller slice — only web layer, MockMvc, no DB (Ch 4)
|
|
215
|
+
@WebMvcTest(ProductController.class)
|
|
216
|
+
public class ProductControllerTest {
|
|
217
|
+
@Autowired
|
|
218
|
+
private MockMvc mockMvc;
|
|
219
|
+
|
|
220
|
+
@MockBean
|
|
221
|
+
private ProductService service;
|
|
222
|
+
|
|
223
|
+
@Test
|
|
224
|
+
void getProduct_found() throws Exception {
|
|
225
|
+
given(service.findById(1L)).willReturn(new Product(1L, "Widget", 9.99));
|
|
226
|
+
|
|
227
|
+
mockMvc.perform(get("/api/products/1").accept(MediaType.APPLICATION_JSON))
|
|
228
|
+
.andExpect(status().isOk())
|
|
229
|
+
.andExpect(jsonPath("$.name").value("Widget"))
|
|
230
|
+
.andExpect(jsonPath("$.price").value(9.99));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@Test
|
|
234
|
+
void getProduct_notFound() throws Exception {
|
|
235
|
+
given(service.findById(99L))
|
|
236
|
+
.willThrow(new ResponseStatusException(HttpStatus.NOT_FOUND));
|
|
237
|
+
|
|
238
|
+
mockMvc.perform(get("/api/products/99"))
|
|
239
|
+
.andExpect(status().isNotFound());
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Repository slice — real DB (H2 in-memory), no web layer (Ch 4)
|
|
244
|
+
@DataJpaTest
|
|
245
|
+
public class ProductRepositoryTest {
|
|
246
|
+
@Autowired
|
|
247
|
+
private ProductRepository repo;
|
|
248
|
+
|
|
249
|
+
@Test
|
|
250
|
+
void findByName_returnsMatches() {
|
|
251
|
+
repo.save(new Product(null, "Blue Widget", 9.99));
|
|
252
|
+
List<Product> found = repo.findByNameContaining("Widget");
|
|
253
|
+
assertThat(found).hasSize(1);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Full stack test — only when testing HTTP ↔ DB integration (Ch 4)
|
|
258
|
+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
259
|
+
public class ProductIntegrationTest {
|
|
260
|
+
@Autowired
|
|
261
|
+
private TestRestTemplate restTemplate;
|
|
262
|
+
|
|
263
|
+
@Test
|
|
264
|
+
void createAndFetch() {
|
|
265
|
+
ResponseEntity<Product> created = restTemplate
|
|
266
|
+
.postForEntity("/api/products", new Product(null, "Gadget", 19.99), Product.class);
|
|
267
|
+
assertThat(created.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Testing Security (Ch 4)
|
|
275
|
+
|
|
276
|
+
```java
|
|
277
|
+
@WebMvcTest(AdminController.class)
|
|
278
|
+
public class AdminControllerTest {
|
|
279
|
+
|
|
280
|
+
@Autowired
|
|
281
|
+
private MockMvc mockMvc;
|
|
282
|
+
|
|
283
|
+
@Test
|
|
284
|
+
void adminEndpoint_rejectsAnonymous() throws Exception {
|
|
285
|
+
mockMvc.perform(get("/admin/dashboard"))
|
|
286
|
+
.andExpect(status().isUnauthorized()); // or 403 depending on config
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@Test
|
|
290
|
+
@WithMockUser(roles = "ADMIN")
|
|
291
|
+
void adminEndpoint_allowsAdmin() throws Exception {
|
|
292
|
+
mockMvc.perform(get("/admin/dashboard"))
|
|
293
|
+
.andExpect(status().isOk());
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@Test
|
|
297
|
+
@WithMockUser(roles = "USER")
|
|
298
|
+
void adminEndpoint_rejectsUser() throws Exception {
|
|
299
|
+
mockMvc.perform(get("/admin/dashboard"))
|
|
300
|
+
.andExpect(status().isForbidden());
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Actuator: Health, Metrics, Custom Indicators (Ch 7)
|
|
308
|
+
|
|
309
|
+
```java
|
|
310
|
+
// Custom HealthIndicator (Ch 7)
|
|
311
|
+
@Component
|
|
312
|
+
public class ExternalApiHealthIndicator implements HealthIndicator {
|
|
313
|
+
private final RestTemplate restTemplate;
|
|
314
|
+
|
|
315
|
+
public ExternalApiHealthIndicator(RestTemplate restTemplate) {
|
|
316
|
+
this.restTemplate = restTemplate;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@Override
|
|
320
|
+
public Health health() {
|
|
321
|
+
try {
|
|
322
|
+
ResponseEntity<String> resp = restTemplate.getForEntity(
|
|
323
|
+
"https://api.partner.com/health", String.class);
|
|
324
|
+
if (resp.getStatusCode().is2xxSuccessful()) {
|
|
325
|
+
return Health.up().withDetail("partner-api", "reachable").build();
|
|
326
|
+
}
|
|
327
|
+
return Health.down().withDetail("status", resp.getStatusCode()).build();
|
|
328
|
+
} catch (Exception e) {
|
|
329
|
+
return Health.down(e).build();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Custom counter metric (Ch 7)
|
|
335
|
+
@Service
|
|
336
|
+
public class OrderService {
|
|
337
|
+
private final Counter ordersCreated;
|
|
338
|
+
|
|
339
|
+
public OrderService(MeterRegistry registry) {
|
|
340
|
+
this.ordersCreated = Counter.builder("orders.created")
|
|
341
|
+
.description("Total orders placed")
|
|
342
|
+
.register(registry);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
public Order place(Order order) {
|
|
346
|
+
Order saved = repo.save(order);
|
|
347
|
+
ordersCreated.increment(); // visible in /actuator/metrics
|
|
348
|
+
return saved;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```properties
|
|
354
|
+
# Secure Actuator — expose only what's needed (Ch 7)
|
|
355
|
+
management.endpoints.web.exposure.include=health,info,metrics
|
|
356
|
+
management.endpoint.health.show-details=when-authorized
|
|
357
|
+
management.endpoint.shutdown.enabled=false
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Deployment: JAR, Profiles, Flyway (Ch 8)
|
|
363
|
+
|
|
364
|
+
```xml
|
|
365
|
+
<!-- pom.xml: flyway for production migrations (Ch 8) -->
|
|
366
|
+
<dependency>
|
|
367
|
+
<groupId>org.flywaydb</groupId>
|
|
368
|
+
<artifactId>flyway-core</artifactId>
|
|
369
|
+
</dependency>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```sql
|
|
373
|
+
-- src/main/resources/db/migration/V1__create_books.sql
|
|
374
|
+
CREATE TABLE book (
|
|
375
|
+
id BIGSERIAL PRIMARY KEY,
|
|
376
|
+
title VARCHAR(255) NOT NULL,
|
|
377
|
+
isbn VARCHAR(20) UNIQUE NOT NULL
|
|
378
|
+
);
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
```properties
|
|
382
|
+
# application-production.properties (Ch 8)
|
|
383
|
+
# Never use create or create-drop in production
|
|
384
|
+
spring.jpa.hibernate.ddl-auto=validate
|
|
385
|
+
spring.flyway.enabled=true
|
|
386
|
+
|
|
387
|
+
# Disable dev tools
|
|
388
|
+
spring.devtools.restart.enabled=false
|
|
389
|
+
|
|
390
|
+
# Tighten logging
|
|
391
|
+
logging.level.root=WARN
|
|
392
|
+
logging.level.com.example=INFO
|
|
393
|
+
|
|
394
|
+
# Actuator: health only
|
|
395
|
+
management.endpoints.web.exposure.include=health
|
|
396
|
+
management.endpoint.health.show-details=never
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
# Build and run (Ch 8)
|
|
401
|
+
mvn clean package -DskipTests
|
|
402
|
+
java -jar target/library-1.0.0.jar --spring.profiles.active=production
|
|
403
|
+
```
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
review.py — Pre-analysis script for Spring Boot in Action reviews.
|
|
4
|
+
Usage: python review.py <file.java|application.properties|application.yml>
|
|
5
|
+
|
|
6
|
+
Scans Spring Boot source files for anti-patterns from the book:
|
|
7
|
+
field injection, hardcoded credentials, missing ResponseEntity, auto-config
|
|
8
|
+
fighting, wrong test annotations, missing Actuator security, and ddl-auto=create.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
JAVA_CHECKS = [
|
|
17
|
+
(
|
|
18
|
+
r"@Autowired\s*\n\s*(private|protected)",
|
|
19
|
+
"Ch 2: Field injection (@Autowired on field)",
|
|
20
|
+
"use constructor injection — fields with @Autowired are not testable without Spring context",
|
|
21
|
+
),
|
|
22
|
+
(
|
|
23
|
+
r"DriverManagerDataSource|BasicDataSource|HikariDataSource",
|
|
24
|
+
"Ch 2/3: Manual DataSource bean",
|
|
25
|
+
"delete this bean and set spring.datasource.* in application.properties — Boot auto-configures HikariCP",
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
r"orElse\(null\)",
|
|
29
|
+
"Ch 2: orElse(null) returns null to client",
|
|
30
|
+
"use .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)) or .map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build())",
|
|
31
|
+
),
|
|
32
|
+
(
|
|
33
|
+
r'setPassword\s*\(\s*"[^"$][^"]*"',
|
|
34
|
+
"Ch 3: Hardcoded password",
|
|
35
|
+
"externalize to environment variable: spring.datasource.password=${DB_PASS}",
|
|
36
|
+
),
|
|
37
|
+
(
|
|
38
|
+
r'setUsername\s*\(\s*"[^"$][^"]*"',
|
|
39
|
+
"Ch 3: Hardcoded username",
|
|
40
|
+
"externalize to environment variable: spring.datasource.username=${DB_USER}",
|
|
41
|
+
),
|
|
42
|
+
(
|
|
43
|
+
r"new ObjectMapper\(\)",
|
|
44
|
+
"Ch 2: Manual ObjectMapper construction",
|
|
45
|
+
"Spring Boot auto-configures Jackson — inject ObjectMapper bean or configure via spring.jackson.* properties",
|
|
46
|
+
),
|
|
47
|
+
(
|
|
48
|
+
r"@SpringBootTest\b(?!.*WebEnvironment)",
|
|
49
|
+
"Ch 4: @SpringBootTest without WebEnvironment",
|
|
50
|
+
"for controller tests use @WebMvcTest; for repository tests use @DataJpaTest; full @SpringBootTest only for integration tests",
|
|
51
|
+
),
|
|
52
|
+
(
|
|
53
|
+
r"System\.out\.println",
|
|
54
|
+
"Ch 3: System.out.println instead of logger",
|
|
55
|
+
"use SLF4J: private static final Logger log = LoggerFactory.getLogger(MyClass.class)",
|
|
56
|
+
),
|
|
57
|
+
(
|
|
58
|
+
r'@Value\s*\(\s*"\$\{[^}]+\}"\s*\)(?:[\s\S]{0,200}@Value\s*\(\s*"\$\{){2}',
|
|
59
|
+
"Ch 3: Multiple @Value annotations",
|
|
60
|
+
"group related config values in a @ConfigurationProperties class with a prefix",
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
PROPERTIES_CHECKS = [
|
|
65
|
+
(
|
|
66
|
+
r"ddl-auto\s*=\s*(create|create-drop)(?!\s*#.*test)",
|
|
67
|
+
"Ch 8: ddl-auto=create or create-drop",
|
|
68
|
+
"destroys data on restart — use 'validate' in production; use Flyway/Liquibase for migrations",
|
|
69
|
+
),
|
|
70
|
+
(
|
|
71
|
+
r"management\.endpoints\.web\.exposure\.include\s*=\s*\*",
|
|
72
|
+
"Ch 7: Actuator exposes all endpoints",
|
|
73
|
+
"restrict to: management.endpoints.web.exposure.include=health,info — never expose env, beans, or shutdown publicly",
|
|
74
|
+
),
|
|
75
|
+
(
|
|
76
|
+
r"spring\.security\.user\.password\s*=\s*(?!(\$\{|\s*$))",
|
|
77
|
+
"Ch 3: Hardcoded security password in properties",
|
|
78
|
+
"use environment variable: spring.security.user.password=${ADMIN_PASS}",
|
|
79
|
+
),
|
|
80
|
+
(
|
|
81
|
+
r"datasource\.password\s*=\s*(?!\$\{)(\S+)",
|
|
82
|
+
"Ch 3: Hardcoded datasource password",
|
|
83
|
+
"use environment variable: spring.datasource.password=${DB_PASS}",
|
|
84
|
+
),
|
|
85
|
+
(
|
|
86
|
+
r"datasource\.url\s*=\s*jdbc:[a-z]+://(?!(\$\{|localhost|127\.0\.0\.1))\S+",
|
|
87
|
+
"Ch 3: Hardcoded production datasource URL",
|
|
88
|
+
"use environment variable: spring.datasource.url=${DB_URL}",
|
|
89
|
+
),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def scan_java(source: str) -> list[dict]:
|
|
94
|
+
findings = []
|
|
95
|
+
lines = source.splitlines()
|
|
96
|
+
for lineno, line in enumerate(lines, start=1):
|
|
97
|
+
if line.strip().startswith("//"):
|
|
98
|
+
continue
|
|
99
|
+
for pattern, label, advice in JAVA_CHECKS:
|
|
100
|
+
if re.search(pattern, line):
|
|
101
|
+
findings.append({"line": lineno, "text": line.rstrip(), "label": label, "advice": advice})
|
|
102
|
+
return findings
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def scan_properties(source: str) -> list[dict]:
|
|
106
|
+
findings = []
|
|
107
|
+
lines = source.splitlines()
|
|
108
|
+
for lineno, line in enumerate(lines, start=1):
|
|
109
|
+
if line.strip().startswith("#"):
|
|
110
|
+
continue
|
|
111
|
+
for pattern, label, advice in PROPERTIES_CHECKS:
|
|
112
|
+
if re.search(pattern, line):
|
|
113
|
+
findings.append({"line": lineno, "text": line.rstrip(), "label": label, "advice": advice})
|
|
114
|
+
return findings
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def sep(char="-", width=70) -> str:
|
|
118
|
+
return char * width
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main() -> None:
|
|
122
|
+
if len(sys.argv) < 2:
|
|
123
|
+
print("Usage: python review.py <file.java|application.properties|application.yml>")
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
|
|
126
|
+
path = Path(sys.argv[1])
|
|
127
|
+
if not path.exists():
|
|
128
|
+
print(f"Error: file not found: {path}")
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
|
|
131
|
+
source = path.read_text(encoding="utf-8", errors="replace")
|
|
132
|
+
|
|
133
|
+
if path.suffix == ".java":
|
|
134
|
+
findings = scan_java(source)
|
|
135
|
+
file_type = "Java"
|
|
136
|
+
elif path.suffix in (".properties", ".yml", ".yaml"):
|
|
137
|
+
findings = scan_properties(source)
|
|
138
|
+
file_type = "Properties/YAML"
|
|
139
|
+
else:
|
|
140
|
+
print(f"Warning: unsupported extension '{path.suffix}' — scanning as Java")
|
|
141
|
+
findings = scan_java(source)
|
|
142
|
+
file_type = "Unknown"
|
|
143
|
+
|
|
144
|
+
groups: dict[str, list] = {}
|
|
145
|
+
for f in findings:
|
|
146
|
+
groups.setdefault(f["label"], []).append(f)
|
|
147
|
+
|
|
148
|
+
print(sep("="))
|
|
149
|
+
print("SPRING BOOT IN ACTION — PRE-REVIEW REPORT")
|
|
150
|
+
print(sep("="))
|
|
151
|
+
print(f"File : {path} ({file_type})")
|
|
152
|
+
print(f"Lines : {len(source.splitlines())}")
|
|
153
|
+
print(f"Issues : {len(findings)} potential anti-patterns across {len(groups)} categories")
|
|
154
|
+
print()
|
|
155
|
+
|
|
156
|
+
if not findings:
|
|
157
|
+
print(" [OK] No common Spring Boot anti-patterns detected.")
|
|
158
|
+
print()
|
|
159
|
+
else:
|
|
160
|
+
for label, items in groups.items():
|
|
161
|
+
print(sep())
|
|
162
|
+
print(f" {label} ({len(items)} occurrence{'s' if len(items) != 1 else ''})")
|
|
163
|
+
print(sep())
|
|
164
|
+
print(f" Advice: {items[0]['advice']}")
|
|
165
|
+
print()
|
|
166
|
+
for item in items[:5]:
|
|
167
|
+
print(f" line {item['line']:>4}: {item['text'][:100]}")
|
|
168
|
+
if len(items) > 5:
|
|
169
|
+
print(f" ... and {len(items) - 5} more")
|
|
170
|
+
print()
|
|
171
|
+
|
|
172
|
+
severity = (
|
|
173
|
+
"HIGH" if len(findings) >= 5
|
|
174
|
+
else "MEDIUM" if len(findings) >= 2
|
|
175
|
+
else "LOW" if findings
|
|
176
|
+
else "NONE"
|
|
177
|
+
)
|
|
178
|
+
print(sep("="))
|
|
179
|
+
print(f"SEVERITY: {severity} | Key chapters: Ch 2 (injection/REST), Ch 3 (config/profiles), Ch 4 (testing), Ch 7 (Actuator), Ch 8 (deployment)")
|
|
180
|
+
print(sep("="))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
main()
|