Skip to content

Optional typed handler interfaces for @Scheduled / @Listener / @Websocket#6010

Open
delchev wants to merge 1 commit into
masterfrom
java-typed-handlers
Open

Optional typed handler interfaces for @Scheduled / @Listener / @Websocket#6010
delchev wants to merge 1 commit into
masterfrom
java-typed-handlers

Conversation

@delchev

@delchev delchev commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Completes the typed-SDK story started with @ExtensionPoint: every decorator that has a runtime callback contract now ships an optional companion interface. Implementing it gives the user compile-time signature checking + IDE autocomplete + refactoring; the engine dispatches the callback via a direct virtual call instead of Method.invoke. Classes that don't implement the interface keep working unchanged through the existing reflective fallback — strictly additive.

New interfaces

Under org.eclipse.dirigible.sdk.*:

Decorator Optional contract Methods
@Scheduled org.eclipse.dirigible.sdk.job.JobHandler void run()
@Listener org.eclipse.dirigible.sdk.messaging.MessageHandler void onMessage(String), default void onError(String) {}
@Websocket org.eclipse.dirigible.sdk.net.WebsocketHandler all four lifecycle methods default to no-op — override only what you need

Engine-side dispatch

  • ScheduledClassConsumer — picks the typed path when instance instanceof JobHandler, falls back to looking up run() by name otherwise. Both log which path was chosen at INFO so the dispatch mode is visible in ops logs.
  • ListenerClassConsumer — same pattern. The JMS receive callback funnels through a small Dispatcher abstraction (typed vs reflective record implementations) so the message-handling code path stays uniform regardless of which form the user chose.
  • WebsocketClassConsumer — unchanged. It only registers the handler in JavaWebsocketRegistry; actual dispatch lives in WebsocketProcessor (engine-websockets) which already reflects by method name. Reflection finds the interface's default method body just as readily as a hand-rolled method of the same name, so users still get the typed-interface benefit (compile-time signature check + IDE) without engine-websockets needing to depend on api-modules-java.

Companion sample-side change

Already merged: dirigiblelabs/sample-java-entity-decorators#6CleanupJob implements JobHandler, OrderListener implements MessageHandler, ChatHandler implements WebsocketHandler. The four single-decorator standalone samples (sample-java-{job,listener,websocket,extension}-decorator) intentionally stay on the reflective form so the fallback path also runs on every CI build.

Example

@Websocket(name = "Java Chat", endpoint = "java-chat")
public class ChatHandler implements WebsocketHandler {
    @Override public void onMessage(String text, String from) { ... }
    // onOpen, onError, onClose inherit the no-op default — no boilerplate
}

Test plan

  • Local: mvn -P unit-tests clean install — BUILD SUCCESS in 6:05.
  • Local: JavaEngineIT — 4/4 pass.
  • CI: integration-tests-h2/postgresql/mssql — exercises the typed path (JavaEntityDecoratorsSampleProjectIT clones the now-typed sample) and the reflective fallback (JavaJobDecoratorSampleProjectIT + JavaListenerDecoratorSampleProjectIT + JavaWebsocketDecoratorSampleProjectIT still on the un-typed form) in the same suite.

🤖 Generated with Claude Code

…cket

Completes the typed-SDK story started with @ExtensionPoint: every
decorator that has a runtime callback contract now ships an optional
companion interface. Implementing it gives the user compile-time
signature checking + IDE autocomplete + refactoring; the engine
dispatches the callback via a direct virtual call instead of
Method.invoke. Classes that don't implement the interface keep working
through the existing reflective fallback — strictly additive.

Three interfaces under org.eclipse.dirigible.sdk.*:

- job.JobHandler          — void run()
- messaging.MessageHandler — void onMessage(String) + default onError(String)
- net.WebsocketHandler    — all four lifecycle methods default to no-op,
                             so users only override what they care about.

ScheduledClassConsumer / ListenerClassConsumer pick the typed path when
`instance instanceof XHandler`, fall back to method-by-name reflection
otherwise. Both log which path was chosen at INFO so the dispatch mode
is visible in operational logs. ListenerClassConsumer extracts a small
Dispatcher abstraction so the JMS message-receive callback stays
uniform across both paths.

WebsocketClassConsumer remains unchanged — it only registers the
instance in JavaWebsocketRegistry; the actual dispatch lives in
WebsocketProcessor (engine-websockets), which already reflects by
method name. Reflection finds the interface's method body just as
readily as a hand-rolled method of the same name, so users still get
the typed-interface benefit (IDE + signature check) without
engine-websockets needing to depend on api-modules-java.

README adds a "Optional typed handler interfaces" section with the
mapping and a WebsocketHandler example showing partial overrides via
default methods.

Companion sample-side PR landed at
dirigiblelabs/sample-java-entity-decorators#6 — CleanupJob,
OrderListener and ChatHandler now implement the new interfaces. The
four single-decorator standalone samples stay on the reflective form
so the fallback path is also exercised by CI.

Verified locally with `mvn -P unit-tests clean install` (BUILD SUCCESS
in 6:05) and JavaEngineIT (4/4 pass).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Fires when the listener container catches an exception while delivering a message. Default is a
* no-op so implementations only override it when they need to react to delivery failures.
*/
default void onError(String error) {
* @param message the text payload
* @param from a stable identifier for the originating session
*/
default void onMessage(String message, String from) {
* @param message the text payload
* @param from a stable identifier for the originating session
*/
default void onMessage(String message, String from) {
}

/** Fires when the underlying transport reports an error. */
default void onError(String error) {
delchev added a commit to dirigible-io/dirigible-io.github.io that referenced this pull request Jun 11, 2026
Pairs with the platform change in eclipse-dirigible/dirigible#6010, which
ships three companion interfaces alongside the existing decorators:

- @scheduled  -> sdk.job.JobHandler        (single void run())
- @Listener   -> sdk.messaging.MessageHandler  (onMessage(String), default onError(String))
- @websocket  -> sdk.net.WebsocketHandler     (4 default no-op lifecycle methods)

Implementing the interface is opt-in. Classes that don't implement it
still work via method-by-name reflection. The typed path gives
compile-time signature checking, IDE autocomplete + refactoring, default
no-op methods for callbacks you don't care about (WebsocketHandler,
MessageHandler.onError), and direct (non-reflective) dispatch.

Updates:

- /sdk/job/decorators        - new JobHandler section, example switched
                                to `implements JobHandler`.
- /sdk/messaging/decorators  - new MessageHandler section, both queue +
                                topic examples switched to typed form;
                                topic example shows the default onError
                                no-op covering the rest.
- /sdk/net/decorators        - new WebsocketHandler section, example
                                shortened to a single onMessage override
                                (the other three callbacks default to
                                no-op via the interface).
- /help/develop/scheduled-jobs, message-listeners, websockets - each
                                Java code block switched to the typed
                                form with a one-paragraph note about
                                the opt-in nature and a link to the
                                matching /sdk/ page.
- /help/develop/decorators-model - parallel-surface table mentions the
                                Java optional interfaces in the
                                @scheduled / @Listener / @websocket
                                rows, plus a new "Optional typed
                                contracts" paragraph after the
                                consumer-ordering section.

Build verified clean (`npm run docs:build`, 25s).

Co-Authored-By: Claude Opus 4.7 <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