@booklib/core 2.0.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/.cursor/rules/booklib-standards.mdc +40 -0
- package/.gemini/context.md +372 -0
- package/AGENTS.md +166 -0
- package/CHANGELOG.md +226 -0
- package/CLAUDE.md +81 -0
- package/CODE_OF_CONDUCT.md +31 -0
- package/CONTRIBUTING.md +304 -0
- package/LICENSE +21 -0
- package/PLAN.md +28 -0
- package/README.ja.md +198 -0
- package/README.ko.md +198 -0
- package/README.md +503 -0
- package/README.pt-BR.md +198 -0
- package/README.uk.md +241 -0
- package/README.zh-CN.md +198 -0
- package/SECURITY.md +9 -0
- package/agents/architecture-reviewer.md +136 -0
- package/agents/booklib-reviewer.md +90 -0
- package/agents/data-reviewer.md +107 -0
- package/agents/jvm-reviewer.md +146 -0
- package/agents/python-reviewer.md +128 -0
- package/agents/rust-reviewer.md +115 -0
- package/agents/ts-reviewer.md +110 -0
- package/agents/ui-reviewer.md +117 -0
- package/assets/logo.svg +36 -0
- package/bin/booklib-mcp.js +304 -0
- package/bin/booklib.js +1705 -0
- package/bin/skills.cjs +1292 -0
- package/booklib-router.mdc +36 -0
- package/booklib.config.json +19 -0
- package/commands/animation-at-work.md +10 -0
- package/commands/clean-code-reviewer.md +10 -0
- package/commands/data-intensive-patterns.md +10 -0
- package/commands/data-pipelines.md +10 -0
- package/commands/design-patterns.md +10 -0
- package/commands/domain-driven-design.md +10 -0
- package/commands/effective-java.md +10 -0
- package/commands/effective-kotlin.md +10 -0
- package/commands/effective-python.md +10 -0
- package/commands/effective-typescript.md +10 -0
- package/commands/kotlin-in-action.md +10 -0
- package/commands/lean-startup.md +10 -0
- package/commands/microservices-patterns.md +10 -0
- package/commands/programming-with-rust.md +10 -0
- package/commands/refactoring-ui.md +10 -0
- package/commands/rust-in-action.md +10 -0
- package/commands/skill-router.md +10 -0
- package/commands/spring-boot-in-action.md +10 -0
- package/commands/storytelling-with-data.md +10 -0
- package/commands/system-design-interview.md +10 -0
- package/commands/using-asyncio-python.md +10 -0
- package/commands/web-scraping-python.md +10 -0
- package/community/registry.json +1616 -0
- package/hooks/hooks.json +23 -0
- package/hooks/posttooluse-capture.mjs +67 -0
- package/hooks/suggest.js +153 -0
- package/lib/agent-behaviors.js +40 -0
- package/lib/agent-detector.js +96 -0
- package/lib/config-loader.js +39 -0
- package/lib/conflict-resolver.js +148 -0
- package/lib/context-builder.js +574 -0
- package/lib/discovery-engine.js +298 -0
- package/lib/doctor/hook-installer.js +83 -0
- package/lib/doctor/usage-tracker.js +87 -0
- package/lib/engine/ai-features.js +253 -0
- package/lib/engine/auditor.js +103 -0
- package/lib/engine/bm25-index.js +178 -0
- package/lib/engine/capture.js +120 -0
- package/lib/engine/corrections.js +198 -0
- package/lib/engine/doctor.js +195 -0
- package/lib/engine/graph-injector.js +137 -0
- package/lib/engine/graph.js +161 -0
- package/lib/engine/handoff.js +405 -0
- package/lib/engine/indexer.js +242 -0
- package/lib/engine/parser.js +53 -0
- package/lib/engine/query-expander.js +42 -0
- package/lib/engine/reranker.js +40 -0
- package/lib/engine/rrf.js +59 -0
- package/lib/engine/scanner.js +151 -0
- package/lib/engine/searcher.js +139 -0
- package/lib/engine/session-coordinator.js +306 -0
- package/lib/engine/session-manager.js +429 -0
- package/lib/engine/synthesizer.js +70 -0
- package/lib/installer.js +70 -0
- package/lib/instinct-block.js +33 -0
- package/lib/mcp-config-writer.js +88 -0
- package/lib/paths.js +57 -0
- package/lib/profiles/design.md +19 -0
- package/lib/profiles/general.md +16 -0
- package/lib/profiles/research-analysis.md +22 -0
- package/lib/profiles/software-development.md +23 -0
- package/lib/profiles/writing-content.md +19 -0
- package/lib/project-initializer.js +916 -0
- package/lib/registry/skills.js +102 -0
- package/lib/registry-searcher.js +99 -0
- package/lib/rules/rules-manager.js +169 -0
- package/lib/skill-fetcher.js +333 -0
- package/lib/well-known-builder.js +70 -0
- package/lib/wizard/index.js +404 -0
- package/lib/wizard/integration-detector.js +41 -0
- package/lib/wizard/project-detector.js +100 -0
- package/lib/wizard/prompt.js +156 -0
- package/lib/wizard/registry-embeddings.js +107 -0
- package/lib/wizard/skill-recommender.js +69 -0
- package/llms-full.txt +254 -0
- package/llms.txt +70 -0
- package/package.json +45 -0
- package/research-reports/2026-04-01-current-architecture.md +160 -0
- package/research-reports/IDEAS.md +93 -0
- package/rules/common/clean-code.md +42 -0
- package/rules/java/effective-java.md +42 -0
- package/rules/kotlin/effective-kotlin.md +37 -0
- package/rules/python/effective-python.md +38 -0
- package/rules/rust/rust.md +37 -0
- package/rules/typescript/effective-typescript.md +42 -0
- package/scripts/gen-llms-full.mjs +36 -0
- package/scripts/gen-og.mjs +142 -0
- package/scripts/validate-frontmatter.js +25 -0
- package/skills/animation-at-work/SKILL.md +270 -0
- package/skills/animation-at-work/assets/example_asset.txt +1 -0
- package/skills/animation-at-work/evals/evals.json +44 -0
- package/skills/animation-at-work/evals/results.json +13 -0
- package/skills/animation-at-work/examples/after.md +64 -0
- package/skills/animation-at-work/examples/before.md +35 -0
- package/skills/animation-at-work/references/api_reference.md +369 -0
- package/skills/animation-at-work/references/review-checklist.md +79 -0
- package/skills/animation-at-work/scripts/audit_animations.py +295 -0
- package/skills/animation-at-work/scripts/example.py +1 -0
- package/skills/clean-code-reviewer/SKILL.md +444 -0
- package/skills/clean-code-reviewer/audit.json +35 -0
- package/skills/clean-code-reviewer/evals/evals.json +185 -0
- package/skills/clean-code-reviewer/evals/results.json +13 -0
- package/skills/clean-code-reviewer/examples/after.md +48 -0
- package/skills/clean-code-reviewer/examples/before.md +33 -0
- package/skills/clean-code-reviewer/references/api_reference.md +158 -0
- package/skills/clean-code-reviewer/references/practices-catalog.md +282 -0
- package/skills/clean-code-reviewer/references/review-checklist.md +254 -0
- package/skills/clean-code-reviewer/scripts/pre-review.py +206 -0
- package/skills/data-intensive-patterns/SKILL.md +267 -0
- package/skills/data-intensive-patterns/assets/example_asset.txt +1 -0
- package/skills/data-intensive-patterns/evals/evals.json +54 -0
- package/skills/data-intensive-patterns/evals/results.json +13 -0
- package/skills/data-intensive-patterns/examples/after.md +61 -0
- package/skills/data-intensive-patterns/examples/before.md +38 -0
- package/skills/data-intensive-patterns/references/api_reference.md +34 -0
- package/skills/data-intensive-patterns/references/patterns-catalog.md +551 -0
- package/skills/data-intensive-patterns/references/review-checklist.md +193 -0
- package/skills/data-intensive-patterns/scripts/adr.py +213 -0
- package/skills/data-intensive-patterns/scripts/example.py +1 -0
- package/skills/data-pipelines/SKILL.md +259 -0
- package/skills/data-pipelines/assets/example_asset.txt +1 -0
- package/skills/data-pipelines/evals/evals.json +45 -0
- package/skills/data-pipelines/evals/results.json +13 -0
- package/skills/data-pipelines/examples/after.md +97 -0
- package/skills/data-pipelines/examples/before.md +37 -0
- package/skills/data-pipelines/references/api_reference.md +301 -0
- package/skills/data-pipelines/references/review-checklist.md +181 -0
- package/skills/data-pipelines/scripts/example.py +1 -0
- package/skills/data-pipelines/scripts/new_pipeline.py +444 -0
- package/skills/design-patterns/SKILL.md +271 -0
- package/skills/design-patterns/assets/example_asset.txt +1 -0
- package/skills/design-patterns/evals/evals.json +46 -0
- package/skills/design-patterns/evals/results.json +13 -0
- package/skills/design-patterns/examples/after.md +52 -0
- package/skills/design-patterns/examples/before.md +29 -0
- package/skills/design-patterns/references/api_reference.md +1 -0
- package/skills/design-patterns/references/patterns-catalog.md +726 -0
- package/skills/design-patterns/references/review-checklist.md +173 -0
- package/skills/design-patterns/scripts/example.py +1 -0
- package/skills/design-patterns/scripts/scaffold.py +807 -0
- package/skills/domain-driven-design/SKILL.md +142 -0
- package/skills/domain-driven-design/assets/example_asset.txt +1 -0
- package/skills/domain-driven-design/evals/evals.json +48 -0
- package/skills/domain-driven-design/evals/results.json +13 -0
- package/skills/domain-driven-design/examples/after.md +80 -0
- package/skills/domain-driven-design/examples/before.md +43 -0
- package/skills/domain-driven-design/references/api_reference.md +1 -0
- package/skills/domain-driven-design/references/patterns-catalog.md +545 -0
- package/skills/domain-driven-design/references/review-checklist.md +158 -0
- package/skills/domain-driven-design/scripts/example.py +1 -0
- package/skills/domain-driven-design/scripts/scaffold.py +421 -0
- package/skills/effective-java/SKILL.md +227 -0
- package/skills/effective-java/assets/example_asset.txt +1 -0
- package/skills/effective-java/evals/evals.json +46 -0
- package/skills/effective-java/evals/results.json +13 -0
- package/skills/effective-java/examples/after.md +83 -0
- package/skills/effective-java/examples/before.md +37 -0
- package/skills/effective-java/references/api_reference.md +1 -0
- package/skills/effective-java/references/items-catalog.md +955 -0
- package/skills/effective-java/references/review-checklist.md +216 -0
- package/skills/effective-java/scripts/checkstyle_setup.py +211 -0
- package/skills/effective-java/scripts/example.py +1 -0
- package/skills/effective-kotlin/SKILL.md +271 -0
- package/skills/effective-kotlin/assets/example_asset.txt +1 -0
- package/skills/effective-kotlin/audit.json +29 -0
- package/skills/effective-kotlin/evals/evals.json +45 -0
- package/skills/effective-kotlin/evals/results.json +13 -0
- package/skills/effective-kotlin/examples/after.md +36 -0
- package/skills/effective-kotlin/examples/before.md +38 -0
- package/skills/effective-kotlin/references/api_reference.md +1 -0
- package/skills/effective-kotlin/references/practices-catalog.md +1228 -0
- package/skills/effective-kotlin/references/review-checklist.md +126 -0
- package/skills/effective-kotlin/scripts/example.py +1 -0
- package/skills/effective-python/SKILL.md +441 -0
- package/skills/effective-python/evals/evals.json +44 -0
- package/skills/effective-python/evals/results.json +13 -0
- package/skills/effective-python/examples/after.md +56 -0
- package/skills/effective-python/examples/before.md +40 -0
- package/skills/effective-python/ref-01-pythonic-thinking.md +202 -0
- package/skills/effective-python/ref-02-lists-and-dicts.md +146 -0
- package/skills/effective-python/ref-03-functions.md +186 -0
- package/skills/effective-python/ref-04-comprehensions-generators.md +211 -0
- package/skills/effective-python/ref-05-classes-interfaces.md +188 -0
- package/skills/effective-python/ref-06-metaclasses-attributes.md +209 -0
- package/skills/effective-python/ref-07-concurrency.md +213 -0
- package/skills/effective-python/ref-08-robustness-performance.md +248 -0
- package/skills/effective-python/ref-09-testing-debugging.md +253 -0
- package/skills/effective-python/ref-10-collaboration.md +175 -0
- package/skills/effective-python/references/api_reference.md +218 -0
- package/skills/effective-python/references/practices-catalog.md +483 -0
- package/skills/effective-python/references/review-checklist.md +190 -0
- package/skills/effective-python/scripts/lint.py +173 -0
- package/skills/effective-typescript/SKILL.md +262 -0
- package/skills/effective-typescript/audit.json +29 -0
- package/skills/effective-typescript/evals/evals.json +37 -0
- package/skills/effective-typescript/evals/results.json +13 -0
- package/skills/effective-typescript/examples/after.md +70 -0
- package/skills/effective-typescript/examples/before.md +47 -0
- package/skills/effective-typescript/references/api_reference.md +118 -0
- package/skills/effective-typescript/references/practices-catalog.md +371 -0
- package/skills/effective-typescript/scripts/review.py +169 -0
- package/skills/kotlin-in-action/SKILL.md +261 -0
- package/skills/kotlin-in-action/assets/example_asset.txt +1 -0
- package/skills/kotlin-in-action/evals/evals.json +43 -0
- package/skills/kotlin-in-action/evals/results.json +13 -0
- package/skills/kotlin-in-action/examples/after.md +53 -0
- package/skills/kotlin-in-action/examples/before.md +39 -0
- package/skills/kotlin-in-action/references/api_reference.md +1 -0
- package/skills/kotlin-in-action/references/practices-catalog.md +436 -0
- package/skills/kotlin-in-action/references/review-checklist.md +204 -0
- package/skills/kotlin-in-action/scripts/example.py +1 -0
- package/skills/kotlin-in-action/scripts/setup_detekt.py +224 -0
- package/skills/lean-startup/SKILL.md +160 -0
- package/skills/lean-startup/assets/example_asset.txt +1 -0
- package/skills/lean-startup/evals/evals.json +43 -0
- package/skills/lean-startup/evals/results.json +13 -0
- package/skills/lean-startup/examples/after.md +80 -0
- package/skills/lean-startup/examples/before.md +34 -0
- package/skills/lean-startup/references/api_reference.md +319 -0
- package/skills/lean-startup/references/review-checklist.md +137 -0
- package/skills/lean-startup/scripts/example.py +1 -0
- package/skills/lean-startup/scripts/new_experiment.py +286 -0
- package/skills/microservices-patterns/SKILL.md +384 -0
- package/skills/microservices-patterns/evals/evals.json +45 -0
- package/skills/microservices-patterns/evals/results.json +13 -0
- package/skills/microservices-patterns/examples/after.md +69 -0
- package/skills/microservices-patterns/examples/before.md +40 -0
- package/skills/microservices-patterns/references/patterns-catalog.md +391 -0
- package/skills/microservices-patterns/references/review-checklist.md +169 -0
- package/skills/microservices-patterns/scripts/new_service.py +583 -0
- package/skills/programming-with-rust/SKILL.md +209 -0
- package/skills/programming-with-rust/evals/evals.json +37 -0
- package/skills/programming-with-rust/evals/results.json +13 -0
- package/skills/programming-with-rust/examples/after.md +107 -0
- package/skills/programming-with-rust/examples/before.md +59 -0
- package/skills/programming-with-rust/references/api_reference.md +152 -0
- package/skills/programming-with-rust/references/practices-catalog.md +335 -0
- package/skills/programming-with-rust/scripts/review.py +142 -0
- package/skills/refactoring-ui/SKILL.md +362 -0
- package/skills/refactoring-ui/assets/example_asset.txt +1 -0
- package/skills/refactoring-ui/evals/evals.json +45 -0
- package/skills/refactoring-ui/evals/results.json +13 -0
- package/skills/refactoring-ui/examples/after.md +85 -0
- package/skills/refactoring-ui/examples/before.md +58 -0
- package/skills/refactoring-ui/references/api_reference.md +355 -0
- package/skills/refactoring-ui/references/review-checklist.md +114 -0
- package/skills/refactoring-ui/scripts/audit_css.py +250 -0
- package/skills/refactoring-ui/scripts/example.py +1 -0
- package/skills/rust-in-action/SKILL.md +350 -0
- package/skills/rust-in-action/evals/evals.json +38 -0
- package/skills/rust-in-action/evals/results.json +13 -0
- package/skills/rust-in-action/examples/after.md +156 -0
- package/skills/rust-in-action/examples/before.md +56 -0
- package/skills/rust-in-action/references/practices-catalog.md +346 -0
- package/skills/rust-in-action/scripts/review.py +147 -0
- package/skills/skill-router/SKILL.md +186 -0
- package/skills/skill-router/evals/evals.json +38 -0
- package/skills/skill-router/evals/results.json +13 -0
- package/skills/skill-router/examples/after.md +63 -0
- package/skills/skill-router/examples/before.md +39 -0
- package/skills/skill-router/references/api_reference.md +24 -0
- package/skills/skill-router/references/routing-heuristics.md +89 -0
- package/skills/skill-router/references/skill-catalog.md +174 -0
- package/skills/skill-router/scripts/route.py +266 -0
- package/skills/spring-boot-in-action/SKILL.md +340 -0
- package/skills/spring-boot-in-action/evals/evals.json +39 -0
- package/skills/spring-boot-in-action/evals/results.json +13 -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/skills/storytelling-with-data/SKILL.md +241 -0
- package/skills/storytelling-with-data/assets/example_asset.txt +1 -0
- package/skills/storytelling-with-data/evals/evals.json +47 -0
- package/skills/storytelling-with-data/evals/results.json +13 -0
- package/skills/storytelling-with-data/examples/after.md +50 -0
- package/skills/storytelling-with-data/examples/before.md +33 -0
- package/skills/storytelling-with-data/references/api_reference.md +379 -0
- package/skills/storytelling-with-data/references/review-checklist.md +111 -0
- package/skills/storytelling-with-data/scripts/chart_review.py +301 -0
- package/skills/storytelling-with-data/scripts/example.py +1 -0
- package/skills/system-design-interview/SKILL.md +233 -0
- package/skills/system-design-interview/assets/example_asset.txt +1 -0
- package/skills/system-design-interview/evals/evals.json +46 -0
- package/skills/system-design-interview/evals/results.json +13 -0
- package/skills/system-design-interview/examples/after.md +94 -0
- package/skills/system-design-interview/examples/before.md +27 -0
- package/skills/system-design-interview/references/api_reference.md +582 -0
- package/skills/system-design-interview/references/review-checklist.md +201 -0
- package/skills/system-design-interview/scripts/example.py +1 -0
- package/skills/system-design-interview/scripts/new_design.py +421 -0
- package/skills/using-asyncio-python/SKILL.md +290 -0
- package/skills/using-asyncio-python/assets/example_asset.txt +1 -0
- package/skills/using-asyncio-python/evals/evals.json +43 -0
- package/skills/using-asyncio-python/evals/results.json +13 -0
- package/skills/using-asyncio-python/examples/after.md +68 -0
- package/skills/using-asyncio-python/examples/before.md +39 -0
- package/skills/using-asyncio-python/references/api_reference.md +267 -0
- package/skills/using-asyncio-python/references/review-checklist.md +149 -0
- package/skills/using-asyncio-python/scripts/check_blocking.py +270 -0
- package/skills/using-asyncio-python/scripts/example.py +1 -0
- package/skills/web-scraping-python/SKILL.md +280 -0
- package/skills/web-scraping-python/assets/example_asset.txt +1 -0
- package/skills/web-scraping-python/evals/evals.json +46 -0
- package/skills/web-scraping-python/evals/results.json +13 -0
- package/skills/web-scraping-python/examples/after.md +109 -0
- package/skills/web-scraping-python/examples/before.md +40 -0
- package/skills/web-scraping-python/references/api_reference.md +393 -0
- package/skills/web-scraping-python/references/review-checklist.md +163 -0
- package/skills/web-scraping-python/scripts/example.py +1 -0
- package/skills/web-scraping-python/scripts/new_scraper.py +231 -0
- package/skills/writing-plans/audit.json +34 -0
- package/tests/agent-detector.test.js +83 -0
- package/tests/corrections.test.js +245 -0
- package/tests/doctor/hook-installer.test.js +72 -0
- package/tests/doctor/usage-tracker.test.js +140 -0
- package/tests/engine/benchmark-eval.test.js +31 -0
- package/tests/engine/bm25-index.test.js +85 -0
- package/tests/engine/capture-command.test.js +35 -0
- package/tests/engine/capture.test.js +17 -0
- package/tests/engine/graph-augmented-search.test.js +107 -0
- package/tests/engine/graph-injector.test.js +44 -0
- package/tests/engine/graph.test.js +216 -0
- package/tests/engine/hybrid-searcher.test.js +74 -0
- package/tests/engine/indexer-bm25.test.js +37 -0
- package/tests/engine/mcp-tools.test.js +73 -0
- package/tests/engine/project-initializer-mcp.test.js +99 -0
- package/tests/engine/query-expander.test.js +36 -0
- package/tests/engine/reranker.test.js +51 -0
- package/tests/engine/rrf.test.js +49 -0
- package/tests/engine/srag-prefix.test.js +47 -0
- package/tests/instinct-block.test.js +23 -0
- package/tests/mcp-config-writer.test.js +60 -0
- package/tests/project-initializer-new-agents.test.js +48 -0
- package/tests/rules/rules-manager.test.js +230 -0
- package/tests/well-known-builder.test.js +40 -0
- package/tests/wizard/integration-detector.test.js +31 -0
- package/tests/wizard/project-detector.test.js +51 -0
- package/tests/wizard/prompt-session.test.js +61 -0
- package/tests/wizard/prompt.test.js +16 -0
- package/tests/wizard/registry-embeddings.test.js +35 -0
- package/tests/wizard/skill-recommender.test.js +34 -0
- package/tests/wizard/slot-count.test.js +25 -0
- package/vercel.json +21 -0
|
@@ -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
|
+
```
|