Skip to content

Spring Boot-style client-Java model: IoC bean container, constructor & collection injection, self-describing handlers, annotation-free extensions#6051

Merged
iliyan-velichkov merged 12 commits into
masterfrom
feat/java-bean-container-constructor-injection
Jun 22, 2026
Merged

Spring Boot-style client-Java model: IoC bean container, constructor & collection injection, self-describing handlers, annotation-free extensions#6051
iliyan-velichkov merged 12 commits into
masterfrom
feat/java-bean-container-constructor-injection

Conversation

@iliyan-velichkov

@iliyan-velichkov iliyan-velichkov commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Reworks the client-Java development model so it follows Spring Boot idioms end to end: a real IoC bean container, constructor & collection injection, two clean handler styles, annotation-free extension points, a client-facing bean facade, and IDE-surfaced wiring errors.

Before this PR the model was a service locator (only @Repository injectable, field-only @Inject, everything else through the platform-internal BeanProvider), component callbacks bound only via class-level annotations with a reflective by-name fallback, the strong-interface style still needed a class annotation for its binding, and extensions required a dedicated @Extension/@ExtensionPoint pair.

Bean container & dependency injection (api-modules-java, engine-java, core-java)

  • @Component (sdk.component) — marks a client class a managed bean; optional value() name, default = decapitalized simple class name (Spring convention). @Repository, @Controller and @Websocket are meta-annotated @Component, so they are beans too.
  • A per-ClientClassLoader-generation container instantiates every bean with constructor injection (preferred), @Inject field injection, and collection injection (List/Set/Collection<T> receives every bean assignable to T), resolving by type independent of declaration order, with construction-cycle detection and @PostConstruct/@PreDestroy lifecycle callbacks.
  • Beans facade (sdk.component.Beans: get(Class), get(name, Class), getAll(Class)) is the client-facing way to reach platform services; client code no longer uses BeanProvider.
  • Removed the now-redundant RepositoryRegistry, RepositoryClassConsumer and the DependencyResolver SPI.

Two clean handler styles — never mixed (jobs, listeners, websockets)

A @Component class uses exactly one style; the engine rejects a class that mixes them.

  1. Self-describing interfaceJobHandler (cron()+run()), MessageHandler (destination()/kind()+onMessage), WebsocketHandler (endpoint()+lifecycle callbacks). The interface carries its own binding, so no class annotation — mirroring org.quartz.Job / jakarta.jms.MessageListener / TextWebSocketHandler.
  2. Method-level annotation@Scheduled(expression=…) / @Listener(name=,kind=) on a @Component method (Spring @Scheduled / @JmsListener style); websockets keep a @Websocket(endpoint=…) class with @OnOpen/@OnMessage/@OnError/@OnClose methods (the endpoint has no method-level home, like Jakarta @ServerEndpoint).

The reflective by-name fallback and the annotation+interface hybrid are removed.

Extension points without annotations

@Extension/@ExtensionPoint are gone. An extension point is a plain Java interface; a contribution is a @Component implementing it (its @Component name is the contribution name). Consume via List<Interface> collection injection (preferred); Extensions.find(Class) is preserved and resolves the same beans, and Extensions.getExtensions(String) stays for cross-runtime TypeScript/JavaScript.

JavaHandler as a bean

A JavaHandler that is also a @Component is now dispatched as the container-built (injected) singleton — so a handler with a constructor-injected collaborator works. A plain JavaHandler (no @Component) is still instantiated per request via its no-arg constructor.

Developer experience & performance

  • Bean-wiring errors surface in the IDE Problems view on the offending file (unsatisfied/ambiguous dependency, construction cycle, duplicate bean name, throwing constructor) — alongside the existing compile-error surfacing — instead of only the server log.
  • Dispatch-time reflection is minimal: the self-describing interface styles dispatch through direct virtual calls (no reflection); the method-level styles cache the Method once at registration; all instantiation/discovery reflection is per rebuild, never per request. ComponentContainer.instanceOf(Class) is now an O(1) type-indexed lookup (was an O(n) scan, O(n²) per rebuild).

Templates

REST controller template uses constructor injection; the intent event-trigger template generates a self-describing MessageHandler.

Tests

  • Unit: ComponentContainerTest (constructor/collection injection, naming, cycles, lifecycle, wiring-error reporting); controller/loader tests adapted. 63 engine-java + 13 data-store-java green. CodeQL "useless parameter" findings fixed.
  • HTTP ITs: JavaComponentIT (constructor + collection injection, plus a @Component JavaHandler) and JavaNoMixingIT (verifies the no-mixing rejection end-to-end); IntentEngineIT updated for the self-describing trigger. Release-profile javadoc and formatter clean.
  • The 4 Java sample-clone ITs are temporarily @Disabled (they clone the dirigiblelabs/sample-java-* repos' master, mid-migration to this API); re-enable after the sample PRs merge.

Breaking change

The annotation+interface hybrid, the reflective by-name callbacks, and @Extension/@ExtensionPoint must move to the new styles. All in-repo samples, templates and tests are migrated.

Companion PRs (merge after this)

  • Samples: dirigiblelabs/sample-java-{entity,listener,job,websocket,extension}-decorator
  • Docs: dirigible-io/dirigible-io.github.io

🤖 Generated with Claude Code

@iliyan-velichkov

Copy link
Copy Markdown
Contributor Author

Addressed the three CodeQL useless parameter findings: removed the unused fqn parameter from ScheduledClassConsumer.schedule(), and the Ping/Pong cycle-detection test fixtures now store their injected collaborator in a field (the parameter is intentional — it forms the constructor-injection cycle the test exercises — so it's now referenced rather than dropped). Pushed in the latest commit.

@iliyan-velichkov iliyan-velichkov changed the title Java model: Spring-style bean container — @Component, constructor & collection injection, method-level callbacks Spring-style Java dev model: bean container, constructor & collection injection, two clean handler styles Jun 22, 2026
iliyan-velichkov and others added 8 commits June 22, 2026 13:43
…tor & collection injection, method-level callbacks

Reworks the client-Java programming model around a real IoC container so it
mirrors Spring Boot idioms.

SDK (api-modules-java):
- New @component (with optional bean name; Spring decapitalized-name default)
  and a client-facing Beans facade (get/get(name)/getAll) replacing direct
  BeanProvider use in client code.
- @repository, @controller, @extension, @scheduled, @Listener, @websocket are
  meta-annotated @component, so every client artefact is a managed bean.
- @Inject now targets constructors/parameters as well as fields.
- @scheduled and @Listener gain METHOD targets (Spring @scheduled / @JmsListener
  method style); new @OnOpen/@OnMessage/@OnError/@onclose for websockets.

Engine (engine-java, core-java):
- ComponentContainer instantiates every bean per ClientClassLoader generation
  with recursive constructor injection, @Inject field injection, collection
  injection (List/Set/Collection<T> -> all impls), @PostConstruct/@PreDestroy,
  and construction-cycle detection. Published via ClientBeansHolder
  (core-java) so the SDK Beans facade resolves client beans without a module
  cycle.
- JavaLoader builds the container between the unload and load passes; the
  Controller/Scheduled/Listener/Websocket consumers now fetch ready, injected
  instances instead of instantiating client classes themselves, and support
  both class-level (interface/convention) and method-level annotation styles.
- Websocket dispatch precedence (WebsocketHandler -> @onx -> reflective)
  centralised in JavaWebsocketRegistry#dispatch; WebsocketProcessor calls it
  reflectively (no engine-java dependency).
- Removed the now-redundant DependencyResolver SPI and data-store-java's
  RepositoryRegistry/RepositoryClassConsumer (repositories are ordinary beans).

Templates: REST controller and event trigger now use constructor injection.

Tests: new ComponentContainerTest and HTTP-only JavaComponentIT (constructor +
collection injection end-to-end); existing controller/loader tests adapted to
the container. Full backward compatibility retained (field @Inject, class-level
annotations, reflective callbacks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…per @component (no mixing)

Aligns the strong-interface style with Spring: the typed interface now carries its
own binding, so it needs no class-level annotation, and a @component class uses
exactly one style — never both.

- JobHandler gains cron(); MessageHandler gains destination()/kind(); WebsocketHandler
  gains endpoint(). A @component implementing one of these IS the job/listener/socket,
  no @Scheduled/@Listener/@websocket needed (like org.quartz.Job / jakarta.jms.MessageListener
  / TextWebSocketHandler).
- @scheduled and @Listener are now @target(METHOD) only and no longer meta-@component;
  the method-level style is a @Scheduled/@Listener method on a @component bean. @websocket
  stays the class marker for the @onx annotation style (the endpoint has no method-level
  home, like Jakarta @serverendpoint).
- Removed the reflective by-name fallback and the annotation+interface hybrid. Each consumer
  now rejects a class that mixes the two styles (implements the interface AND has the method
  annotation / @websocket), mirroring the existing @controller+JavaHandler rejection.
- Event-trigger template migrated to a self-describing MessageHandler.

Breaking change: hybrid and reflective handlers must move to one of the two styles; all
in-repo samples/templates are migrated. Engine + data-store unit tests green; formatter and
release-javadoc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
JavaNoMixingIT drops a WebsocketHandler that also carries @Websocket/@OnMessage (a mix) plus a clean interface-style handler; asserts over HTTP that the clean one registers and the mixed one does not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the unused fqn parameter from ScheduledClassConsumer.schedule(); store the injected collaborator in the Ping/Pong cycle test fixtures so the constructor parameter is used. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Compile errors already reach the developer via the Problems view + a FAILED
JavaFile artefact, but bean-wiring failures (unsatisfied/ambiguous dependency,
construction cycle, duplicate bean name, throwing constructor) only hit the
server log — invisible in the browser IDE. ComponentContainer now records these
per client-class FQN; JavaLoader carries them on RebuildResult; JavaSynchronizer
projects them onto the same Problems surface and marks the source FAILED. So a
developer whose @controller can't be wired now sees e.g. "No client bean of type
X to inject into demo.MyController" on the file itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ints are plain interfaces + @component; index beans by type

Extensions no longer need a dedicated annotation: an extension point is a plain Java
interface and a contribution is a @component bean implementing it (its @component name
is the contribution name). Consume via List<Interface> collection injection;
Extensions.find(Class) is preserved and now resolves the same beans from the container
(Beans.getAll). Removed @extension, @ExtensionPoint and ExtensionClassConsumer.

Performance: ComponentContainer now indexes singletons by runtime class, so
instanceOf(Class) — called by every behaviour consumer for every loaded class during a
rebuild — is O(1) instead of an O(n) scan (previously O(n^2) per rebuild). Dispatch-time
reflection is unchanged and already minimal: the self-describing interface styles dispatch
through direct virtual calls (no reflection); the method-level annotation styles cache the
Method once at registration and only Method.invoke per event; all instantiation/discovery
reflection is per-generation (rebuild), never per request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… sample ITs during the API migration

- IntentEngineIT: the event-trigger template now generates a self-describing MessageHandler
  (destination() returns the topic) instead of @Listener(name=...); assert on the destination
  string + `implements MessageHandler`.
- The 5 Java sample ITs clone the dirigiblelabs sample repos' master, which is mid-migration to
  the new client-Java API (this PR's breaking changes). The four that exercise the removed
  reflective/@Extension/class-level paths are @disabled until the matching sample PRs merge; the
  engine itself stays covered by JavaEngineIT, JavaComponentIT and JavaNoMixingIT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@iliyan-velichkov iliyan-velichkov force-pushed the feat/java-bean-container-constructor-injection branch from 52e2c28 to 2f6c8d1 Compare June 22, 2026 10:48
…ontainer bean

A class implementing JavaHandler that is also a @component was built (and
constructor/field-injected) by the bean container, but the JavaHandler dispatch
path still instantiated it via its no-arg constructor — so a handler with an
injected collaborator failed at request time with NoSuchMethodException:
`<init>()`. HandlerClassConsumer now fetches the container-built singleton (when
the handler is a @component) and LoadedHandler dispatches it; a plain JavaHandler
with no @component is still instantiated per request via its no-arg constructor.

Regression covered by JavaComponentIT (a @component JavaHandler with a
constructor-injected service served over HTTP).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@iliyan-velichkov iliyan-velichkov changed the title Spring-style Java dev model: bean container, constructor & collection injection, two clean handler styles Spring Boot-style client-Java model: IoC bean container, constructor & collection injection, self-describing handlers, annotation-free extensions Jun 22, 2026
…sessions

- New components/engine/engine-java/CLAUDE.md: the deep guide (bean container,
  constructor/field/collection injection, Beans facade, two never-mixed handler
  styles, JavaHandler-as-bean, annotation-free extensions, error surfacing,
  removed internals, three-repo sequencing).
- Root CLAUDE.md "Client Java code" section condensed to a summary + pointer to
  the module guide (matching the engine-intent / engine-native-apps pattern);
  removed the stale SPI/@Order/RepositoryRegistry/engine.java.annotations text.
- engine-intent/CLAUDE.md: the generated trigger is a self-describing
  MessageHandler (not the @Listener+interface hybrid); added a handler-style note
  pointing at engine-java/CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@iliyan-velichkov iliyan-velichkov self-assigned this Jun 22, 2026
iliyan-velichkov and others added 2 commits June 22, 2026 15:40
…cts; re-enable sample ITs

- JavaComponentIT / JavaNoMixingIT: move the inline .java source strings into real fixture
  projects under src/main/resources/{JavaComponentIT,JavaNoMixingIT} and deploy them via a new
  ClientJavaProjectDeployer (copies the resource project into /registry/public and forces a sync
  cycle — fast, HTTP-only, no IDE). Drop the manual @AfterEach cleanup() — DirigibleCleaner
  already resets DB + the dirigible folder between integration tests.
- Re-enable the four @disabled sample ITs (entity / listener / job / extension); their sample PRs
  are being merged ahead of the platform PR. Assertions already match the migrated sample surface.
- JavaExtensionDecoratorSampleProjectIT: also assert the InjectingConsumer collection-injection
  endpoint (the headline feature), alongside the Extensions.find(...) path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… each sample IT

Each SampleProjectRepositoryIT now exercises both the self-describing-interface and the
method-level-annotation style its repo demonstrates:
- job: CleanupJob (JobHandler) + Maintenance (@scheduled method)
- listener: OrderListener (MessageHandler) + InvoiceListener (@Listener method → Auditor)
- websocket: ChatHandler (WebsocketHandler) + TickerHandler (@Websocket/@onx) via WebsocketStatus
- entity: CountryController (@Entity/@Repository/@controller) + GreetingController DI showcase
  (constructor injection + the Beans facade)

Paired with the sample-repo edits that expose the annotation-style signal (invoice-queue trigger,
two-endpoint WebsocketStatus, faster Maintenance cron, relocated GreetingController).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@iliyan-velichkov iliyan-velichkov requested a review from delchev June 22, 2026 14:42
@iliyan-velichkov iliyan-velichkov merged commit 456214d into master Jun 22, 2026
12 checks passed
@iliyan-velichkov iliyan-velichkov deleted the feat/java-bean-container-constructor-injection branch June 22, 2026 14:42
iliyan-velichkov added a commit to dirigible-io/dirigible-io.github.io that referenced this pull request Jun 22, 2026
…Spring Boot" guide, per-language split, annotation-free extensions (#128)

* Develop docs: Spring-style Java model

Update the Develop section for the new Spring-style Java SDK surface
introduced by eclipse-dirigible/dirigible#6051:

- dependency-injection.md: rewrite the Java section to @component (with
  Spring naming), constructor injection (preferred), field @Inject,
  collection injection (List<T>), @PostConstruct/@PreDestroy, and the
  Beans facade; client code should not use BeanProvider. Describe the
  single ComponentContainer per ClientClassLoader generation and remove
  RepositoryClassConsumer / RepositoryRegistry / DependencyResolver and
  the fixed @order 100/200/300 chain.
- decorators-model.md: add @component to the parallel-surface table;
  note strong interfaces AND method-level annotations for jobs /
  listeners / websockets; replace the @order wiring description.
- scheduled-jobs.md, message-listeners.md, websockets.md: show BOTH the
  strong interface and the method-level annotation on a @component
  (@scheduled / @Listener / @OnOpen/@OnMessage/@OnError/@onclose),
  keeping the reflective fallback note.
- extension-providers.md: present collection injection as the
  recommended Spring-style way to consume all providers, keeping
  Extensions.find as the programmatic / cross-runtime alternative.
- rest-apis.md, entities-and-persistence.md: controllers use
  constructor injection of the repository.
- security-and-roles.md: add a Java vs TypeScript User API parity table.
- languages/java.md: align its DI section with the new model.

Adds Sample project and SDK reference links across the Java sections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Refine Java handler model to two self-describing styles

Rewrite the Java sections of scheduled-jobs, message-listeners and
websockets to exactly two styles, never mixed on one @component class,
with no reflective by-name fallback:

- Style 1 is the self-describing interface (JobHandler.cron(),
  MessageHandler.destination()/kind(), WebsocketHandler.endpoint()) on a
  @component, carrying the binding itself - no class-level
  @Scheduled/@Listener/@websocket. Mirrors org.quartz.Job /
  jakarta.jms.MessageListener / TextWebSocketHandler.
- Style 2 is the method-level annotation (@Scheduled/@Listener), except
  websockets keep @websocket as a class annotation (the endpoint has no
  method-level home, like Jakarta @serverendpoint) with @OnOpen/
  @OnMessage/@OnError/@onclose methods.

Drop all reflective/hybrid wording and update the decorators-model
parallel-surface table and handler-styles section to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(develop): add "Coming from Spring Boot" page and real sample examples

Add a Spring Boot -> Dirigible mapping page (annotation table + key
differences) and link it from the develop index. Replace the placeholder
Java snippets in the building-block pages with the actual code from the
dirigiblelabs/sample-java-* repos, showing both the self-describing
interface and method-level annotation styles where applicable, each with
a "Sample project" link and an SDK reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(develop): nav entry, annotation-free Java extensions, data split, per-artifact comparisons

- Add "Coming from Spring Boot" to the Develop sidebar (config.mts)
- Rewrite extension-providers: extension point = plain interface,
  contribution = @component (no @Extension/@ExtensionPoint); split
  Consume-at-runtime into Java (List injection + Extensions.find) and TS
- working-with-data: split SQL examples into Java / TS subsections, drop
  the Multi-tenancy section
- coming-from-spring-boot: add side-by-side per-artifact examples
  (Listener, Job, WebSocket, Controller, Repository, Extension) using
  real classes from the sample-java-* repos

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(develop): Spring-then-Dirigible side-by-side per artifact; drop Key differences section

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(develop): split Java vs TS/JS examples into per-language subsections

Reorganize security-and-roles, working-with-files-and-cms, and
working-with-git develop pages so each topic separates its Java and
TypeScript/JavaScript examples under ### Java / ### TypeScript / JavaScript
subheadings (Java first), matching the convention already used on the data,
listeners, and jobs pages. Where a feature is config-only or facade-only,
that is stated explicitly instead of forcing a split. No documented APIs
or behavior changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(develop): separate Java/TS examples (rest-apis, entities); keep DI 'How it is wired' implementation-agnostic

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(develop): remove tenancy sections from entities-and-persistence and working-with-files-and-cms

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(sdk): align Java SDK reference with Spring-style bean container (PR #6051)

- component: @component bean + field/constructor/parameter @Inject + collection
  injection; @repository is a plain bean; new Beans facade page + sidebar entry
- job: @scheduled is method-level only; JobHandler is self-describing (cron()+run())
- messaging: @Listener is method-level only; MessageHandler is self-describing
- net: @websocket + @onx, or self-describing WebsocketHandler; drop reflective fallback
- extensions: remove @Extension/@ExtensionPoint; plain-interface points + @component
  contributions; find(Class) no longer throws checked exception
- http: @controller is a managed bean (constructor injection, no no-arg ctor needed)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(sdk): use Beans.get(JavaEntityStore) for client access, not BeanProvider

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: update client-Java model to Spring-style @component container

Sweep the help docs and the Java-decorators blog so the Java model
matches eclipse-dirigible/dirigible #6051: SDK annotations under
org.eclipse.dirigible.sdk.*, @Component/@Inject DI with constructor
injection preferred, @repository extends JavaRepository<T>, the Beans
facade instead of the platform-internal BeanProvider, method-level
@scheduled(expression=...), and plain-interface extension points with
@component contributions consumed via List<T> / Extensions.find.

Removes the retired Java surface (@Extension/@ExtensionPoint,
@scheduled(cron=...), the JavaClassConsumer fan-out and the named
internals) from the Java sections only. TypeScript content, the classic
registry artefacts, and docs/help/develop|api|sdk are left unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(develop): drop removed Java @Extension/@ExtensionPoint refs and stale meta-annotation list

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(develop): add SDK imports to the Dirigible Java samples in 'Coming from Spring Boot'

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(develop): add Spring/Jakarta imports to the Spring examples in 'Coming from Spring Boot'

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(develop): drop the feature-branch annotation from the extension sample link

The sample-java-* PRs are merged to master, so the extension-provider sample now lives on
master — reference the repo without the (branch feat/spring-style-extension-injection) suffix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants