Getting Started
Getting Started
Section titled “Getting Started”This chapter walks you through building a real-time chat endpoint with Atmosphere. By the end, you will have a running server that accepts WebSocket and SSE connections.
Prerequisites
Section titled “Prerequisites”- JDK 21 or later
- Maven 3.9+ (or use the Maven Wrapper
./mvnw)
Maven Dependency
Section titled “Maven Dependency”Add the Atmosphere runtime to your project:
<dependency> <groupId>org.atmosphere</groupId> <artifactId>atmosphere-runtime</artifactId> <version>LATEST</version> <!-- check Maven Central for latest --></dependency>All Atmosphere modules share the org.atmosphere group ID. The atmosphere-runtime artifact is the core framework that provides Broadcaster, AtmosphereResource, @ManagedService, and all transport support.
The Message Class
Section titled “The Message Class”Before writing the endpoint, define a simple data class to carry chat messages. This is a plain POJO that Jackson can serialize and deserialize:
package org.atmosphere.samples.chat;
import java.util.Date;
public class Message {
private String message; private String author; private long time;
public Message() { this("", ""); }
public Message(String author, String message) { this.author = author; this.message = message; this.time = new Date().getTime(); }
public String getMessage() { return message; } public String getAuthor() { return author; } public long getTime() { return time; }
public void setAuthor(String author) { this.author = author; } public void setMessage(String message) { this.message = message; } public void setTime(long time) { this.time = time; }}Encoder and Decoder
Section titled “Encoder and Decoder”Atmosphere uses Encoder and Decoder interfaces to convert between your domain objects and the wire format. Here is a JacksonEncoder that converts a Message to JSON:
package org.atmosphere.samples.chat;
import com.fasterxml.jackson.databind.ObjectMapper;import org.atmosphere.config.managed.Encoder;
import jakarta.inject.Inject;import java.io.IOException;
public class JacksonEncoder implements Encoder<Message, String> {
@Inject private ObjectMapper mapper;
@Override public String encode(Message m) { try { return mapper.writeValueAsString(m); } catch (IOException e) { throw new RuntimeException(e); } }}And the corresponding JacksonDecoder:
package org.atmosphere.samples.chat;
import com.fasterxml.jackson.databind.ObjectMapper;import org.atmosphere.config.managed.Decoder;
import jakarta.inject.Inject;import java.io.IOException;
public class JacksonDecoder implements Decoder<String, Message> {
@Inject private ObjectMapper mapper;
@Override public Message decode(String s) { try { return mapper.readValue(s, Message.class); } catch (IOException e) { throw new RuntimeException(e); } }}Notice that both the encoder and decoder use @Inject to receive an ObjectMapper. Atmosphere’s built-in CDI-like injection handles this automatically.
Your First @ManagedService Endpoint
Section titled “Your First @ManagedService Endpoint”This is the complete chat endpoint, taken directly from the Atmosphere chat sample (samples/chat/src/main/java/org/atmosphere/samples/chat/Chat.java):
package org.atmosphere.samples.chat;
import org.atmosphere.config.service.Disconnect;import org.atmosphere.config.service.Heartbeat;import org.atmosphere.config.service.ManagedService;import org.atmosphere.config.service.Ready;import org.atmosphere.cpr.AtmosphereResource;import org.atmosphere.cpr.AtmosphereResourceEvent;import org.atmosphere.cpr.Broadcaster;import org.atmosphere.samples.chat.custom.Config;import org.slf4j.Logger;import org.slf4j.LoggerFactory;
import jakarta.inject.Inject;import jakarta.inject.Named;import java.io.IOException;
import static org.atmosphere.cpr.ApplicationConfig.MAX_INACTIVE;
@Config@ManagedService(path = "/chat", atmosphereConfig = MAX_INACTIVE + "=120000")public class Chat { private final Logger logger = LoggerFactory.getLogger(Chat.class);
@Inject @Named("/chat") private Broadcaster broadcaster;
@Inject private AtmosphereResource r;
@Inject private AtmosphereResourceEvent event;
@Heartbeat public void onHeartbeat(final AtmosphereResourceEvent event) { logger.trace("Heartbeat send by {}", event.getResource()); }
@Ready public void onReady() { logger.info("Browser {} connected (broadcaster: {})", r.uuid(), broadcaster.getID()); }
@Disconnect public void onDisconnect() { if (event.isCancelled()) { logger.info("Browser {} unexpectedly disconnected", event.getResource().uuid()); } else if (event.isClosedByClient()) { logger.info("Browser {} closed the connection", event.getResource().uuid()); } }
@org.atmosphere.config.service.Message(encoders = {JacksonEncoder.class}, decoders = {JacksonDecoder.class}) public Message onMessage(Message message) throws IOException { logger.info("{} just sent {}", message.getAuthor(), message.getMessage()); return message; }}Here is what each piece does:
@ManagedService(path = "/chat")— registers this class as a real-time endpoint at/chat. Atmosphere creates aBroadcasterfor this path and subscribes every connecting client.atmosphereConfig = MAX_INACTIVE + "=120000"— sets the maximum inactivity timeout to 120 seconds.@Injectfields — Atmosphere injectsBroadcaster(via@Namedwith the path),AtmosphereResource, andAtmosphereResourceEventautomatically. Usesjakarta.inject.Injectandjakarta.inject.Named.@Ready— called when a client connection is suspended and ready to receive messages.@Disconnect— called when the client disconnects. TheAtmosphereResourceEventtells you whether the disconnect was clean (isClosedByClient()) or unexpected (isCancelled()).@Heartbeat— called when the client sends a heartbeat ping.@Message— called when a message is broadcast. Thedecodersattribute deserializes incoming JSON into aMessageobject. Theencodersattribute serializes the return value back to JSON before broadcasting. Returning a value from@Messagebroadcasts it to all subscribers on this path.
Running with Embedded Jetty
Section titled “Running with Embedded Jetty”To run the endpoint without a WAR container, use embedded Jetty. This is from the samples/embedded-jetty-websocket-chat sample:
package org.atmosphere.samples.chat;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;import org.eclipse.jetty.ee10.servlet.ServletHolder;import org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer;import org.eclipse.jetty.server.Server;import org.eclipse.jetty.server.ServerConnector;import org.atmosphere.cpr.ApplicationConfig;import org.atmosphere.cpr.AtmosphereServlet;
public class EmbeddedJettyWebSocketChat {
public static void main(String[] args) throws Exception { Server server = new Server(); ServerConnector connector = new ServerConnector(server); connector.setPort(8080); server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); context.setContextPath("/");
// Configure WebSocket BEFORE AtmosphereServlet init JakartaWebSocketServletContainerInitializer.configure(context, (servletContext, serverContainer) -> { });
// Register AtmosphereServlet ServletHolder atmosphereServlet = new ServletHolder(AtmosphereServlet.class); atmosphereServlet.setInitParameter( ApplicationConfig.ANNOTATION_PACKAGE, "org.atmosphere.samples.chat"); atmosphereServlet.setInitParameter( ApplicationConfig.WEBSOCKET_CONTENT_TYPE, "application/json"); atmosphereServlet.setInitParameter( ApplicationConfig.WEBSOCKET_SUPPORT, "true"); atmosphereServlet.setInitOrder(1); atmosphereServlet.setAsyncSupported(true); context.addServlet(atmosphereServlet, "/chat/*");
server.setHandler(context); server.start(); server.join(); }}The key configuration points:
| Parameter | Value | Purpose |
|---|---|---|
ANNOTATION_PACKAGE | "org.atmosphere.samples.chat" | Tells Atmosphere which package to scan for @ManagedService classes |
WEBSOCKET_CONTENT_TYPE | "application/json" | Sets the content type for WebSocket messages |
WEBSOCKET_SUPPORT | "true" | Enables WebSocket transport |
setInitOrder(1) | Ensures the servlet is loaded on startup | |
setAsyncSupported(true) | Required for long-polling and SSE transports |
Note that JakartaWebSocketServletContainerInitializer.configure() must be called before AtmosphereServlet initializes, so that the WebSocket ServerContainer is available in the ServletContext when Atmosphere starts up.
Running with Spring Boot
Section titled “Running with Spring Boot”If you prefer Spring Boot, add the starter dependency instead:
<dependency> <groupId>org.atmosphere</groupId> <artifactId>atmosphere-spring-boot-starter</artifactId> <version>LATEST</version> <!-- check Maven Central for latest --></dependency>The auto-configuration handles servlet registration for you. Your @ManagedService class is identical — only the path prefix differs by convention. From the Spring Boot chat sample (samples/spring-boot-chat):
@ManagedService(path = "/atmosphere/chat", atmosphereConfig = MAX_INACTIVE + "=120000")public class Chat {
@Inject @Named("/atmosphere/chat") private Broadcaster broadcaster;
@Inject private AtmosphereResource r;
@Inject private AtmosphereResourceEvent event;
@Ready public void onReady() { logger.info("Browser {} connected (broadcaster: {})", r.uuid(), broadcaster.getID()); }
@Disconnect public void onDisconnect() { if (event.isCancelled()) { logger.info("Browser {} unexpectedly disconnected", event.getResource().uuid()); } else if (event.isClosedByClient()) { logger.info("Browser {} closed the connection", event.getResource().uuid()); } }
@org.atmosphere.config.service.Message(encoders = {JacksonEncoder.class}, decoders = {JacksonDecoder.class}) public Message onMessage(Message message) throws IOException { logger.info("{} just sent {}", message.getAuthor(), message.getMessage()); return message; }}The only difference from the standalone version is the path (/atmosphere/chat instead of /chat). The @Inject, @Ready, @Disconnect, and @Message annotations work identically.
What Just Happened
Section titled “What Just Happened”With the code above, you now have a server that:
- Listens for client connections at
/chat(or/atmosphere/chatfor Spring Boot). - Auto-negotiates the transport — WebSocket if the client supports it, SSE or long-polling as fallback.
- Subscribes each connecting client to a
Broadcasterkeyed by the path. - When any client sends a JSON message, the
@Messagemethod decodes it, and the returned value is broadcast to all subscribers. - Heartbeats keep the connection alive. The
@Heartbeatmethod is called on each ping. - When a client disconnects, the
@Disconnectmethod fires and the resource is automatically removed from theBroadcaster.
AI Quick Start
Section titled “AI Quick Start”If your goal is to stream LLM texts to a browser, you can get there in under 20 lines. Add the AI module alongside the Spring Boot starter:
<dependency> <groupId>org.atmosphere</groupId> <artifactId>atmosphere-spring-boot-starter</artifactId> <version>LATEST</version> <!-- check Maven Central for latest --></dependency><dependency> <groupId>org.atmosphere</groupId> <artifactId>atmosphere-ai</artifactId> <version>LATEST</version> <!-- check Maven Central for latest --></dependency>Set your LLM credentials in application.properties:
atmosphere.ai.llm.provider=openaiatmosphere.ai.llm.api-key=${OPENAI_API_KEY}atmosphere.ai.llm.model=gpt-4oThen write the endpoint:
@AiEndpoint(path = "/atmosphere/ai-chat", systemPrompt = "You are a helpful assistant", conversationMemory = true)public class AiChat {
@Prompt public void onPrompt(String message, StreamingSession session) { session.stream(message); // sends to the LLM, streams streaming texts back to the client }}That’s it. @AiEndpoint handles connection lifecycle, transport negotiation, and virtual thread dispatch automatically. session.stream(message) auto-detects the AI framework on the classpath — swap atmosphere-ai for atmosphere-spring-ai or atmosphere-langchain4j and the same code works with a different backend.
On the client side, connect with atmosphere.js:
import { Atmosphere } from 'atmosphere.js';
const client = Atmosphere.newClient();const request = client.subscribe({ url: '/atmosphere/ai-chat', transport: 'websocket', fallbackTransport: 'sse', trackMessageLength: true, onMessage(response) { const message = response.responseBody; // Each message is a streaming text from the LLM — append to the UI document.getElementById('output').textContent += message; }});
// Send a promptrequest.push('What is the Atmosphere Framework?');For the full @AiEndpoint API — system prompts from files, @AiTool methods, guardrails, conversation memory, multi-model routing, and framework adapters — see @AiEndpoint & Streaming.
Next Steps
Section titled “Next Steps”Where to go next:
- AI streaming? Jump to @AiEndpoint & Streaming for the full AI platform API.
- Real-time pub/sub? Continue to @ManagedService Deep Dive to learn every attribute and lifecycle annotation.