spring-boot4-skill 1.1.0 → 1.3.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 +61 -0
- package/README.md +36 -8
- package/jest.config.cjs +9 -0
- package/lib/generator.js +10 -5
- package/lib/generator.test.js +82 -0
- package/lib/prompts.js +11 -0
- package/lib/templates.test.js +80 -0
- package/package.json +6 -2
- package/scripts/smoke-cli.sh +38 -0
- package/skills/java-spring-framework/SKILL.md +6 -1
- package/skills/java-spring-framework/references/build-templates.md +2 -0
- package/skills/java-spring-framework/references/spring-boot-4.md +177 -0
- package/skills/java-spring-framework/references/spring-framework-7.md +2 -0
- package/skills/java-spring-framework/references/spring-messaging.md +132 -0
- package/templates/gradle-kotlin/build.gradle.kts.template +1 -0
- package/templates/gradle-kotlin/src/main/java/com/example/app/Application.java.template +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
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.3.0] - 2026-02-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Skill:** spring-boot-4.md: new section 12 (Rate limiting — Bucket4j, time-window vs @ConcurrencyLimit) and section 13 (Resources & performance — HikariCP/R2DBC pools, Actuator pool metrics, Caffeine cache, performance tuning summary).
|
|
13
|
+
- **Skill:** SKILL.md Reference Files "Load when" extended for rate limiting, connection pools, resource metrics, caching, performance tuning; Quick decision mermaid node for "Rate limit / resources / performance?".
|
|
14
|
+
- **Skill:** spring-framework-7.md Resilience section: note linking to spring-boot-4.md for time-window rate limiting.
|
|
15
|
+
- **README:** Skill reference table: spring-boot-4.md description now includes rate limiting, connection pools, caching, performance.
|
|
16
|
+
|
|
17
|
+
[1.3.0]: https://github.com/AyrtonAldayr/agent-skill-java-spring-framework/compare/v1.2.0...v1.3.0
|
|
18
|
+
|
|
19
|
+
## [1.2.0] - 2026-02-21
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Docs:** CHANGELOG.md (Keep a Changelog); README "What you get" example tree, expanded Contributing, npm version badge.
|
|
24
|
+
- **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.
|
|
25
|
+
- **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.
|
|
26
|
+
- **Quality:** Jest config and unit tests for templates and buildContext; `scripts/smoke-cli.sh` and `npm run smoke`; `buildContext` exported for tests.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **CLI:** Generator context includes `description` default; success message already showed absolute path.
|
|
31
|
+
|
|
32
|
+
[1.2.0]: https://github.com/AyrtonAldayr/agent-skill-java-spring-framework/compare/v1.1.0...v1.2.0
|
|
33
|
+
|
|
34
|
+
## [1.1.0] - 2026-02-21
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **Skill:** New reference `spring-security-7.md` — OAuth2 Resource Server, JWT, method security (`@PreAuthorize`), CORS.
|
|
39
|
+
- **Skill:** New reference `troubleshooting-migration.md` — common errors (javax/jakarta, RestTemplate, JSpecify, native), Boot 3→4 migration checklist.
|
|
40
|
+
- **Skill:** "When NOT to use this skill" section and Quick decision (mermaid) in SKILL.md.
|
|
41
|
+
- **Skill:** Spring Boot 4 reference: Testcontainers subsection, secure Actuator exposure, Reactive stack (R2DBC + WebFlux), redirect to spring-security-7.md for OAuth2/JWT.
|
|
42
|
+
- **Skill:** Spring Modulith reference: "Common pitfalls" subsection.
|
|
43
|
+
- **Skill:** Spring Framework 7: note for reactive apps (WebFlux + R2DBC) with link to spring-boot-4.md.
|
|
44
|
+
- **README:** Install options for skill via `npx skills add` and `claude skills install`; skill reference table updated.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **Skill:** Reference table in SKILL.md now includes Spring Security 7 and Troubleshooting & migration; triggers listed in body.
|
|
49
|
+
|
|
50
|
+
## [1.0.0] - 2026-02-21
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- **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.
|
|
55
|
+
- **CLI:** Non-interactive mode (`--no-interactive`) and Maven option (`--maven`).
|
|
56
|
+
- **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.
|
|
57
|
+
- **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).
|
|
58
|
+
- **npm:** Package `spring-boot4-skill` publishable with `npx spring-boot4-skill`.
|
|
59
|
+
|
|
60
|
+
[1.1.0]: https://github.com/AyrtonAldayr/agent-skill-java-spring-framework/compare/v1.0.0...v1.1.0
|
|
61
|
+
[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
|
+
[](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.
|
|
6
|
+
> By [AyrtonAldayr](https://github.com/AyrtonAldayr) · **v1.3.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
|
```
|
|
@@ -112,8 +134,9 @@ Once installed, Claude Code acts as a **Senior Spring Boot 4 architect**:
|
|
|
112
134
|
| File | Contents |
|
|
113
135
|
|---|---|
|
|
114
136
|
| `skills/java-spring-framework/references/spring-framework-7.md` | All Spring 7 APIs with code examples |
|
|
115
|
-
| `skills/java-spring-framework/references/spring-boot-4.md` | Boot 4: native, virtual threads, testing (Testcontainers), reactive stack |
|
|
137
|
+
| `skills/java-spring-framework/references/spring-boot-4.md` | Boot 4: native, virtual threads, testing (Testcontainers), reactive stack, rate limiting, connection pools, caching, performance |
|
|
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.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
- [
|
|
158
|
-
|
|
159
|
-
-
|
|
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
|
|
package/jest.config.cjs
ADDED
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.
|
|
3
|
+
"version": "1.3.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,10 @@ 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]
|
|
45
|
+
A --> O{Rate limit / resources / performance?}
|
|
46
|
+
O -->|Yes| P[spring-boot-4.md]
|
|
43
47
|
```
|
|
44
48
|
|
|
45
49
|
## Mandatory Workflow
|
|
@@ -77,8 +81,9 @@ Load these as needed — do not load all at once:
|
|
|
77
81
|
| Topic | File | Load when |
|
|
78
82
|
|---|---|---|
|
|
79
83
|
| Spring Framework 7 APIs | `references/spring-framework-7.md` | Framework-level features: versioning, resilience, JSpecify, SpEL, streaming |
|
|
80
|
-
| Spring Boot 4 features | `references/spring-boot-4.md` | Boot auto-config, Actuator, native images, testing, virtual threads |
|
|
84
|
+
| Spring Boot 4 features | `references/spring-boot-4.md` | Boot auto-config, Actuator, native images, testing, virtual threads, rate limiting, connection pools, resource metrics, caching, performance tuning |
|
|
81
85
|
| Spring Security 7 | `references/spring-security-7.md` | OAuth2 Resource Server, JWT, method security, CORS, authentication/authorization |
|
|
86
|
+
| Messaging (Kafka) | `references/spring-messaging.md` | Kafka, event-driven, messaging, @KafkaListener, producer/consumer |
|
|
82
87
|
| Spring Modulith | `references/spring-modulith.md` | Domain-driven module design, event-driven architecture |
|
|
83
88
|
| Build templates | `references/build-templates.md` | Gradle KTS or Maven POM scaffolding with 2026 BOM versions |
|
|
84
89
|
| 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
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
9. [Docker Compose Support](#9-docker-compose-support)
|
|
17
17
|
10. [Spring Security 7 Basics](#10-spring-security-7-basics)
|
|
18
18
|
11. [Reactive Stack (R2DBC + WebFlux)](#11-reactive-stack-r2dbc--webflux)
|
|
19
|
+
12. [Rate Limiting](#12-rate-limiting)
|
|
20
|
+
13. [Resources & Performance](#13-resources--performance)
|
|
19
21
|
|
|
20
22
|
---
|
|
21
23
|
|
|
@@ -201,6 +203,31 @@ management:
|
|
|
201
203
|
|
|
202
204
|
Protect actuator in Security 7: allow `health`/`info` for load balancers, require authentication for `metrics`/`prometheus`/`traces`. See `references/spring-security-7.md`.
|
|
203
205
|
|
|
206
|
+
### Health groups (readiness / liveness)
|
|
207
|
+
|
|
208
|
+
Customize health groups for Kubernetes or load balancers. Example: a custom "readiness" group that includes DB and a custom indicator:
|
|
209
|
+
|
|
210
|
+
```yaml
|
|
211
|
+
management:
|
|
212
|
+
endpoint:
|
|
213
|
+
health:
|
|
214
|
+
show-details: when-authorized
|
|
215
|
+
health:
|
|
216
|
+
livenessstate:
|
|
217
|
+
enabled: true
|
|
218
|
+
readinessstate:
|
|
219
|
+
enabled: true
|
|
220
|
+
db:
|
|
221
|
+
enabled: true
|
|
222
|
+
group:
|
|
223
|
+
readiness:
|
|
224
|
+
include: readinessState,db
|
|
225
|
+
liveness:
|
|
226
|
+
include: livenessState
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Expose only the group endpoints (e.g. `/actuator/health/readiness`, `/actuator/health/liveness`) in Security 7 for k8s probes.
|
|
230
|
+
|
|
204
231
|
### Dependencies (Micrometer + OTEL)
|
|
205
232
|
|
|
206
233
|
```kotlin
|
|
@@ -421,3 +448,153 @@ Use `Netty` as the default server (Boot chooses it when `spring-boot-starter-web
|
|
|
421
448
|
### R2DBC repositories and WebFlux controllers
|
|
422
449
|
|
|
423
450
|
Define reactive repositories (`ReactiveCrudRepository`) and inject them into `@RestController` or handler functions. Use `ServerWebExchange`, `Mono`, and `Flux` for reactive types. For **RestClient** in a reactive app, use the reactive variant and streaming; see `references/spring-framework-7.md` (Streaming Support) for alignment with Spring 7 APIs.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## 12. Rate Limiting
|
|
455
|
+
|
|
456
|
+
**Concurrency vs time-based:** For **limiting concurrent calls per method**, use Spring 7’s built-in `@ConcurrencyLimit(maxConcurrentCalls = N)` — see `references/spring-framework-7.md` (Resilience Annotations). For **rate limiting by time window** (e.g. 100 requests per minute per IP or per user), use an in-app filter/interceptor with a token-bucket implementation or a gateway.
|
|
457
|
+
|
|
458
|
+
### Time-based rate limit (in application)
|
|
459
|
+
|
|
460
|
+
Use **Bucket4j** (token bucket) with a key per client (e.g. IP or authenticated user). Apply it in a `Filter` or `HandlerInterceptor` and return `429 Too Many Requests` when the bucket is exhausted.
|
|
461
|
+
|
|
462
|
+
**Dependency:**
|
|
463
|
+
|
|
464
|
+
```kotlin
|
|
465
|
+
// build.gradle.kts
|
|
466
|
+
implementation("com.bucket4j:bucket4j-core:8.10.1")
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**Example: filter keyed by IP**
|
|
470
|
+
|
|
471
|
+
```java
|
|
472
|
+
import io.github.bucket4j.Bandwidth;
|
|
473
|
+
import io.github.bucket4j.Bucket;
|
|
474
|
+
import io.github.bucket4j.Refill;
|
|
475
|
+
import jakarta.servlet.FilterChain;
|
|
476
|
+
import jakarta.servlet.ServletException;
|
|
477
|
+
import jakarta.servlet.http.HttpServletRequest;
|
|
478
|
+
import jakarta.servlet.http.HttpServletResponse;
|
|
479
|
+
import org.springframework.core.annotation.Order;
|
|
480
|
+
import org.springframework.stereotype.Component;
|
|
481
|
+
import org.springframework.web.filter.OncePerRequestFilter;
|
|
482
|
+
|
|
483
|
+
import java.io.IOException;
|
|
484
|
+
import java.time.Duration;
|
|
485
|
+
import java.util.Map;
|
|
486
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
487
|
+
|
|
488
|
+
@Component
|
|
489
|
+
@Order(1)
|
|
490
|
+
public class RateLimitFilter extends OncePerRequestFilter {
|
|
491
|
+
|
|
492
|
+
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
|
|
493
|
+
private static final int CAPACITY = 100;
|
|
494
|
+
private static final Duration REFILL_DURATION = Duration.ofMinutes(1);
|
|
495
|
+
|
|
496
|
+
@Override
|
|
497
|
+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
|
498
|
+
FilterChain filterChain) throws ServletException, IOException {
|
|
499
|
+
String key = clientKey(request); // e.g. request.getRemoteAddr() or principal name
|
|
500
|
+
Bucket bucket = buckets.computeIfAbsent(key, k -> newBucket());
|
|
501
|
+
if (bucket.tryConsume(1)) {
|
|
502
|
+
filterChain.doFilter(request, response);
|
|
503
|
+
} else {
|
|
504
|
+
response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
|
|
505
|
+
response.getWriter().write("Rate limit exceeded");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private static String clientKey(HttpServletRequest request) {
|
|
510
|
+
return request.getRemoteAddr();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private static Bucket newBucket() {
|
|
514
|
+
Bandwidth limit = Bandwidth.classic(CAPACITY, Refill.greedy(CAPACITY, REFILL_DURATION));
|
|
515
|
+
return Bucket.builder().addLimit(limit).build();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
For **per-user** limits, use a key derived from `SecurityContextHolder.getContext().getAuthentication()` (e.g. principal name) and exempt unauthenticated or health endpoints from the filter if needed.
|
|
521
|
+
|
|
522
|
+
**When to use which:** Use `@ConcurrencyLimit` to cap concurrent executions of a specific method. Use a filter with Bucket4j (or similar) for API-level limits per time window (per IP or per user). If traffic is fronted by **Spring Cloud Gateway**, rate limiting can also be implemented at the edge; that is outside the scope of this reference.
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## 13. Resources & Performance
|
|
527
|
+
|
|
528
|
+
### Connection pools (HikariCP, R2DBC)
|
|
529
|
+
|
|
530
|
+
**HikariCP (blocking JDBC)** — Spring Boot configures it by default. Tune in `application.yaml`:
|
|
531
|
+
|
|
532
|
+
```yaml
|
|
533
|
+
spring:
|
|
534
|
+
datasource:
|
|
535
|
+
hikari:
|
|
536
|
+
maximum-pool-size: 20
|
|
537
|
+
minimum-idle: 5
|
|
538
|
+
connection-timeout: 30000
|
|
539
|
+
idle-timeout: 600000
|
|
540
|
+
max-lifetime: 1800000
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
See [Spring Boot Data Access](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.sql.datasource.hikari) for all options.
|
|
544
|
+
|
|
545
|
+
**R2DBC** — For reactive apps, configure the connection pool similarly (e.g. `spring.r2dbc.pool.*`). DB health is included in health groups (section 4); use readiness to avoid sending traffic to instances that cannot get a connection.
|
|
546
|
+
|
|
547
|
+
### Metrics for resources
|
|
548
|
+
|
|
549
|
+
Actuator exposes **HikariCP** metrics (e.g. `hikaricp.connections.active`, `hikaricp.connections.idle`, `hikaricp.connections.pending`). Use these in Prometheus/Grafana to size pools and alert on exhaustion. R2DBC pool metrics follow a similar pattern when the reactive stack is used.
|
|
550
|
+
|
|
551
|
+
### Caching
|
|
552
|
+
|
|
553
|
+
Use **Spring Cache** with **Caffeine** for in-process caching. Reduces repeated work and database load.
|
|
554
|
+
|
|
555
|
+
**Dependencies:**
|
|
556
|
+
|
|
557
|
+
```kotlin
|
|
558
|
+
// build.gradle.kts
|
|
559
|
+
implementation("org.springframework.boot:spring-boot-starter-cache")
|
|
560
|
+
implementation("com.github.ben-manes.caffeine:caffeine")
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Enable and configure:**
|
|
564
|
+
|
|
565
|
+
```java
|
|
566
|
+
@SpringBootApplication
|
|
567
|
+
@EnableCaching
|
|
568
|
+
public class Application { ... }
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
```yaml
|
|
572
|
+
# application.yaml
|
|
573
|
+
spring:
|
|
574
|
+
cache:
|
|
575
|
+
cache-names: products,users
|
|
576
|
+
caffeine:
|
|
577
|
+
spec: maximumSize=1000,expireAfterWrite=300s
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**Usage:**
|
|
581
|
+
|
|
582
|
+
```java
|
|
583
|
+
@Service
|
|
584
|
+
public class ProductService {
|
|
585
|
+
|
|
586
|
+
@Cacheable(cacheNames = "products", key = "#id")
|
|
587
|
+
public Product findById(Long id) {
|
|
588
|
+
return productRepository.findById(id).orElseThrow();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Use short TTLs or size limits to avoid stale or unbounded caches.
|
|
594
|
+
|
|
595
|
+
### Performance tuning (summary)
|
|
596
|
+
|
|
597
|
+
- **Virtual threads** (section 6): Enable for high concurrency with blocking I/O; no need to size a large thread pool.
|
|
598
|
+
- **Pool sizing:** Set HikariCP (or R2DBC pool) size based on observed metrics (connections in use, pending); avoid over-provisioning.
|
|
599
|
+
- **N+1:** Avoid N+1 queries in JPA (e.g. `@EntityGraph`, fetch joins, or DTO projections) so a single request does not open many statements.
|
|
600
|
+
- **Observability:** Use Actuator/Prometheus and traces (section 4) to find bottlenecks (slow endpoints, DB, or external calls) and tune accordingly. No JVM-level tuning (heap, GC) is covered here.
|
|
@@ -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.
|
|
@@ -5,9 +5,10 @@ import org.springframework.boot.SpringApplication;
|
|
|
5
5
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* {{appName}} —
|
|
8
|
+
* {{appName}} — {{description}}
|
|
9
|
+
* Spring Boot {{bootVersion}} / Java {{javaVersion}} / Jakarta EE 11
|
|
9
10
|
*
|
|
10
|
-
* Generated by
|
|
11
|
+
* Generated by spring-boot4-skill ({{year}})
|
|
11
12
|
*/
|
|
12
13
|
@NullMarked
|
|
13
14
|
@SpringBootApplication(proxyBeanMethods = false)
|