Skip to content

fix(devloop): restart for a new Spring bean or entity the running app never had (#25595) (CP: 25.3) - #25704

Merged
vaadin-bot merged 1 commit into
25.3from
cherry-pick-25595-to-25.3-1789384617527
Sep 14, 2026
Merged

vaadin-bot merged 1 commit into
25.3from
cherry-pick-25595-to-25.3-1789384617527

Conversation

@vaadin-bot

Copy link
Copy Markdown
Collaborator

This PR cherry-picks changes from the original PR #25595 to branch 25.3.

Original PR description

Fixes #25559

behavior deviation · dev loop · Spring applications using vaadin-dev

Background — component scanning. Spring builds its bean definitions
once, while the context starts, from the classes that exist at that
moment, and JPA fixes its metamodel and its schema at startup in the same
way. A class compiled later is loaded by the JVM but never scanned.

Adding a new Spring bean or a new JPA entity and running apply reported
a successful hot reload, and the application then failed with a Spring
error instead of asking for a restart. A class that did not exist at
startup gets no bean definition and no entity mapping, so a hot reload
can never make it live. apply now restarts for such a class.

Risks:

  • ⚠️ Behavior change: apply restarts, instead of reporting a hot
    reload, when the change set adds a class the running application never
    had that carries a Spring stereotype or a JPA mapping annotation. A
    bean or an entity the application started with still hot swaps.

  • ✅ No public or protected API change, nothing to migrate, and no
    security, memory, serialization, threading or performance impact.

  • Escalated apply to a restart, with the reason new Spring bean (X),
    for a class carrying @Component, @Service, @Repository,
    @Controller, @RestController, @ControllerAdvice,
    @RestControllerAdvice or @Configuration that the running
    application never had. A brand-new @Entity escalates too, because the
    new bytes of a class this JVM never loaded are now inspected.

  • Read the stereotype off the compiled bytes and reported it in a new
    stereotypes= field, appended last so an older daemon finds every
    existing field of the REDEFINE reply where it was.

  • Answered whether the application ever had a class from a snapshot of
    the classpath taken at each application start, per class and by binary
    name. What the JVM has loaded cannot answer it: HotswapAgent defines a
    new class as soon as it sees the class file, and its Spring plugin,
    which would rescan, is turned off for stability.

  • Split DevLoopRedefiner.redefine into inspect and reply, so the
    decision is testable without a running application.

  • Documented the escalation and its known gap — a stereotype composed
    through a project's own meta-annotation — in the daemon README.md,
    and covered the behavior in DevLoopRestartIT, DevLoopApplyIT and
    unit tests on both sides of the protocol.

… never had (#25595)

Fixes #25559

**behavior deviation** · dev loop · Spring applications using
`vaadin-dev`

**Background — component scanning.** Spring builds its bean definitions
once, while the context starts, from the classes that exist at that
moment, and JPA fixes its metamodel and its schema at startup in the
same
way. A class compiled later is loaded by the JVM but never scanned.

Adding a new Spring bean or a new JPA entity and running `apply`
reported
a successful hot reload, and the application then failed with a Spring
error instead of asking for a restart. A class that did not exist at
startup gets no bean definition and no entity mapping, so a hot reload
can never make it live. `apply` now restarts for such a class.

**Risks:**
- ⚠️ Behavior change: `apply` restarts, instead of reporting a hot
  reload, when the change set adds a class the running application never
  had that carries a Spring stereotype or a JPA mapping annotation. A
  bean or an entity the application started with still hot swaps.
- ✅ No public or protected API change, nothing to migrate, and no
  security, memory, serialization, threading or performance impact.

- Escalated `apply` to a restart, with the reason `new Spring bean (X)`,
  for a class carrying `@Component`, `@Service`, `@Repository`,
  `@Controller`, `@RestController`, `@ControllerAdvice`,
  `@RestControllerAdvice` or `@Configuration` that the running
application never had. A brand-new `@Entity` escalates too, because the
  new bytes of a class this JVM never loaded are now inspected.
- Read the stereotype off the compiled bytes and reported it in a new
  `stereotypes=` field, appended last so an older daemon finds every
  existing field of the `REDEFINE` reply where it was.
- Answered whether the application ever had a class from a snapshot of
  the classpath taken at each application start, per class and by binary
  name. What the JVM has loaded cannot answer it: HotswapAgent defines a
  new class as soon as it sees the class file, and its Spring plugin,
  which would rescan, is turned off for stability.
- Split `DevLoopRedefiner.redefine` into `inspect` and `reply`, so the
  decision is testable without a running application.
- Documented the escalation and its known gap — a stereotype composed
  through a project's own meta-annotation — in the daemon `README.md`,
  and covered the behavior in `DevLoopRestartIT`, `DevLoopApplyIT` and
  unit tests on both sides of the protocol.

---------

Co-authored-by: totally-not-ai[bot] <290682512+totally-not-ai[bot]@users.noreply.github.com>
Co-authored-by: Tomi Virtanen <tltv@vaadin.com>
@github-actions

Copy link
Copy Markdown
Contributor

A new @Component now restarts because the daemon joins the app's stereotype list with its own launch snapshot

sequenceDiagram
    participant T as TransactionEngine.apply (daemon)
    participant R as DevLoopRedefiner (running app)
    participant C as Compile.launchedWith (snapshot)
    T->>R: REDEFINE tx.classes
    R->>R: inspect reads bytes, declaresSpringBean
    R-->>T: OK reply, now carrying stereotypes= (new, appended last)
    T->>C: classesUnknownToTheApp(tx.classes)
    C-->>T: classes absent from the launch snapshot
    T->>T: blockedReason joins stereotypes with unknown classes
    Note over T: non-empty join now restarts with reason new Spring bean X; before, apply reported Stable and hot-reloaded it
Loading

The figure shows the apply round trip that decides hot-reload versus restart. Neither side can answer alone: DevLoopRedefiner.inspect reads the stereotype off the compiled bytes (it cannot know the app's history), while Compile.classesUnknownToTheApp compares against launchedWith, a classpath snapshot seeded when the app registers (onConnectorseedClasses). TransactionEngine.blockedReason intersects the two; a class that both carries a stereotype and was never launched-with escalates to a restart. Mechanism per the PR description and #25559; the stereotypes= field is appended last so an older daemon still finds every existing field.

Diagram Bot draws the mechanism this pull request touches; it does not review the change. Verify it against the diff.

Generated by Diagram Bot for issue #25704 ·

@vaadin-bot

Copy link
Copy Markdown
Collaborator Author

This PR is eligible for auto-merging policy, so it has been approved automatically. If there are pending conditions, auto merge (with 'squash' method) has been enabled for this PR [Message is sent from bot]

@vaadin-bot
vaadin-bot enabled auto-merge (squash) September 14, 2026 11:29
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

 1 439 files  ± 0   1 523 suites  ±0   1h 32m 29s ⏱️ - 3m 53s
12 066 tests +15  11 998 ✅ +15  68 💤 ±0  0 ❌ ±0 
12 384 runs  +15  12 316 ✅ +15  68 💤 ±0  0 ❌ ±0 

Results for commit d9c37ce. ± Comparison against base commit 357fffc.

@vaadin-bot
vaadin-bot merged commit 64ae307 into 25.3 Sep 14, 2026
42 checks passed
@vaadin-bot
vaadin-bot deleted the cherry-pick-25595-to-25.3-1789384617527 branch September 14, 2026 11:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants