Eventos enviados pelo servidor (SSE) no JAX-RS

Eventos enviados pelo servidor (SSE) no JAX-RS

1. Visão geral

O Server-Sent Events (SSE) é uma especificação baseada em HTTP que fornece uma maneira de estabelecer uma conexão monocanal de longa duração do servidor para o cliente.

O cliente inicia a conexão SSE usando o tipo de mídiatext/event-stream no cabeçalhoAccept.

Mais tarde, é atualizado automaticamente sem solicitar o servidor.

Podemos verificar mais detalhes sobre a especificação emthe official spec.

Neste tutorial, apresentaremos a nova implementação JAX-RS 2.1 de SSE.

Portanto, veremos como podemos publicar eventos com a API do servidor JAX-RS. Além disso, exploraremos como podemos consumi-los pela API do cliente JAX-RS ou apenas por um cliente HTTP como a ferramentacurl.

2. Compreendendo eventos de SSE

Um evento SSE é um bloco de texto composto pelos seguintes campos:

  • Event: o tipo de evento. O servidor pode enviar muitas mensagens de tipos diferentes e o cliente pode apenas ouvir um tipo específico ou pode processar diferentemente cada tipo de evento

  • Data: a mensagem enviada pelo servidor. Podemos ter muitas linhas de dados para o mesmo evento

  • Id: o id do evento, usado para enviar o cabeçalhoLast-Event-ID, após uma nova tentativa de conexão. É útil, pois pode impedir o servidor de enviar eventos já enviados

  • Retry: o tempo, em milissegundos, para o cliente estabelecer uma nova conexão quando a corrente for perdida. O último Id recebido será enviado automaticamente através do sheaderLast-Event-ID 

  • :‘: este é um comentário e é ignorado pelo cliente

Além disso, dois eventos consecutivos são separados por uma nova linha dupla ‘ ’.

Além disso, os dados no mesmo evento podem ser gravados em várias linhas, como pode ser visto no exemplo a seguir:

event: stock
id: 1
: price change
retry: 4000
data: {"dateTime":"2018-07-14T18:06:00.285","id":1,
data: "name":"GOOG","price":75.7119}

event: stock
id: 2
: price change
retry: 4000
data: {"dateTime":"2018-07-14T18:06:00.285","id":2,"name":"IBM","price":83.4611}

EmJAX RS, um evento SSE é abstraído pela interfaceSseEvent _, or more precisely, by the two subinterfaces _OutboundSseEventeInboundSseEvent.

While the OutboundSseEvent is used on the Server API and designs a sent event, the InboundSseEvent is used by the Client API and abstracts a received event.

3. Publicação de eventos SSE

Agora que discutimos o que é um evento SSE, vamos ver como podemos construí-lo e enviá-lo a um cliente HTTP.

3.1. Configuração do Projeto

Já temosa tutorial sobre a configuração de um projeto Maven baseado em JAX RS. Sinta-se à vontade para dar uma olhada lá para ver como configurar dependências e começar a usar o JAX RS.

3.2. Método de recurso SSE

Um método SSE Resource é um método JAX RS que:

  • Pode produzir um tipo de mídiatext/event-stream

  • Tem um parâmetroSseEventSink injetado, para onde os eventos são enviados

  • Também pode ter um parâmetroSse injetado que é usado como um ponto de entrada para criar um criador de eventos

@GET
@Path("prices")
@Produces("text/event-stream")
public void getStockPrices(@Context SseEventSink sseEventSink, @Context Sse sse) {
    //...
}

Em conseqüência, o cliente deve fazer a primeira solicitação HTTP, com o seguinte cabeçalho HTTP:

Accept: text/event-stream

3.3. A instância SSE

Uma instância do SSE é um bean de contexto que o JAX RS Runtime disponibilizará para injeção.

Poderíamos usá-lo como uma fábrica para criar:

  • OutboundSseEvent.Builder – nos permite criar eventos então

  • SseBroadcaster – nos permite transmitir eventos para vários assinantes

Vamos ver como isso funciona:

@Context
public void setSse(Sse sse) {
    this.sse = sse;
    this.eventBuilder = sse.newEventBuilder();
    this.sseBroadcaster = sse.newBroadcaster();
}

Agora, vamos nos concentrar no criador de eventos. OutboundSseEvent.Builder é responsável por criar oOutboundSseEvent:

OutboundSseEvent sseEvent = this.eventBuilder
  .name("stock")
  .id(String.valueOf(lastEventId))
  .mediaType(MediaType.APPLICATION_JSON_TYPE)
  .data(Stock.class, stock)
  .reconnectDelay(4000)
  .comment("price change")
  .build();

Como podemos ver,the builder has methods to set values for all event fields shown above. Além disso, o métodomediaType() é usado para serializar o objeto Java do campo de dados em um formato de texto adequado.

Por padrão, o tipo de mídia do campo de dados étext/plain. Portanto, ele não precisa ser especificado explicitamente ao lidar com o tipo de dadosString.

Caso contrário, se quisermos lidar com um objeto personalizado, precisamos especificar o tipo de mídia ou fornecer umMessageBodyWriter.The JAX RS Runtime provides MessageBodyWriters for the most known media types personalizado.

A instância Sse também possui dois atalhos de construtores para criar um evento apenas com o campo de dados ou o tipo e os campos de dados:

OutboundSseEvent sseEvent = sse.newEvent("cool Event");
OutboundSseEvent sseEvent = sse.newEvent("typed event", "data Event");

3.4. Enviando Evento Simples

Agora que sabemos como criar eventos e entendemos como um Recurso SSE funciona. Vamos enviar um evento simples.

A interfaceSseEventSink abstrai uma única conexão HTTP. O tempo de execução do JAX-RS pode disponibilizá-lo apenas por injeção no método de recurso SSE.

Enviar um evento é tão simples quanto invocarSseEventSink.send(). 

No próximo exemplo, enviará várias atualizações de estoque e, eventualmente, fechará o fluxo de eventos:

@GET
@Path("prices")
@Produces("text/event-stream")
public void getStockPrices(@Context SseEventSink sseEventSink /*..*/) {
    int lastEventId = //..;
    while (running) {
        Stock stock = stockService.getNextTransaction(lastEventId);
        if (stock != null) {
            OutboundSseEvent sseEvent = this.eventBuilder
              .name("stock")
              .id(String.valueOf(lastEventId))
              .mediaType(MediaType.APPLICATION_JSON_TYPE)
              .data(Stock.class, stock)
              .reconnectDelay(3000)
              .comment("price change")
              .build();
            sseEventSink.send(sseEvent);
            lastEventId++;
        }
     //..
    }
    sseEventSink.close();
}

Depois de enviar todos os eventos, o servidor fecha a conexão invocando explicitamente o métodoclose() ou, de preferência, usandotry-with-resource,, poisSseEventSink estende a interfaceAutoClosable:

try (SseEventSink sink = sseEventSink) {
    OutboundSseEvent sseEvent = //..
    sink.send(sseEvent);
}

Em nosso aplicativo de exemplo, podemos ver isso em execução se visitarmos:

http://localhost:9080/sse-jaxrs-server/sse.html

3.5. Transmitindo eventos

Broadcasting é o processo pelo qual os eventos são enviados para vários clientes simultaneamente. Isso é realizado pela APISseBroadcaster e é feito em três etapas simples:

Primeiro, criamos o objetoSseBroadcaster a partir de um contexto Sse injetado, conforme mostrado anteriormente:

SseBroadcaster sseBroadcaster = sse.newBroadcaster();

Em seguida, os clientes devem se inscrever para poder receber eventos Sse. Isso geralmente é feito em um método de recurso SSE em que uma instância de contextoSseEventSink é injetada:

@GET
@Path("subscribe")
@Produces(MediaType.SERVER_SENT_EVENTS)
public void listen(@Context SseEventSink sseEventSink) {
    this.sseBroadcaster.register(sseEventSink);
}

E finalmente,we can trigger the event publishing by invoking the broadcast() method:

@GET
@Path("publish")
public void broadcast() {
    OutboundSseEvent sseEvent = //...;
    this.sseBroadcaster.broadcast(sseEvent);
}

Isso enviará o mesmo evento para cadaSseEventSink. registrado

Para mostrar a transmissão, podemos acessar este URL:

http://localhost:9080/sse-jaxrs-server/sse-broadcast.html

E então podemos acionar a transmissão invocando o método de recurso broadcast ():

curl -X GET http://localhost:9080/sse-jaxrs-server/sse/stock/publish

4. Consumir eventos SSE

Para consumir um evento SSE enviado pelo servidor, podemos usar qualquer cliente HTTP, mas para este tutorial, usaremos a API do cliente JAX RS.

4.1. API JAX RS Client para SSE

Para iniciar a API do cliente para SSE, precisamos fornecer dependências para a implementação do JAX RS Client.

Aqui, usaremos a implementação do cliente Apache CXF:


    org.apache.cxf
    cxf-rt-rs-client
    ${cxf-version}


    org.apache.cxf
    cxf-rt-rs-sse
    ${cxf-version}

OSseEventSource é o coração desta API e é construído a partir doWebTarget.

Começamos ouvindo os eventos de entrada que são abstraídos pela interfaceInboundSseEvent:

Client client = ClientBuilder.newClient();
WebTarget target = client.target(url);
try (SseEventSource source = SseEventSource.target(target).build()) {
    source.register((inboundSseEvent) -> System.out.println(inboundSseEvent));
    source.open();
}

Assim que a conexão for estabelecida, o consumidor de evento registrado será chamado para cada * InboundSseEvent *.

Podemos então usar o métodoreadData() para ler os dados originaisString:

String data = inboundSseEvent.readData();

Ou podemos usar a versão sobrecarregada para obter o Objeto Java desserializado usando o tipo de mídia adequado:

Stock stock = inboundSseEvent.readData(Stock.class, MediaType.Application_Json);

Aqui, fornecemos apenas um consumidor de eventos simples que imprime o evento recebido no console.

5. Conclusão

Neste tutorial, focamos em como usar os eventos enviados pelo servidor no JAX RS 2.1. Fornecemos um exemplo que mostra como enviar eventos para um único cliente e também como transmitir eventos para vários clientes.

Por fim, consumimos esses eventos usando a API do cliente JAX-RS.

Como de costume, o código deste tutorial pode ser encontradoover on Github.