@booklib/skills 1.4.0 → 1.5.1

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.
@@ -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()