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 +50 -0
- package/README.md +35 -7
- 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 +3 -0
- package/skills/java-spring-framework/references/build-templates.md +2 -0
- package/skills/java-spring-framework/references/spring-boot-4.md +25 -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,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
|
+
[](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.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.
|
|
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.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.
|
|
@@ -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)
|