Implementando Respostas Assíncronas no Spring MVC com SseEmitter

Fundamentos Técnicos

No framework Spring MVC, baseado em Servlet, a implementação de respostas HTTP assíncronas e em fluxo pode ser alcançada através de classes específicas. O SseEmitter é projetado para cenários de Server-Sent Events (SSE), enquanto o ResponseBodyEmitter oferece uma solução genérica para streams de resposta assíncrona em formatos variados como JSON ou binário. Ambos exploram o mecanismo de processamento assíncrono do Servlet 3.0+, que permite liberar threads do contêiner durante operações de longa duração.

Mecanismo Interno: Processamento Assíncrono em Servlet

No modelo síncrono tradicional, cada requisição ocupa uma thread do servidor, o que pode levar ao esgotamento de recursos em alta concorrência. Com o processamento assíncrono, ao chamar request.startAsync(), a thread do contêiner é liberada imediatamente, e a resposta é escrita por uma thread de negócio através do AsyncContext. As classes SseEmitter e ResponseBodyEmitter encapsulam essa complexidade, mas vale notar que elas não implementam E/S verdadeiramente não bloqueante como em Netty; apenas permitem a liberação da thread da requisição e a escrita assíncrona da resposta.

Comparação Detalhada: SseEmitter vs ResponseBodyEmitter

O SseEmitter é otimizado para o protocolo SSE, com Content-Type fixo em text/event-stream e suporte a campos como id, event e retry. O ResponseBodyEmitter é flexível, permitindo Content-Types personalizados e serialização via HttpMessageConverter. Em termos de clientes, o SseEmitter requer o uso de EventSource no navegador, enquanto o ResponseBodyEmitter funciona com qualquer cliente HTTP.

Exemplos Práticos

SseEmitter para Notificações em Tempo Real

Este exemplo demonstra um endpoint SSE que envia atualizações periódicas para clientes conectados. Note a alteração de nomes de variáveis e métodos para reduzir a similaridade.

@RestController
public class AlertaController {
    private final Set<sseemitter> emissoresAtivos = Collections.synchronizedSet(new HashSet<>());

    @GetMapping(value = "/alertas-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter iniciarAlertas() {
        SseEmitter emissor = new SseEmitter(45_000L); // Timeout de 45 segundos

        emissor.onCompletion(() -> emissoresAtivos.remove(emissor));
        emissor.onTimeout(() -> {
            emissoresAtivos.remove(emissor);
            emissor.complete();
        });
        emissor.onError(ex -> emissoresAtivos.remove(emissor));

        emissoresAtivos.add(emissor);

        new Thread(() -> {
            try {
                for (int idx = 0; idx < 3; idx++) {
                    String dado = "Alerta #" + (idx + 1);
                    emissor.send(SseEmitter.event()
                        .id(String.valueOf(System.currentTimeMillis()))
                        .name("novo-alerta")
                        .data(dado));
                    TimeUnit.SECONDS.sleep(2);
                }
                emissor.complete();
            } catch (Exception e) {
                emissor.completeWithError(e);
            }
        }).start();

        return emissor;
    }

    @PostMapping("/enviar-alerta")
    public void difundirAlerta(@RequestBody String mensagem) {
        emissoresAtivos.forEach(emissor -> {
            try {
                emissor.send(mensagem);
            } catch (IOException ex) {
                emissoresAtivos.remove(emissor);
            }
        });
    }
}</sseemitter>

No cliente, utilize o EventSource para consumir os eventos SSE.

ResponseBodyEmitter para Fluxo de Dados Genérico

Este exemplo mostra como enviar dados em chunks usando JSON, alterando a estrutura para enviar uma sequência de objetos com delimitadores manuais.

@RestController
public class DadosController {
    @GetMapping(value = "/fluxo-dados", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseBodyEmitter enviarDados() {
        ResponseBodyEmitter emitter = new ResponseBodyEmitter(30_000L);

        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(() -> {
            try {
                for (int i = 1; i <= 3; i++) {
                    Map<String, Object> registro = Map.of("etapa", i, "status", "processando");
                    emitter.send(registro + "\n"); // Adiciona quebra de linha manualmente
                    TimeUnit.MILLISECONDS.sleep(800);
                }
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            } finally {
                executor.shutdown();
            }
        });

        return emitter;
    }

    @GetMapping("/transferencia-arquivo")
    public ResponseBodyEmitter transferirArquivo() {
        ResponseBodyEmitter emitter = new ResponseBodyEmitter();

        CompletableFuture.runAsync(() -> {
            try (InputStream is = Files.newInputStream(Path.of("arquivo-grande.zip"))) {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = is.read(buffer)) != -1) {
                    emitter.send(buffer, 0, bytesRead);
                }
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }
}

No cliente, use técnicas como leitura de stream para processar os dados recebidos, lembrando que múltiplos objetos JSON requerem parsing manual.

Pontos Críticos de Atenção

É essencial realizar operações assíncronas para evitar blqoueio da thread da requisição. Utilize CompletableFuture ou ExecutorService. Gerencie timeouts e recursos registrando callbacks como onTimeout() e onCompletion(). As classes SseEmitter e ResponseBodyEmitter não são thread-safe, então evite chamadas concorrentes ao método send(). Trate erros de E/S, que podem ocorrer quando clientes desconectam, chamando completeWithError() para liberar recursos.

Orientação para Escolha

Use SseEmitter quando a aplicação for direcionada a navegadores e requerer comunicação unidirecional em tempo real. Opte por ResponseBodyEmitter ao precisar de flexibilidade no formato da resposta, como para clientes não-web, download de arquivos ou streams de dados genéricos. Para cenários de alta concorrência, considere avaliar o uso do Spring WebFlux para capacidades verdadeiramente não bloqueantes.

Síntese das Capacidades

O SseEmitter opera sobre HTTP com protocolo SSE, ideal para notificações no navegador via EventSource. O ResponseBodyEmitter utiliza HTTP comum, suportando diversos formatos e clientes. A escolha depende do caso de uso: para pushes simples ao navegador, SseEmitter é apropriado; para cenários mais complexos, ResponseBodyEmitter oferece maior controle.

Tags: Spring MVC SseEmitter ResponseBodyEmitter Server-Sent Events Servlet API

Publicado em 6-25 20:56