Use Server-Sent Event in Spring 4.2

Today, the Spring Framework was released to 4.2 RC2. In Spring 4.2, better application events and Server-Sent Event(SSE) are supported.

In this article, I'll introduce you to the two new features.

What's Server-Sent Event

Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via an HTTP connection. The Server-Sent Events EventSource API is standardized as part of HTML5 by the W3C.

Server-sent event is a standard describing how servers can initiate data transmission toward clients once an initial client connection has been established. They are commonly used to send message updates or continuous data streams to a browser client and are designed to enhance native, cross-browser streaming through a JavaScript API called EventSource, through which a client requests a particular URL in order to receive an event stream.

Web browser support for Server-Sent Events

BrowserSupportedNotes
Internet ExplorerNo 
Mozilla FirefoxYesStarting with Firefox 6.0
Google ChromeYesStarting with Chrome 6
OperaYesStarting with Opera 11
SafariYesStarting with Safari 5.0

Setup Maven

Since Spring 4.2 hasn't been released to the Maven central repository, we need to set up our own repository.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <properties>
        <spring.version>4.2.0.RC2</spring.version>
    </properties>
    <repositories>
        <repository>
            <id>Springframework Milestone</id>
            <url>http://repo.spring.io/milestone</url>
        </repository>
    </repositories>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        ...
    </dependencies>
    ...
</project>

Use Server-Sent Event in Spring Controller

First of all, let me introduce you to the basic usage of Server-Sent Event.
The Spring provide us a SseEmitter class with can return an HTTP response whose content type is text/event-stream.

Before we start, we need to add sync-support for Spring. To do this, you can edit the web.xml as following.

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>

Then, we can add a method in the Spring Controller.

@RequestMapping("/getRealTimeMessage.action")
public SseEmitter getRealTimeMessageAction(
        HttpServletRequest request) {
    SseEmitter sseEmitter = new SseEmitter();
    // You can send message here
    sseEmitter.send("Message #1");
    return sseEmitter;
}

// Do something with sseEmitter
// You can also send message in another method
sseEmitter.send("Message #2");

// Complete Send Message
sseEmitter.complete();

Use Server-Sent Event with Message Queue

In a project, I used ActiveMQ to communicate with another process. When I receive a message from the message queue, onMessage() method will be triggered. I want to push the message to the browser using Server-Sent Event. In this section, I'll tell you how to do it.

First of all, let's take a quick glance at onMessage() method.

@Component
public class MessageReceiver implements MessageListener {
    /* (non-Javadoc)
     * @see javax.jms.MessageListener#onMessage(javax.jms.Message)
     */
    public void onMessage(Message message) {
        if ( message instanceof MapMessage ) {
            final MapMessage mapMessage = (MapMessage) message;

            try {
                String event = mapMessage.getString("event");
                Long submissionId = mapMessage.getLong("submissionId");

                eventPublisher.publish(new SubmissionEvent(this, submissionId, "Message"))
            } catch (JMSException ex) {
                LOGGER.catching(ex);
            }
        }
    }

    /**
     * New feature in Spring 4.2.
     */
    @Autowired
    private ApplicationEventPublisher eventPublisher;
}

Here's the definition of SubmissionEvent.

public class SubmissionEvent extends ApplicationEvent {
    public SubmissionEvent(Object source, long submissionId, String message) {
        super(source);
        this.submissionId = submissionId;
        this.message = message;
    }

    // getters and setters

    private final long submissionId;

    private final String message;
}

As you see, I used ApplicationEventPublisher in the class to publish an event. This event will be caught by a listener.

@Component
public class ApplicationEventListener {
    @EventListener
    public void submissionEventHandler(SubmissionEvent event) throws IOException {
        long submissionId = event.getSubmissionId();
        String message = event.getMessage();
        SseEmitter sseEmitter = sseEmitters.get(submissionId);

        if ( sseEmitter == null ) {
            LOGGER.warn(String.format("CANNOT get the SseEmitter for submission #%d.", submissionId));
            return;
        }
        sseEmitter.send(message);
    }

    public void addSseEmitters(long submissionId, SseEmitter sseEmitter) {
        sseEmitters.put(submissionId, sseEmitter);
    }

    /**
     * The list of the objects of SseEmitter.
     * The key of the map stands for submissionId.
     * The value of the map is the corresponding SseEmitter object.
     */
    private static Map<Long, SseEmitter> sseEmitters = new Hashtable<Long, SseEmitter>();
}

At last, register the SseEmitter to ApplicationEventListener in controller.

@RequestMapping("/getRealTimeMessage.action")
public SseEmitter getRealTimeMessageAction(
        @RequestParam(value = "submissionId", required = true) long submissionId,
        HttpServletRequest request) throws IOException {
    Submission submission = submissionService.getSubmission(submissionId);

    if ( submission == null ) {
        throw new ResourceNotFoundException();
    }
    SseEmitter sseEmitter = new SseEmitter();
    applicationEventListener.addSseEmitters(submissionId, sseEmitter);
    return sseEmitter;
}

Try it out!

Sever-Sent-Event

Reference

  • http://stackoverflow.com/questions/31229015/sse-implementation-in-spring-rest
  • https://spring.io/blog/2015/02/11/better-application-events-in-spring-framework-4-2
  • http://docs.spring.io/spring/docs/4.2.0.RC2/spring-framework-reference/htmlsingle/#mvc-ann-async-http-streaming
  • https://en.wikipedia.org/wiki/Server-sent_events
Contact Us
  • Tencent AI Lab, Shenzhen, China
  • cshzxie [at] gmail [dot] com