Skip to content

declarative websockets#3917

Merged
jknack merged 5 commits intojooby-project:mainfrom
kliushnichenko:feat/declarative-websockets
Apr 21, 2026
Merged

declarative websockets#3917
jknack merged 5 commits intojooby-project:mainfrom
kliushnichenko:feat/declarative-websockets

Conversation

@kliushnichenko
Copy link
Copy Markdown
Contributor

So,

@WebSocketRoute("/chat/{username}")
public class ChatWebsocket {

  @OnConnect
  public String onConnect(WebSocket ws, Context ctx) {
    return "welcome";
  }

  @OnMessage
  public Map<String, String> onMessage(WebSocket ws, Context ctx, WebSocketMessage message) {
    return Map.of("echo", message.value());
  }

  @OnClose
  public void onClose(WebSocket ws, Context ctx, WebSocketCloseStatus status) {}

  @OnError
  public void onError(WebSocket ws, Context ctx, Throwable cause) {}
}

Will produce

@io.jooby.annotation.Generated(ChatWebsocket.class)
public class ChatWebsocketWs_ implements io.jooby.Extension {
    protected java.util.function.Function<io.jooby.Context, ChatWebsocket> factory;

    public ChatWebsocketWs_() {
      this(io.jooby.SneakyThrows.singleton(ChatWebsocket::new));
    }

    public ChatWebsocketWs_(ChatWebsocket instance) {
      setup(ctx -> instance);
    }

    public ChatWebsocketWs_(io.jooby.SneakyThrows.Supplier<ChatWebsocket> provider) {
      setup(ctx -> provider.get());
    }

    public ChatWebsocketWs_(io.jooby.SneakyThrows.Function<Class<ChatWebsocket>, ChatWebsocket> provider) {
      setup(ctx -> provider.apply(ChatWebsocket.class));
    }

    private void setup(java.util.function.Function<io.jooby.Context, ChatWebsocket> factory) {
      this.factory = factory;
    }

    public void install(io.jooby.Jooby app) throws Exception {
      app.ws("/chat/{username}", this::wsInit);
    }

    private void wsInit(io.jooby.Context ctx, io.jooby.WebSocketConfigurer configurer) {
      /** See {@link ChatWebsocket#onConnect(io.jooby.WebSocket, io.jooby.Context)} */
      configurer.onConnect(ws -> {
        var c = this.factory.apply(ctx);
        var __wsReturn = c.onConnect(ws, ctx);
        ws.send(__wsReturn);
      });

      /** See {@link ChatWebsocket#onMessage(io.jooby.WebSocket, io.jooby.Context, io.jooby.WebSocketMessage)} */
      configurer.onMessage((ws, message) -> {
        var c = this.factory.apply(ctx);
        var __wsReturn = c.onMessage(ws, ctx, message);
        ws.render(__wsReturn);
      });

      /** See {@link ChatWebsocket#onClose(io.jooby.WebSocket, io.jooby.Context, io.jooby.WebSocketCloseStatus)} */
      configurer.onClose((ws, status) -> {
        var c = this.factory.apply(ctx);
        c.onClose(ws, ctx, status);
      });

      /** See {@link ChatWebsocket#onError(io.jooby.WebSocket, io.jooby.Context, Throwable)} */
      configurer.onError((ws, cause) -> {
        var c = this.factory.apply(ctx);
        c.onError(ws, ctx, cause);
      });
    }
}

and can be registered over

{
    ws(new ChatWebsocketWs_());
}

Annotation name is @WebSocketRoute to avoid the clash with io.jooby.WebSocket interface.
If we want a nice annotation like @WebSocket -> need to rename io.jooby.WebSocket into io.jooby.WebSocketConnection or something.

@jknack
Copy link
Copy Markdown
Member

jknack commented Apr 20, 2026

Will, look closer in a bit. For now why not @Path instead of @WebSocketRoute. Also, should we do the same for SSE?

@kliushnichenko
Copy link
Copy Markdown
Contributor Author

why not @path instead of @WebSocketRoute

@WebSocketRoute makes it vividly recognizable as a WS handler, easier to catch in apt.

@Path can be. If @Path + @OnConnect/@OnMessage is sufficient to catch in apt

@jknack
Copy link
Copy Markdown
Member

jknack commented Apr 20, 2026

Think path fit better and we don't introduce a new annotation.

@jknack
Copy link
Copy Markdown
Member

jknack commented Apr 20, 2026

@kliushnichenko
Copy link
Copy Markdown
Contributor Author

upd:

  • removed @WebSocketRoute in favor of @Path
  • @Path is optional
  • @OnConnect or @OnMessage annotation is the minimal required condition to treat the class as a WS handler

@jknack
Copy link
Copy Markdown
Member

jknack commented Apr 21, 2026

looks good. Should we add support for parameter binding? Beside the WebSocketMessage, like onMessage(MyBean bean); onMessage(String message);, etc... WebSocketMessage it is a io.jooby.value.Value we probably can reuse binding from Rest/MVC generator.

@kliushnichenko
Copy link
Copy Markdown
Contributor Author

Should we add support for parameter binding?

Good point, just added it, a bit hacky, but it seems to work fine, and no changes to MvcParameter

Copy link
Copy Markdown
Member

@jknack jknack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love it, thank you

@jknack jknack added this to the 4.5.0 milestone Apr 21, 2026
@jknack jknack merged commit e3a6d7c into jooby-project:main Apr 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants