spring-boot4-skill 1.1.0 → 1.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.2.0] - 2026-02-21
9
+
10
+ ### Added
11
+
12
+ - **Docs:** CHANGELOG.md (Keep a Changelog); README "What you get" example tree, expanded Contributing, npm version badge.
13
+ - **CLI:** Project `description` in wizard and in generated Application.java, build.gradle.kts, pom.xml; friendly error messages (EEXIST, EACCES); "Minimal (API only)" option in wizard.
14
+ - **Skill:** New reference `spring-messaging.md` (Kafka, @KafkaListener, producer/consumer); SKILL.md Quick decision node for Messaging/Kafka; Health groups (readiness/liveness) in spring-boot-4.md; BOM sync note in build-templates.md.
15
+ - **Quality:** Jest config and unit tests for templates and buildContext; `scripts/smoke-cli.sh` and `npm run smoke`; `buildContext` exported for tests.
16
+
17
+ ### Changed
18
+
19
+ - **CLI:** Generator context includes `description` default; success message already showed absolute path.
20
+
21
+ [1.2.0]: https://github.com/AyrtonAldayr/agent-skill-java-spring-framework/compare/v1.1.0...v1.2.0
22
+
23
+ ## [1.1.0] - 2026-02-21
24
+
25
+ ### Added
26
+
27
+ - **Skill:** New reference `spring-security-7.md` — OAuth2 Resource Server, JWT, method security (`@PreAuthorize`), CORS.
28
+ - **Skill:** New reference `troubleshooting-migration.md` — common errors (javax/jakarta, RestTemplate, JSpecify, native), Boot 3→4 migration checklist.
29
+ - **Skill:** "When NOT to use this skill" section and Quick decision (mermaid) in SKILL.md.
30
+ - **Skill:** Spring Boot 4 reference: Testcontainers subsection, secure Actuator exposure, Reactive stack (R2DBC + WebFlux), redirect to spring-security-7.md for OAuth2/JWT.
31
+ - **Skill:** Spring Modulith reference: "Common pitfalls" subsection.
32
+ - **Skill:** Spring Framework 7: note for reactive apps (WebFlux + R2DBC) with link to spring-boot-4.md.
33
+ - **README:** Install options for skill via `npx skills add` and `claude skills install`; skill reference table updated.
34
+
35
+ ### Changed
36
+
37
+ - **Skill:** Reference table in SKILL.md now includes Spring Security 7 and Troubleshooting & migration; triggers listed in body.
38
+
39
+ ## [1.0.0] - 2026-02-21
40
+
41
+ ### Added
42
+
43
+ - **CLI:** Interactive wizard for project name, package, build tool (Gradle KTS / Maven), Java version (25/21/17), Spring Boot version, database (PostgreSQL, MySQL, MongoDB, H2, None), features (Actuator, Security, Validation, Modulith, Native, WebFlux, Docker Compose), and Java preview features.
44
+ - **CLI:** Non-interactive mode (`--no-interactive`) and Maven option (`--maven`).
45
+ - **Templates:** Gradle Kotlin DSL and Maven POM with Spring Boot 4.0.3 BOM; Application.java with JSpecify `@NullMarked`, application.yaml with virtual threads and OTEL, ApplicationTests, optional compose.yaml and Modulith skeleton.
46
+ - **Skill:** Core SKILL.md with mandatory workflow (Analyze → Implement → Optimize → Document), Core Principles table, and reference files (spring-framework-7, spring-boot-4, spring-modulith, build-templates).
47
+ - **npm:** Package `spring-boot4-skill` publishable with `npx spring-boot4-skill`.
48
+
49
+ [1.1.0]: https://github.com/AyrtonAldayr/agent-skill-java-spring-framework/compare/v1.0.0...v1.1.0
50
+ [1.0.0]: https://github.com/AyrtonAldayr/agent-skill-java-spring-framework/releases/tag/v1.0.0
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # Spring Boot 4 · Java 25 · Spring Framework 7
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/spring-boot4-skill.svg)](https://www.npmjs.com/package/spring-boot4-skill)
4
+
3
5
  > **2026-standard** project scaffolder + Claude Code AI skill for Java / Spring development.
4
- > By [AyrtonAldayr](https://github.com/AyrtonAldayr) · **v1.1.0**
6
+ > By [AyrtonAldayr](https://github.com/AyrtonAldayr) · **v1.2.0**
5
7
 
6
8
  This repository provides two tools in one:
7
9
 
@@ -44,6 +46,26 @@ After answering a few prompts, you get a complete project with:
44
46
  - **`compose.yaml`** *(optional)* — Docker Compose for PostgreSQL 17, MongoDB 7, OTEL Collector
45
47
  - **Spring Modulith module skeleton** *(optional)*
46
48
 
49
+ **Example output** (Gradle, default options):
50
+
51
+ ```
52
+ my-service/
53
+ ├── build.gradle.kts
54
+ ├── settings.gradle.kts
55
+ ├── src/
56
+ │ ├── main/
57
+ │ │ ├── java/<package-path>/
58
+ │ │ │ └── Application.java
59
+ │ │ └── resources/
60
+ │ │ └── application.yaml
61
+ │ └── test/
62
+ │ └── java/<package-path>/
63
+ │ └── ApplicationTests.java
64
+ └── compose.yaml # if Docker Compose was selected
65
+ ```
66
+
67
+ The generator prints the full path of the created project (e.g. `Project created at /path/to/my-service`).
68
+
47
69
  ### Wizard options
48
70
 
49
71
  ```
@@ -114,6 +136,7 @@ Once installed, Claude Code acts as a **Senior Spring Boot 4 architect**:
114
136
  | `skills/java-spring-framework/references/spring-framework-7.md` | All Spring 7 APIs with code examples |
115
137
  | `skills/java-spring-framework/references/spring-boot-4.md` | Boot 4: native, virtual threads, testing (Testcontainers), reactive stack |
116
138
  | `skills/java-spring-framework/references/spring-security-7.md` | OAuth2 Resource Server, JWT, method security, CORS |
139
+ | `skills/java-spring-framework/references/spring-messaging.md` | Kafka, event-driven, @KafkaListener, producer/consumer |
117
140
  | `skills/java-spring-framework/references/spring-modulith.md` | Module structure, events, integration testing, common pitfalls |
118
141
  | `skills/java-spring-framework/references/build-templates.md` | Complete Gradle KTS + Maven POM templates |
119
142
  | `skills/java-spring-framework/references/troubleshooting-migration.md` | Common errors (javax/jakarta, RestTemplate), Boot 3→4 checklist |
@@ -151,12 +174,17 @@ npx spring-boot4-skill
151
174
 
152
175
  ## Contributing
153
176
 
154
- PRs welcome. Please keep all generated code aligned with:
155
-
156
- - [Spring Boot 4.x docs](https://docs.spring.io/spring-boot/)
157
- - [Spring Framework 7.x docs](https://docs.spring.io/spring-framework/reference/)
158
- - [Spring Modulith docs](https://docs.spring.io/spring-modulith/reference/)
159
- - [JSpecify](https://jspecify.dev/)
177
+ PRs welcome.
178
+
179
+ 1. **Clone and install:** `git clone <repo> && cd agent-skill-java-spring-framework && npm install`
180
+ 2. **Run CLI locally:** `node bin/create-spring-app.js [project-name]` or `node bin/create-spring-app.js my-test --no-interactive`
181
+ 3. **Tests:** `npm test` (unit tests); `npm run smoke` (CLI smoke test).
182
+ 4. **Skill changes:** Keep the structure under `skills/java-spring-framework/` (SKILL.md plus `references/*.md`). Load criteria in the Reference Files table should stay accurate.
183
+ 5. **Generated code** must align with:
184
+ - [Spring Boot 4.x docs](https://docs.spring.io/spring-boot/)
185
+ - [Spring Framework 7.x docs](https://docs.spring.io/spring-framework/reference/)
186
+ - [Spring Modulith docs](https://docs.spring.io/spring-modulith/reference/)
187
+ - [JSpecify](https://jspecify.dev/)
160
188
 
161
189
  ---
162
190
 
@@ -0,0 +1,9 @@
1
+ /** @type {import('jest').Config} */
2
+ module.exports = {
3
+ testEnvironment: 'node',
4
+ testMatch: ['**/*.test.js'],
5
+ transform: {},
6
+ moduleNameMapper: {
7
+ '^(\\.{1,2}/.*)\\.js$': '$1',
8
+ },
9
+ };
package/lib/generator.js CHANGED
@@ -15,11 +15,9 @@ const TEMPLATES_DIR = join(__dirname, '../templates');
15
15
  export async function generateProject(config) {
16
16
  const spinner = ora('Generating project…').start();
17
17
  const projectDir = resolve(process.cwd(), config.projectName);
18
+ const ctx = buildContext(config);
18
19
 
19
20
  try {
20
- // Derive additional config values
21
- const ctx = buildContext(config);
22
-
23
21
  // Create root project directory
24
22
  mkdirSync(projectDir, { recursive: true });
25
23
 
@@ -45,15 +43,21 @@ export async function generateProject(config) {
45
43
 
46
44
  } catch (err) {
47
45
  spinner.fail('Generation failed');
46
+ const msg = err.code === 'EEXIST'
47
+ ? `Directory already exists: ${projectDir}`
48
+ : err.code === 'EACCES'
49
+ ? `Permission denied writing to: ${projectDir}`
50
+ : err.message || String(err);
51
+ console.error(chalk.red(msg));
48
52
  throw err;
49
53
  }
50
54
  }
51
55
 
52
56
  // ---------------------------------------------------------------------------
53
- // Context builder
57
+ // Context builder (exported for tests)
54
58
  // ---------------------------------------------------------------------------
55
59
 
56
- function buildContext(config) {
60
+ export function buildContext(config) {
57
61
  const appName = toPascalCase(config.projectName);
58
62
  const packagePath = config.packageName.replace(/\./g, '/');
59
63
  const features = config.features || [];
@@ -76,6 +80,7 @@ function buildContext(config) {
76
80
 
77
81
  return {
78
82
  ...config,
83
+ description: config.description ?? 'Spring Boot 4 microservice',
79
84
  appName,
80
85
  packagePath,
81
86
  features,
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Unit tests for the generator context builder (buildContext).
3
+ */
4
+
5
+ import { buildContext } from './generator.js';
6
+
7
+ describe('buildContext', () => {
8
+ it('includes projectName, packageName, appName, packagePath', () => {
9
+ const ctx = buildContext({
10
+ projectName: 'my-service',
11
+ packageName: 'com.acme',
12
+ database: 'none',
13
+ features: [],
14
+ });
15
+ expect(ctx.projectName).toBe('my-service');
16
+ expect(ctx.packageName).toBe('com.acme');
17
+ expect(ctx.appName).toBe('MyService');
18
+ expect(ctx.packagePath).toBe('com/acme');
19
+ });
20
+
21
+ it('sets hasJpa true for postgresql, mysql, h2', () => {
22
+ expect(buildContext({ projectName: 'x', packageName: 'c', database: 'postgresql', features: [] }).hasJpa).toBe(true);
23
+ expect(buildContext({ projectName: 'x', packageName: 'c', database: 'mysql', features: [] }).hasJpa).toBe(true);
24
+ expect(buildContext({ projectName: 'x', packageName: 'c', database: 'h2', features: [] }).hasJpa).toBe(true);
25
+ expect(buildContext({ projectName: 'x', packageName: 'c', database: 'none', features: [] }).hasJpa).toBe(false);
26
+ });
27
+
28
+ it('sets hasActuator, hasSecurity, hasValidation from features', () => {
29
+ const ctx = buildContext({
30
+ projectName: 'x',
31
+ packageName: 'c',
32
+ database: 'none',
33
+ features: ['actuator', 'validation'],
34
+ });
35
+ expect(ctx.hasActuator).toBe(true);
36
+ expect(ctx.hasSecurity).toBe(false);
37
+ expect(ctx.hasValidation).toBe(true);
38
+ });
39
+
40
+ it('sets hasModulith, hasNative, hasDockerCompose from features', () => {
41
+ const ctx = buildContext({
42
+ projectName: 'x',
43
+ packageName: 'c',
44
+ database: 'postgresql',
45
+ features: ['modulith', 'docker-compose'],
46
+ });
47
+ expect(ctx.hasModulith).toBe(true);
48
+ expect(ctx.hasNative).toBe(false);
49
+ expect(ctx.hasDockerCompose).toBe(true);
50
+ });
51
+
52
+ it('provides description default when missing', () => {
53
+ const ctx = buildContext({
54
+ projectName: 'x',
55
+ packageName: 'c',
56
+ database: 'none',
57
+ features: [],
58
+ });
59
+ expect(ctx.description).toBe('Spring Boot 4 microservice');
60
+ });
61
+
62
+ it('uses provided description when set', () => {
63
+ const ctx = buildContext({
64
+ projectName: 'x',
65
+ packageName: 'c',
66
+ description: 'My API',
67
+ database: 'none',
68
+ features: [],
69
+ });
70
+ expect(ctx.description).toBe('My API');
71
+ });
72
+
73
+ it('sets modulithDataDep for JPA when modulith and postgresql', () => {
74
+ const ctx = buildContext({
75
+ projectName: 'x',
76
+ packageName: 'c',
77
+ database: 'postgresql',
78
+ features: ['modulith'],
79
+ });
80
+ expect(ctx.modulithDataDep).toContain('modulith-starter-jpa');
81
+ });
82
+ });
package/lib/prompts.js CHANGED
@@ -7,6 +7,7 @@ import inquirer from 'inquirer';
7
7
  const DEFAULTS = {
8
8
  projectName: 'my-service',
9
9
  packageName: 'com.example',
10
+ description: 'Spring Boot 4 microservice',
10
11
  buildTool: 'gradle',
11
12
  javaVersion: '25',
12
13
  bootVersion: '4.0.3',
@@ -88,6 +89,12 @@ export async function runWizard(projectNameArg, options = {}) {
88
89
  ],
89
90
  default: 'postgresql',
90
91
  },
92
+ {
93
+ type: 'confirm',
94
+ name: 'minimal',
95
+ message: 'Minimal project (API only — no Actuator, Security, Validation)?',
96
+ default: false,
97
+ },
91
98
  {
92
99
  type: 'checkbox',
93
100
  name: 'features',
@@ -101,6 +108,7 @@ export async function runWizard(projectNameArg, options = {}) {
101
108
  { name: 'Spring WebFlux (Reactive)', value: 'webflux', checked: false },
102
109
  { name: 'Docker Compose support', value: 'docker-compose', checked: false },
103
110
  ],
111
+ when: (ans) => !ans.minimal,
104
112
  },
105
113
  {
106
114
  type: 'confirm',
@@ -111,5 +119,8 @@ export async function runWizard(projectNameArg, options = {}) {
111
119
  },
112
120
  ]);
113
121
 
122
+ if (answers.minimal) {
123
+ answers.features = [];
124
+ }
114
125
  return answers;
115
126
  }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Unit tests for the template engine (renderTemplate, interpolate, #if, #each).
3
+ * Uses in-memory template strings; does not read from disk.
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { tmpdir } from 'os';
9
+ import { renderTemplate } from './templates.js';
10
+
11
+ describe('templates', () => {
12
+ let tempDir;
13
+
14
+ beforeEach(() => {
15
+ tempDir = mkdtempSync(join(tmpdir(), 'skill-tpl-'));
16
+ });
17
+
18
+ afterEach(() => {
19
+ rmSync(tempDir, { recursive: true });
20
+ });
21
+
22
+ function createTemplate(content) {
23
+ const path = join(tempDir, 'tpl.txt');
24
+ writeFileSync(path, content);
25
+ return path;
26
+ }
27
+
28
+ describe('interpolate', () => {
29
+ it('replaces {{key}} with context value', () => {
30
+ const path = createTemplate('Hello {{name}}');
31
+ expect(renderTemplate(path, { name: 'World' })).toBe('Hello World');
32
+ });
33
+
34
+ it('replaces multiple placeholders', () => {
35
+ const path = createTemplate('{{a}} and {{b}}');
36
+ expect(renderTemplate(path, { a: 'A', b: 'B' })).toBe('A and B');
37
+ });
38
+
39
+ it('replaces undefined/null with empty string', () => {
40
+ const path = createTemplate('x{{missing}}y');
41
+ expect(renderTemplate(path, {})).toBe('xy');
42
+ });
43
+ });
44
+
45
+ describe('{{#if}}', () => {
46
+ it('includes block when value is truthy', () => {
47
+ const path = createTemplate('{{#if ok}}yes{{/if}}');
48
+ expect(renderTemplate(path, { ok: true })).toBe('yes');
49
+ });
50
+
51
+ it('omits block when value is falsy', () => {
52
+ const path = createTemplate('{{#if ok}}yes{{/if}}');
53
+ expect(renderTemplate(path, { ok: false })).toBe('');
54
+ });
55
+
56
+ it('supports negation {{#if !key}}', () => {
57
+ const path = createTemplate('{{#if !hide}}show{{/if}}');
58
+ expect(renderTemplate(path, { hide: true })).toBe('');
59
+ expect(renderTemplate(path, { hide: false })).toBe('show');
60
+ });
61
+ });
62
+
63
+ describe('{{#each}}', () => {
64
+ it('iterates array and exposes {{item}}', () => {
65
+ const path = createTemplate('{{#each items}}({{item}}){{/each}}');
66
+ expect(renderTemplate(path, { items: ['a', 'b'] })).toBe('(a)(b)');
67
+ });
68
+
69
+ it('returns empty string when key is not an array', () => {
70
+ const path = createTemplate('{{#each items}}x{{/each}}');
71
+ expect(renderTemplate(path, { items: null })).toBe('');
72
+ });
73
+ });
74
+
75
+ it('combines if and interpolate', () => {
76
+ const path = createTemplate('{{name}}{{#if active}} (active){{/if}}');
77
+ expect(renderTemplate(path, { name: 'Foo', active: true })).toBe('Foo (active)');
78
+ expect(renderTemplate(path, { name: 'Bar', active: false })).toBe('Bar');
79
+ });
80
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spring-boot4-skill",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Interactive CLI scaffold for Java 25 / Spring Boot 4.x projects — with a bundled Claude Code skill for AI-assisted development.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/create-spring-app.js",
11
- "test": "node --experimental-vm-modules node_modules/.bin/jest"
11
+ "test": "node --experimental-vm-modules node_modules/.bin/jest",
12
+ "smoke": "bash scripts/smoke-cli.sh"
12
13
  },
13
14
  "keywords": [
14
15
  "spring-boot",
@@ -40,5 +41,8 @@
40
41
  "commander": "^12.1.0",
41
42
  "inquirer": "^10.1.5",
42
43
  "ora": "^8.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "jest": "^29.7.0"
43
47
  }
44
48
  }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # Smoke test: run CLI in non-interactive mode and verify key files exist.
3
+ set -e
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
7
+ CLI="$REPO_ROOT/bin/create-spring-app.js"
8
+ PROJECT_NAME="smoke-test-project"
9
+ TMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'skill-smoke')
10
+
11
+ cd "$TMP_DIR"
12
+ node "$CLI" "$PROJECT_NAME" --no-interactive
13
+
14
+ PROJECT_DIR="$TMP_DIR/$PROJECT_NAME"
15
+ if [ ! -d "$PROJECT_DIR" ]; then
16
+ echo "FAIL: Project directory not created: $PROJECT_DIR"
17
+ exit 1
18
+ fi
19
+
20
+ if [ ! -f "$PROJECT_DIR/build.gradle.kts" ]; then
21
+ echo "FAIL: build.gradle.kts not found"
22
+ exit 1
23
+ fi
24
+
25
+ if [ ! -f "$PROJECT_DIR/src/main/resources/application.yaml" ]; then
26
+ echo "FAIL: application.yaml not found"
27
+ exit 1
28
+ fi
29
+
30
+ # Default package is com.example; app name from "smoke-test-project" -> SmokeTestProject
31
+ JAVA_FILE=$(find "$PROJECT_DIR/src/main/java" -name "*Application.java" 2>/dev/null | head -1)
32
+ if [ -z "$JAVA_FILE" ] || [ ! -f "$JAVA_FILE" ]; then
33
+ echo "FAIL: Application main class not found under src/main/java"
34
+ exit 1
35
+ fi
36
+
37
+ echo "PASS: Smoke test — CLI generated project with build.gradle.kts, application.yaml, and Application class."
38
+ rm -rf "$TMP_DIR"
@@ -40,6 +40,8 @@ flowchart TD
40
40
  I -->|Yes| J[build-templates.md]
41
41
  A --> K{Migration or errors?}
42
42
  K -->|Yes| L[troubleshooting-migration.md]
43
+ A --> M{Messaging / Kafka?}
44
+ M -->|Yes| N[spring-messaging.md]
43
45
  ```
44
46
 
45
47
  ## Mandatory Workflow
@@ -79,6 +81,7 @@ Load these as needed — do not load all at once:
79
81
  | Spring Framework 7 APIs | `references/spring-framework-7.md` | Framework-level features: versioning, resilience, JSpecify, SpEL, streaming |
80
82
  | Spring Boot 4 features | `references/spring-boot-4.md` | Boot auto-config, Actuator, native images, testing, virtual threads |
81
83
  | Spring Security 7 | `references/spring-security-7.md` | OAuth2 Resource Server, JWT, method security, CORS, authentication/authorization |
84
+ | Messaging (Kafka) | `references/spring-messaging.md` | Kafka, event-driven, messaging, @KafkaListener, producer/consumer |
82
85
  | Spring Modulith | `references/spring-modulith.md` | Domain-driven module design, event-driven architecture |
83
86
  | Build templates | `references/build-templates.md` | Gradle KTS or Maven POM scaffolding with 2026 BOM versions |
84
87
  | Troubleshooting & migration | `references/troubleshooting-migration.md` | Migration from Boot 3, compile/runtime errors (javax/jakarta, RestTemplate, native, null-safety) |
@@ -276,6 +276,8 @@ dependencies {
276
276
  | Micrometer | 1.15.x |
277
277
  | io.spring.dependency-management | 1.1.7 |
278
278
 
279
+ When updating BOM versions, keep the CLI templates under `templates/` and this reference in sync.
280
+
279
281
  ---
280
282
 
281
283
  ## 4. Spring Initializr CLI Quick Start
@@ -201,6 +201,31 @@ management:
201
201
 
202
202
  Protect actuator in Security 7: allow `health`/`info` for load balancers, require authentication for `metrics`/`prometheus`/`traces`. See `references/spring-security-7.md`.
203
203
 
204
+ ### Health groups (readiness / liveness)
205
+
206
+ Customize health groups for Kubernetes or load balancers. Example: a custom "readiness" group that includes DB and a custom indicator:
207
+
208
+ ```yaml
209
+ management:
210
+ endpoint:
211
+ health:
212
+ show-details: when-authorized
213
+ health:
214
+ livenessstate:
215
+ enabled: true
216
+ readinessstate:
217
+ enabled: true
218
+ db:
219
+ enabled: true
220
+ group:
221
+ readiness:
222
+ include: readinessState,db
223
+ liveness:
224
+ include: livenessState
225
+ ```
226
+
227
+ Expose only the group endpoints (e.g. `/actuator/health/readiness`, `/actuator/health/liveness`) in Security 7 for k8s probes.
228
+
204
229
  ### Dependencies (Micrometer + OTEL)
205
230
 
206
231
  ```kotlin
@@ -0,0 +1,132 @@
1
+ # Spring Messaging (Kafka) — Boot 4
2
+
3
+ **Spring Boot**: 4.0.x | **Spring Kafka**: aligned with Boot BOM | **Jakarta EE**: 11
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Dependencies](#1-dependencies)
10
+ 2. [application.yaml](#2-applicationyaml)
11
+ 3. [Consumer — @KafkaListener](#3-consumer--kafkalistener)
12
+ 4. [Producer](#4-producer)
13
+ 5. [Records for payloads](#5-records-for-payloads)
14
+
15
+ ---
16
+
17
+ ## 1. Dependencies
18
+
19
+ Spring Boot 4 BOM manages Spring Kafka. Add the starter:
20
+
21
+ ```kotlin
22
+ // build.gradle.kts
23
+ dependencies {
24
+ implementation("org.springframework.kafka:spring-kafka")
25
+ // or explicitly:
26
+ // implementation("org.springframework.boot:spring-boot-starter-web") // or webflux
27
+ // implementation("org.springframework.kafka:spring-kafka")
28
+ }
29
+ ```
30
+
31
+ For JSON (de)serialization with Jackson 3 (Jakarta):
32
+
33
+ ```kotlin
34
+ implementation("org.springframework.kafka:spring-kafka")
35
+ // Jackson is provided by Boot; ensure jakarta.* for JSON
36
+ ```
37
+
38
+ ---
39
+
40
+ ## 2. application.yaml
41
+
42
+ ```yaml
43
+ spring:
44
+ kafka:
45
+ bootstrap-servers: localhost:9092
46
+ consumer:
47
+ group-id: my-app
48
+ auto-offset-reset: earliest
49
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
50
+ value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
51
+ properties:
52
+ spring.json.trusted.packages: "*"
53
+ producer:
54
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
55
+ value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 3. Consumer — @KafkaListener
61
+
62
+ Use Records for message payloads where possible. Listen on a topic and process with Jakarta and JSpecify where applicable.
63
+
64
+ ```java
65
+ import org.springframework.kafka.annotation.KafkaListener;
66
+ import org.springframework.kafka.support.KafkaHeaders;
67
+ import org.springframework.messaging.handler.annotation.Header;
68
+ import org.springframework.messaging.handler.annotation.Payload;
69
+ import org.springframework.stereotype.Component;
70
+
71
+ @Component
72
+ public class OrderEventsConsumer {
73
+
74
+ @KafkaListener(topics = "orders", groupId = "my-app")
75
+ public void onOrder(@Payload OrderEvent event,
76
+ @Header(KafkaHeaders.RECEIVED_KEY) String key) {
77
+ // process event (OrderEvent as record)
78
+ }
79
+ }
80
+ ```
81
+
82
+ With batch consumption:
83
+
84
+ ```java
85
+ @KafkaListener(topics = "orders", groupId = "my-app", containerFactory = "batchFactory")
86
+ public void onOrders(@Payload List<OrderEvent> events) {
87
+ events.forEach(this::process);
88
+ }
89
+ ```
90
+
91
+ ---
92
+
93
+ ## 4. Producer
94
+
95
+ Inject `KafkaTemplate` and send records. Use `JsonSerializer` for value when configured in application.yaml.
96
+
97
+ ```java
98
+ import org.springframework.kafka.core.KafkaTemplate;
99
+ import org.springframework.stereotype.Service;
100
+
101
+ @Service
102
+ public class OrderEventsProducer {
103
+
104
+ private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
105
+
106
+ public OrderEventsProducer(KafkaTemplate<String, OrderEvent> kafkaTemplate) {
107
+ this.kafkaTemplate = kafkaTemplate;
108
+ }
109
+
110
+ public void send(OrderEvent event) {
111
+ kafkaTemplate.send("orders", event.orderId(), event);
112
+ }
113
+ }
114
+ ```
115
+
116
+ For a simple `KafkaTemplate<String, Object>` with JSON, ensure your payload type is on the trusted packages list for the consumer’s `JsonDeserializer`.
117
+
118
+ ---
119
+
120
+ ## 5. Records for payloads
121
+
122
+ Prefer Java records for event DTOs (Jackson 3 supports them):
123
+
124
+ ```java
125
+ public record OrderEvent(String orderId, String productId, int quantity, java.time.Instant at) {}
126
+ ```
127
+
128
+ Use `jakarta.*` and JSpecify nullability in shared libraries if you need strict null contracts; for internal events, records are often sufficient.
129
+
130
+ ---
131
+
132
+ **Summary:** Use `spring-kafka` with Boot 4 BOM, configure bootstrap servers and (de)serializers in `application.yaml`, and use `@KafkaListener` for consumers and `KafkaTemplate` for producers. Prefer records for event payloads. For Spring Cloud Stream (bindings), see the Spring Cloud Stream docs; the same Kafka dependencies can be used with Stream if you add the appropriate starters.
@@ -9,6 +9,7 @@ plugins {
9
9
 
10
10
  group = "{{packageName}}"
11
11
  version = "0.0.1-SNAPSHOT"
12
+ description = "{{description}}"
12
13
 
13
14
  java {
14
15
  toolchain {
@@ -5,9 +5,10 @@ import org.springframework.boot.SpringApplication;
5
5
  import org.springframework.boot.autoconfigure.SpringBootApplication;
6
6
 
7
7
  /**
8
- * {{appName}} — Spring Boot {{bootVersion}} / Java {{javaVersion}} / Jakarta EE 11
8
+ * {{appName}} — {{description}}
9
+ * Spring Boot {{bootVersion}} / Java {{javaVersion}} / Jakarta EE 11
9
10
  *
10
- * Generated by create-spring-boot4 ({{year}})
11
+ * Generated by spring-boot4-skill ({{year}})
11
12
  */
12
13
  @NullMarked
13
14
  @SpringBootApplication(proxyBeanMethods = false)